From 7ca4249f042520d1de66ace48e4f15539bc69cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Fri, 9 Feb 2024 08:12:26 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20742:=20=E3=83=AD=E3=82=B0?= =?UTF-8?q?=E3=82=A2=E3=82=A6=E3=83=88=E3=81=9B=E3=81=9A=E3=81=AB=E8=AA=8D?= =?UTF-8?q?=E8=A8=BC=E5=88=87=E3=82=8C=E3=81=BE=E3=81=A7=E5=BE=85=E3=81=A3?= =?UTF-8?q?=E3=81=9F=E5=BE=8C=E3=81=ABTOP=E3=83=9A=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=81=8B=E3=82=89=E3=83=AD=E3=82=B0=E3=82=A4=E3=83=B3=E3=81=97?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=A8=E3=81=99=E3=82=8B=E3=81=A8=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=81=8C=E7=99=BA=E7=94=9F=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3680: ログアウトせずに認証切れまで待った後にTOPページからログインしようとするとエラーが発生する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3680) - 現象 - ブラウザバック対策のコードで、アクセストークンの寿命を意識せずにログイン後画面に遷移していたため、リフレッシュトークンとアクセストークンの再発行がSkipされた上で寿命が切れたトークンを使うような状態でログイン後画面に遷移してしまっていた - 対応 - /login, /authで、アクセストークンチェック→IdTokenを使ったログイン中プロセスかチェック の順番を変更 - 有効なアクセストークンがあったとしても、再ログインをしてるのであればリフレッシュトークン等の再発行を実施する形に変更 ## レビューポイント - Bugの現象が解消する以外の挙動の変更が発生してしまわないか - 特にブラウザバック等 - 実装方法で問題がありそうな部分はないか - 不要なTokenのクリア等はないか ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 ## 補足 - **可能であればローカルで挙動を確認していただきたいです** - .env.local の `ACCESS_TOKEN_LIFETIME_WEB` を60(=1分)とかにすれば確認はしやすいはず --- dictation_client/src/App.tsx | 13 ++---- dictation_client/src/common/token.ts | 13 ++++++ dictation_client/src/main.tsx | 11 ++++- dictation_client/src/pages/AuthPage/index.tsx | 41 +---------------- .../src/pages/ErrorPage/index.tsx | 4 +- .../src/pages/LoginPage/index.tsx | 45 ++++++++++--------- 6 files changed, 53 insertions(+), 74 deletions(-) diff --git a/dictation_client/src/App.tsx b/dictation_client/src/App.tsx index 9f35aca..1d47893 100644 --- a/dictation_client/src/App.tsx +++ b/dictation_client/src/App.tsx @@ -1,8 +1,6 @@ import AppRouter from "AppRouter"; import { BrowserRouter } from "react-router-dom"; -import { PublicClientApplication } from "@azure/msal-browser"; -import { MsalProvider, useMsal } from "@azure/msal-react"; -import { msalConfig } from "common/msalConfig"; +import { useMsal } from "@azure/msal-react"; import { useEffect, useLayoutEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import globalAxios, { AxiosError, AxiosResponse } from "axios"; @@ -19,7 +17,6 @@ const App = (): JSX.Element => { const { instance } = useMsal(); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [t, i18n] = useTranslation(); - const pca = new PublicClientApplication(msalConfig); useEffect(() => { const id = globalAxios.interceptors.response.use( (response: AxiosResponse) => response, @@ -70,11 +67,9 @@ const App = (): JSX.Element => { dispatch(closeSnackbar()); }} /> - - - - - + + + ); }; diff --git a/dictation_client/src/common/token.ts b/dictation_client/src/common/token.ts index ac7c3e9..88e42d3 100644 --- a/dictation_client/src/common/token.ts +++ b/dictation_client/src/common/token.ts @@ -76,3 +76,16 @@ export const getIdTokenFromLocalStorage = ( } return null; }; + +// JWTが有効期限切れかどうかを判定する +export const isTokenExpired = (token: string | null): boolean => { + if (token == null) { + return true; + } + const tokenObject = JSON.parse(atob(token.split(".")[1])); + if (isToken(tokenObject)) { + const now = Math.floor(Date.now() / 1000); + return tokenObject.exp < now; + } + return true; +}; diff --git a/dictation_client/src/main.tsx b/dictation_client/src/main.tsx index 1aac073..924f95b 100644 --- a/dictation_client/src/main.tsx +++ b/dictation_client/src/main.tsx @@ -3,18 +3,25 @@ import React from "react"; import { createRoot } from "react-dom/client"; import { I18nextProvider } from "react-i18next"; import { Provider } from "react-redux"; +import { PublicClientApplication } from "@azure/msal-browser"; +import { msalConfig } from "common/msalConfig"; +import { MsalProvider } from "@azure/msal-react"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import i18n from "./i18n"; +const pca = new PublicClientApplication(msalConfig); + const container = document.getElementById("root"); if (container) { const root = createRoot(container); root.render( - - + + + + ); diff --git a/dictation_client/src/pages/AuthPage/index.tsx b/dictation_client/src/pages/AuthPage/index.tsx index e8d81f9..c891d0f 100644 --- a/dictation_client/src/pages/AuthPage/index.tsx +++ b/dictation_client/src/pages/AuthPage/index.tsx @@ -10,14 +10,6 @@ import { import React, { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; -import { - clearToken, - isAdminUser, - isApproveTier, - isStandardUser, - loadAccessToken, -} from "features/auth"; -import { TIERS } from "components/auth/constants"; const AuthPage: React.FC = (): JSX.Element => { const { instance } = useMsal(); @@ -34,38 +26,7 @@ const AuthPage: React.FC = (): JSX.Element => { (async () => { try { - // ログイン済みの場合、ログイン後の遷移先を決定する - if (loadAccessToken()) { - // 第一~第四階層の管理者はライセンス画面へ遷移 - if ( - isApproveTier([ - TIERS.TIER1, - TIERS.TIER2, - TIERS.TIER3, - TIERS.TIER4, - ]) && - isAdminUser() - ) { - navigate("/license"); - return; - } - // 第五階層の管理者はユーザー画面へ遷移 - if (isApproveTier([TIERS.TIER5]) && isAdminUser()) { - navigate("/user"); - return; - } - // 一般ユーザーはdictationPageへ遷移 - if (isStandardUser()) { - navigate("/dictations"); - return; - } - // それ以外は認証エラー画面へ遷移 - instance.logoutRedirect({ - postLogoutRedirectUri: "/AuthError", - }); - clearToken(); - return; - } + // idTokenが有効セットされているかを確認する const loginResult = await instance.handleRedirectPromise(); if (loginResult && loginResult.account) { const { homeAccountId, idTokenClaims } = loginResult.account; diff --git a/dictation_client/src/pages/ErrorPage/index.tsx b/dictation_client/src/pages/ErrorPage/index.tsx index 4592ea0..6d82f50 100644 --- a/dictation_client/src/pages/ErrorPage/index.tsx +++ b/dictation_client/src/pages/ErrorPage/index.tsx @@ -1,8 +1,10 @@ import React from "react"; +import { Link } from "react-router-dom"; export const AuthErrorPage = (): JSX.Element => (
-

