湯本 開 7ca4249f04 Merged PR 742: ログアウトせずに認証切れまで待った後にTOPページからログインしようとするとエラーが発生する
## 概要
[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分)とかにすれば確認はしやすいはず
2024-02-09 08:12:26 +00:00

141 lines
4.8 KiB
TypeScript

import { useMsal } from "@azure/msal-react";
import { AppDispatch } from "app/store";
import { isIdToken, isTokenExpired } from "common/token";
import {
clearToken,
isAdminUser,
isApproveTier,
isStandardUser,
loadAccessToken,
loadRefreshToken,
} from "features/auth";
import { loginAsync, selectLocalStorageKeyforIdToken } from "features/login";
import React, { useCallback, useEffect } from "react";
import Footer from "components/footer";
import Header from "components/header";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { isErrorObject } from "common/errors";
import { TIERS } from "components/auth/constants";
const LoginPage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate();
const [, i18n] = useTranslation();
const localStorageKeyforIdToken = useSelector(
selectLocalStorageKeyforIdToken
);
// ログイン後の遷移先を決定する
const navigateToLoginedPage = useCallback(() => {
// 第一~第四階層の管理者はライセンス画面へ遷移
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",
});
dispatch(clearToken());
}, [instance, navigate, dispatch]);
const tokenSetAndNavigate = useCallback(
async (idToken: string) => {
// ログイン処理呼び出し
const { meta, payload } = await dispatch(loginAsync({ idToken }));
// ログイン失敗した場合、B2Cをログアウトしてからエラーページに遷移する
if (meta.requestStatus === "rejected") {
if (isErrorObject(payload)) {
// 未同意の規約がある場合は利用規約同意画面に遷移する
if (payload.error.code === "E010209") {
navigate("/terms");
return;
}
}
instance.logoutRedirect({
postLogoutRedirectUri: "/AuthError",
});
}
if (meta.requestStatus === "fulfilled") {
const accessToken = loadAccessToken();
const refreshToken = loadRefreshToken();
const url = `${
import.meta.env.VITE_DESK_TOP_APP_SCHEME
}:login?accessToken=${accessToken}&refreshToken=${refreshToken}&language=${
i18n.language
}`; // カスタムURLスキーム
const a = document.createElement("a");
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// ログイン成功した場合、適切なページに遷移する
navigateToLoginedPage();
}
},
[dispatch, i18n.language, instance, navigate, navigateToLoginedPage]
);
useEffect(() => {
// idTokenStringがあるか⇒認証中
// accessTokenがある場合⇒ログイン済みなのにブラウザバックでログイン画面に戻ってきた場合
// どちらもなければURL直打ち
(async () => {
// ローカルストレージにidTokenがある場合は取得する
let idTokenString: string | null = null;
if (localStorageKeyforIdToken !== null) {
idTokenString = localStorage.getItem(localStorageKeyforIdToken);
}
// idTokenがない(=正常なログインプロセス中でない)場合は有効なアクセストークンを所持しているか確認し、
// 有効であればログイン画面に遷移 or 無効であればトップページに遷移
if (idTokenString === null) {
const token = loadAccessToken();
// アクセストークンがない or 有効期限切れ場合はトップページに遷移
if (isTokenExpired(token)) {
navigate("/");
} else {
// 有効なアクセストークンがある場合はログイン画面に遷移
navigateToLoginedPage();
}
return;
}
const idTokenObject = JSON.parse(idTokenString);
if (isIdToken(idTokenObject)) {
await tokenSetAndNavigate(idTokenObject.secret);
}
})();
// 画面描画後のみ実行するため引数を設定しない
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<Header />
<h3>loading ...</h3>
<Footer />
</>
);
};
export default LoginPage;