Merged PR 548: ログイン回り修正

## 概要
[Task1828: IDトークンを一度しか使えないようにする](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1828)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
ログイン時にRedisを参照し、同じIDトークンでアクセスされていた場合はアクセスを拒否する。
初めて認証で使われるIDトークンの場合は、Redisに保存する(有効期限は300秒(IDトークンの有効期限の最小値))
ログイン後、クライアントはローカルストレージの不要な情報を破棄する(accessToken,refreshToken,displayInfo以外)
- 影響範囲(他の機能にも影響があるか)
新規機能のため、なし。

## レビューポイント
- keyの形式id-token:{idトークンの中身}は想定通りか。
- ログイン後のローカルストレージの状態が想定通りか。
- controllerやservice層の実装箇所が妥当か。
- stg環境、本番環境のIDトークンの有効期限を最小値に設定するタスクを備忘用PBIに切り出しました。
https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%2021-1?workitem=3041

## UIの変更
https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task1828?csf=1&web=1&e=1Sc1VP

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
水本 祐希 2023-11-08 02:22:20 +00:00
parent 86d17d6729
commit 0cdb0f4267
9 changed files with 61 additions and 24 deletions

View File

@ -39,6 +39,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [
"E10501",
];
/**
*
* @const {string[]}
*/
export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"];
/**
*
* @const {number}

View File

@ -1,17 +1,15 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { getAccessToken, setToken } from "features/auth";
import {
AuthApi,
UsersApi,
GetMyUserResponse,
TokenResponse,
} from "../../api/api";
import { setToken } from "features/auth/authSlice";
import { KEYS_TO_PRESERVE } from "components/auth/constants";
import { AuthApi, UsersApi, GetMyUserResponse } from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ErrorObject, createErrorObject } from "../../common/errors";
export const loginAsync = createAsyncThunk<
TokenResponse,
{
//
},
{
idToken: string;
},
@ -35,6 +33,7 @@ export const loginAsync = createAsyncThunk<
idToken,
type: "web",
});
// アクセストークン・リフレッシュトークンをlocalStorageに保存
thunkApi.dispatch(
setToken({
@ -42,8 +41,19 @@ export const loginAsync = createAsyncThunk<
refreshToken: data.refreshToken,
})
);
// ローカルストレージに残すキー
const keysToPreserve = KEYS_TO_PRESERVE;
return data;
// すべてのローカルストレージキーを取得
const allKeys = Object.keys(localStorage);
// 特定のキーを除外して削除
allKeys.forEach((key) => {
if (!keysToPreserve.includes(key)) {
localStorage.removeItem(key);
}
});
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
@ -66,8 +76,7 @@ export const getUserInfoAsync = createAsyncThunk<
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);

View File

@ -1 +1,3 @@
export const ADB2C_PREFIX = 'adb2c-external-id:';
export const IDTOKEN_PREFIX = 'id-token:';

View File

@ -1,4 +1,4 @@
import { ADB2C_PREFIX } from './constants';
import { ADB2C_PREFIX, IDTOKEN_PREFIX } from './constants';
/**
* ADB2Cのユーザー格納用のキーを生成する
@ -17,3 +17,12 @@ export const makeADB2CKey = (externalId: string): string => {
export const restoreAdB2cID = (key: string): string => {
return key.replace(ADB2C_PREFIX, '');
};
/**
* ADB2CのIDトークン格納用のキーを生成する
* @param idToken IDトークン
* @returns
*/
export const makeIDTokenKey = (idToken: string): string => {
return `${IDTOKEN_PREFIX}${idToken}`;
};

View File

@ -38,6 +38,8 @@ import { TermsService } from '../../features/terms/terms.service';
import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module';
import { TermsModule } from '../../features/terms/terms.module';
import { CacheModule } from '@nestjs/common';
import { RedisModule } from '../../gateways/redis/redis.module';
import { RedisService } from '../../gateways/redis/redis.service';
export const makeTestingModule = async (
datasource: DataSource,
@ -77,6 +79,7 @@ export const makeTestingModule = async (
SortCriteriaRepositoryModule,
WorktypesRepositoryModule,
TermsRepositoryModule,
RedisModule,
CacheModule.register({ isGlobal: true }),
],
providers: [
@ -90,6 +93,7 @@ export const makeTestingModule = async (
TemplatesService,
WorkflowsService,
TermsService,
RedisService,
],
})
.useMocker(async (token) => {

View File

@ -33,14 +33,15 @@ import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES, TIERS } from '../../constants';
import jwt from 'jsonwebtoken';
import { AccessToken, RefreshToken } from '../../common/token';
import { makeIDTokenKey } from '../../common/cache';
import { RedisService } from '../../gateways/redis/redis.service';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(
// TODO「タスク 1828: IDトークンを一度しか使えないようにする」で使用する予定
// private readonly redisService: RedisService,
private readonly authService: AuthService,
private readonly redisService: RedisService,
) {}
@Post('token')
@ -76,6 +77,18 @@ export class AuthController {
}
const context = makeContext(uuidv4());
const key = makeIDTokenKey(body.idToken);
const isTokenExists = await this.redisService.get<boolean>(key);
if (!isTokenExists) {
// IDトークンがキャッシュに存在しない場合(idTokenの有効期限をADB2Cの有効期限と合わせる(300秒))
await this.redisService.set(key, true, 300);
} else {
// IDトークンがキャッシュに存在する場合エラー
throw new HttpException(
makeErrorResponse('E000106'),
HttpStatus.UNAUTHORIZED,
);
}
// 同意済み利用規約バージョンが最新かチェック
const isAcceptedLatestVersion =

View File

@ -4,15 +4,10 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module';
import { RedisService } from '../../gateways/redis/redis.service';
@Module({
imports: [
ConfigModule,
AdB2cModule,
UsersRepositoryModule,
TermsRepositoryModule,
],
imports: [ConfigModule, AdB2cModule, UsersRepositoryModule],
controllers: [AuthController],
providers: [AuthService],
providers: [AuthService, RedisService],
})
export class AuthModule {}

View File

@ -723,7 +723,6 @@ describe('updateDelegationAccessToken', () => {
}
});
});
const idTokenPayload = {
exp: 9000000000,
nbf: 1000000000,

View File

@ -4,7 +4,7 @@ import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-grap
import { CACHE_MANAGER, Inject, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import { B2cMetadata, JwkSignKey } from '../../common/token';
import { B2cMetadata, IDToken, JwkSignKey } from '../../common/token';
import { AdB2cResponse, AdB2cUser } from './types/types';
import { isPromiseRejectedResult } from './utils/utils';
import { Context } from '../../common/log';