diff --git a/dictation_client/src/components/auth/constants.ts b/dictation_client/src/components/auth/constants.ts index 34f51b0..649d569 100644 --- a/dictation_client/src/components/auth/constants.ts +++ b/dictation_client/src/components/auth/constants.ts @@ -39,6 +39,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [ "E10501", ]; +/** + * ローカルストレージに残すキー類 + * @const {string[]} + */ +export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"]; + /** * アクセストークンを更新する基準の秒数 * @const {number} diff --git a/dictation_client/src/features/login/operations.ts b/dictation_client/src/features/login/operations.ts index 07185ba..f320ac2 100644 --- a/dictation_client/src/features/login/operations.ts +++ b/dictation_client/src/features/login/operations.ts @@ -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); diff --git a/dictation_server/src/common/cache/constants.ts b/dictation_server/src/common/cache/constants.ts index b79dd78..9f588f6 100644 --- a/dictation_server/src/common/cache/constants.ts +++ b/dictation_server/src/common/cache/constants.ts @@ -1 +1,3 @@ export const ADB2C_PREFIX = 'adb2c-external-id:'; + +export const IDTOKEN_PREFIX = 'id-token:'; diff --git a/dictation_server/src/common/cache/index.ts b/dictation_server/src/common/cache/index.ts index 3355a54..4985492 100644 --- a/dictation_server/src/common/cache/index.ts +++ b/dictation_server/src/common/cache/index.ts @@ -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}`; +}; diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index 9c3d578..61c0396 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -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) => { diff --git a/dictation_server/src/features/auth/auth.controller.ts b/dictation_server/src/features/auth/auth.controller.ts index a07b666..89e4de6 100644 --- a/dictation_server/src/features/auth/auth.controller.ts +++ b/dictation_server/src/features/auth/auth.controller.ts @@ -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(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 = diff --git a/dictation_server/src/features/auth/auth.module.ts b/dictation_server/src/features/auth/auth.module.ts index 2e3dc4c..d86bf30 100644 --- a/dictation_server/src/features/auth/auth.module.ts +++ b/dictation_server/src/features/auth/auth.module.ts @@ -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 {} diff --git a/dictation_server/src/features/auth/auth.service.spec.ts b/dictation_server/src/features/auth/auth.service.spec.ts index 1be5e40..abeaf61 100644 --- a/dictation_server/src/features/auth/auth.service.spec.ts +++ b/dictation_server/src/features/auth/auth.service.spec.ts @@ -723,7 +723,6 @@ describe('updateDelegationAccessToken', () => { } }); }); - const idTokenPayload = { exp: 9000000000, nbf: 1000000000, diff --git a/dictation_server/src/gateways/adb2c/adb2c.service.ts b/dictation_server/src/gateways/adb2c/adb2c.service.ts index 69bd890..947148a 100644 --- a/dictation_server/src/gateways/adb2c/adb2c.service.ts +++ b/dictation_server/src/gateways/adb2c/adb2c.service.ts @@ -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';