./knowledge-base/frontend/src/auth/AuthContext.tsx
/**
* 認証コンテキスト(Okta 実装)
*
* OktaAuth の認証状態をアプリ全体に提供する。
* AuthContextValue のインターフェースは変えていないため、
* useAuth() を使っている各ページの変更は不要。
*/
import { createContext, useContext, useEffect, useState } from "react";
import type { ReactNode } from "react";
import type { AuthState } from "@okta/okta-auth-js";
import { oktaAuth } from "./oktaConfig";
export interface AuthUser {
/** Okta の sub クレーム */
id: string;
/** Okta の name クレーム */
name: string;
/** Okta の email クレーム */
email: string;
}
export interface AuthContextValue {
/** 認証済みかどうか */
isAuthenticated: boolean;
/** 認証状態のロード中かどうか(初回チェック完了前は true) */
isLoading: boolean;
/** ログイン中ユーザー情報(未認証時は null) */
user: AuthUser | null;
/** Okta ログインページへリダイレクト */
login: () => void;
/** Okta セッションを破棄してログアウト */
logout: () => void;
/** API 呼び出し用 Access Token を取得 */
getAccessToken: () => string | undefined;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
/**
* 認証トークンを Cookie に保存する
* Lambda@Edge が /era-assist/* の認証確認に使用
* @param token Okta Access Token
*/
function saveTokenToCookie(token: string | undefined): void {
if (!token) {
// ログアウト時は Cookie を削除
document.cookie =
"okta_access_token=; path=/; max-age=0; SameSite=Lax; Secure";
return;
}
// JWT の exp クレームから有効期限を取得
try {
const payload = JSON.parse(atob(token.split(".")[1]));
const maxAge = payload.exp - Math.floor(Date.now() / 1000);
if (maxAge > 0) {
document.cookie = `okta_access_token=${token}; path=/; max-age=${maxAge}; SameSite=Lax; Secure`;
}
} catch {
// デコード失敗時はデフォルト 1時間
document.cookie = `okta_access_token=${token}; path=/; max-age=3600; SameSite=Lax; Secure`;
}
}
/** 認証コンテキストプロバイダー */
export function AuthProvider({ children }: { children: ReactNode }) {
const [authState, setAuthState] = useState<AuthState | null>(
oktaAuth.authStateManager.getAuthState(),
);
useEffect(() => {
// 認証状態が変化したら再描画
const handler = (newState: AuthState) => setAuthState(newState);
oktaAuth.authStateManager.subscribe(handler);
// コールバック処理中でなければサービス開始(トークン自動更新など)
if (!oktaAuth.isLoginRedirect()) {
oktaAuth.start();
}
return () => {
oktaAuth.authStateManager.unsubscribe(handler);
};
}, []);
// authState === null の間はロード中
const isLoading = authState === null;
const isAuthenticated = authState?.isAuthenticated ?? false;
const user: AuthUser | null = authState?.idToken?.claims
? {
id: String(authState.idToken.claims.sub ?? ""),
name: String(authState.idToken.claims.name ?? ""),
email: String(authState.idToken.claims.email ?? ""),
}
: null;
// 認証状態変化時にトークンを Cookie に同期する
useEffect(() => {
if (isAuthenticated) {
saveTokenToCookie(authState?.accessToken?.accessToken);
} else if (!isLoading) {
// ログアウトまたは未認証: Cookie が存在する場合は削除
saveTokenToCookie(undefined);
}
}, [isAuthenticated, isLoading, authState?.accessToken?.accessToken]);
const login = () => {
// 現在の URL(?redirect_to= 含む)を originalUri として保存する
// 認証後に LoginCallbackPage がここから redirect_to を取り出す
oktaAuth.signInWithRedirect({
originalUri:
window.location.pathname +
window.location.search +
window.location.hash,
});
};
const logout = () => {
oktaAuth.signOut();
};
const getAccessToken = (): string | undefined => {
return authState?.accessToken?.accessToken;
};
return (
<AuthContext.Provider
value={{
isAuthenticated,
isLoading,
user,
login,
logout,
getAccessToken,
}}
>
{children}
</AuthContext.Provider>
);
}
/** 認証コンテキストを取得するカスタムフック */
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}