ログインに失敗しました

+

login failed


+ return to TopPage
); diff --git a/dictation_client/src/pages/LoginPage/index.tsx b/dictation_client/src/pages/LoginPage/index.tsx index c335bd3..8727452 100644 --- a/dictation_client/src/pages/LoginPage/index.tsx +++ b/dictation_client/src/pages/LoginPage/index.tsx @@ -1,6 +1,6 @@ import { useMsal } from "@azure/msal-react"; import { AppDispatch } from "app/store"; -import { isIdToken } from "common/token"; +import { isIdToken, isTokenExpired } from "common/token"; import { clearToken, isAdminUser, @@ -52,10 +52,10 @@ const LoginPage: React.FC = (): JSX.Element => { instance.logoutRedirect({ postLogoutRedirectUri: "/AuthError", }); - clearToken(); - }, [instance, navigate]); + dispatch(clearToken()); + }, [instance, navigate, dispatch]); - const tokenSet = useCallback( + const tokenSetAndNavigate = useCallback( async (idToken: string) => { // ログイン処理呼び出し const { meta, payload } = await dispatch(loginAsync({ idToken })); @@ -96,31 +96,32 @@ const LoginPage: React.FC = (): JSX.Element => { useEffect(() => { // idTokenStringがあるか⇒認証中 - // accessTokenがある場合⇒ログイン済み - // どちらもなければ直打ち + // accessTokenがある場合⇒ログイン済みなのにブラウザバックでログイン画面に戻ってきた場合 + // どちらもなければURL直打ち (async () => { - if (loadAccessToken()) { - navigateToLoginedPage(); - return; + // ローカルストレージにidTokenがある場合は取得する + let idTokenString: string | null = null; + if (localStorageKeyforIdToken !== null) { + idTokenString = localStorage.getItem(localStorageKeyforIdToken); } - // AADB2Cのログイン画面とLoginPageを経由していない場合はトップページに遷移する - if (!localStorageKeyforIdToken) { - navigate("/"); - return; - } - const idTokenString = localStorage.getItem(localStorageKeyforIdToken); - + // idTokenがない(=正常なログインプロセス中でない)場合は有効なアクセストークンを所持しているか確認し、 + // 有効であればログイン画面に遷移 or 無効であればトップページに遷移 if (idTokenString === null) { - navigate("/"); + const token = loadAccessToken(); + // アクセストークンがない or 有効期限切れ場合はトップページに遷移 + if (isTokenExpired(token)) { + navigate("/"); + } else { + // 有効なアクセストークンがある場合はログイン画面に遷移 + navigateToLoginedPage(); + } return; } - if (idTokenString) { - const idTokenObject = JSON.parse(idTokenString); - if (isIdToken(idTokenObject)) { - await tokenSet(idTokenObject.secret); - } + const idTokenObject = JSON.parse(idTokenString); + if (isIdToken(idTokenObject)) { + await tokenSetAndNavigate(idTokenObject.secret); } })(); // 画面描画後のみ実行するため引数を設定しない