Package Exports
- fuju-auth-react
Readme
fuju-auth-react
Fuju エコシステム向けの認証フロントエンド(React + TypeScript)。AuthCore(バックエンド)との Cookie ベースのセッションをクライアント側で橋渡しし、unstyled なコンポーネントとフックを提供する。<AuthProvider> をツリーの最上位に置き、<AuthGuard> で守られた子要素では useAuth() から常に認証済みユーザーが取れる。
- CSS を出さない。
className/style/ CSS カスタムプロパティでスタイルを当てる。 - ルーター非依存。 React Router、Next.js App Router、TanStack Router などと組み合わせられる。
- トークンハンドリング内蔵。 アクセストークンはメモリ、リフレッシュトークンは
HttpOnlyCookie、サイレントリフレッシュはタイマー、ログアウトはタブ間でブロードキャスト。 - 依存が軽い。
peerDependenciesはreact/react-domのみ。ランタイムdependenciesなし。
インストール
npm install fuju-auth-reactpeerDependencies は react ^18 || ^19(react-dom も同様)。
Quick Start
AuthProvider + AuthGuard + useAuth の最小構成。
import { AuthGuard, AuthProvider, useAuth } from 'fuju-auth-react';
function Dashboard() {
const { user, logout } = useAuth();
return (
<>
<img src={user.iconUrl ?? '/default-avatar.png'} alt="" />
<p>こんにちは、{user.displayName} さん</p>
<p>ID: {user.id}</p>
<button onClick={logout}>ログアウト</button>
</>
);
}
export function App() {
return (
<AuthProvider
config={{
baseURL: 'https://auth.fuju.example.com',
onUnauthenticatedNavigate: (path) => window.location.assign(path),
}}
>
<AuthGuard>
<Dashboard />
</AuthGuard>
</AuthProvider>
);
}<AuthGuard> の内側では useAuth().user は常に非 null。ガードの外側(共有ヘッダーなど)で条件分岐したい場合は useAuthStatus を使う。
プロバイダ
AuthProvider
React ツリーの最上位に置く DI ルート。AuthConfig を受け取り、内部ストアの生成・bootstrap(Cookie からのセッション復元)・silent refresh・タブ間同期を担当する。config.loadingFallback は bootstrap 中のプレースホルダーとして描画される。props 型は AuthProviderProps。
リロードやブラウザ再起動をまたいだセッション保持の長さは、AuthCore が発行するリフレッシュ Cookie の Max-Age / SameSite / Secure / Path に依存する。フロント側ではアクセストークンをメモリにしか置かないため、Max-Age が無い(session cookie)と必ずブラウザ再起動でログアウトされる。保持期間の要件は AuthCore 側と合わせて設計する。
import { useAuthStatus, type SocialProvider } from 'fuju-auth-react';
export function SocialCallback({
provider,
searchParams,
}: {
provider: SocialProvider;
searchParams: URLSearchParams;
}) {
const { completeSocialCallback } = useAuthStatus();
useEffect(() => {
const state = searchParams.get('state');
const code = searchParams.get('code');
if (!state || !code) return;
void completeSocialCallback(provider, { state, code });
}, []);
return <p>サインイン中…</p>;
}セッション保持の切り分け(onDiagnostic)
「リロード後に MFA 画面に戻る」「毎回ログイン画面に飛ばされる」などセッション保持の問題が出たときは、AuthConfig.onDiagnostic を噛ませると bootstrap / refresh / login / MFA verify / 401 ハンドリングの進行を観測できる。Network タブの Set-Cookie / リクエスト Cookie ヘッダと突き合わせる用途を想定している。
<AuthProvider
config={{
baseURL: 'https://auth.fuju.example.com',
onDiagnostic: (ev) => {
// 副作用の無い軽い実装が望ましい(console.log / バッファリングのみ)。
console.log('[fuju-auth]', ev);
},
}}
>
{/* ... */}
</AuthProvider>観測できる variant は AuthDiagnosticEvent(src/types.ts)を参照。現状は bootstrap-* / refresh-* / session-hint-* / unauthorized-from-api / login-success / mfa-verify-success が発火する。union は open ended なので、将来新しい variant を追加しても型を広げるだけで済む(未知の type は無視する実装を推奨)。
Cookie 属性の確認ポイント(Max-Age / SameSite=None; Secure / Path=/ / Domain / HttpOnly)は docs/frontend-flow.md §5.3.5 にチェックリストがある。
スキーマ定義
以下は src/** からの型定義の転載。JSDoc コメントはコード側と同じものを保持している。
AuthConfig
src/types.ts
export interface AuthConfig {
/** AuthCore のベース URL。例: `https://auth.fuju.example.com`。 */
baseURL: string;
/**
* 未認証状態で AuthGuard に到達したときに呼ばれる。ホスト側のルーターで
* `/login` などへ遷移させるために使う。未指定なら AuthGuard が組み込みの
* LoginForm を直接描画する。
*/
onUnauthenticatedNavigate?: (path: string) => void;
/**
* ソーシャルログインのコールバック URL。AuthCore 側の OAuth クライアントに
* 登録した値と一致させる。未指定なら `${location.origin}/auth/callback/:provider`。
*/
socialRedirectURI?: string;
/**
* デフォルト UI に並べるソーシャルログインボタンの一覧。`['google']` を渡すと
* LoginForm / RegisterForm に「Googleログイン」「Googleで登録」ボタンが並ぶ。
* 未指定 or 空配列ならソーシャルボタンは描画されない(メール + パスワードのみ)。
* AuthGuard が `loginWithSocial` を自動で配線するため、コンシューマー側での
* ハンドラ実装は不要。ただし `loginFormComponent` / `registerFormComponent` を
* カスタム差し替えした場合は `providers` と `loginWithSocial` を自力で渡す。
*/
providers?: readonly SocialProvider[];
/** fetch 実装の注入。テストで MSW を差し込むときなどに使用。 */
fetch?: typeof fetch;
/** silent refresh のタイマーを無効化する。テスト・SSR 検証で使うことがある。 */
disableSilentRefresh?: boolean;
/** bootstrap 実行中に描画されるプレースホルダー。 */
loadingFallback?: ReactNode;
/** 組み込みの `MFAChallenge` を差し替える。 */
mfaChallengeComponent?: ComponentType<MFAChallengeProps>;
/** 組み込みの `MFASetupWizard` を差し替える。 */
mfaSetupComponent?: ComponentType<MFASetupWizardProps>;
/** 組み込みの `LoginForm` を差し替える。差し替え時は `providers` の配線も自前で行う。 */
loginFormComponent?: ComponentType<LoginFormProps>;
/** 組み込みの `RegisterForm` を差し替える。差し替え時は `providers` の配線も自前で行う。 */
registerFormComponent?: ComponentType<RegisterFormProps>;
/**
* 組み込みの `SocialSignupPublicIdForm` を差し替える。ソーシャル経由で初回
* サインアップしたユーザーに対して公開 ID を設定させる UI をカスタマイズする。
*/
socialSignupComponent?: ComponentType<SocialSignupPublicIdFormProps>;
/**
* bootstrap / refresh / 401 ハンドリングなどの内部イベントを受け取る診断フック。
* 未指定なら何も通知しない。主にリロード直後のログアウト問題など、サーバー側の
* Cookie 設定とフロントの挙動を突き合わせるための観測点として使う。`console.log`
* に流して Network タブの `Set-Cookie` と突き合わせる用途を想定しているため、
* 副作用の無い軽い実装が望ましい。
*/
onDiagnostic?: (event: AuthDiagnosticEvent) => void;
}AuthProviderProps
src/AuthProvider.tsx
export interface AuthProviderProps {
config: AuthConfig;
children: ReactNode;
}AuthGuardProps
src/AuthGuard.tsx
export interface AuthGuardProps {
children: ReactNode;
required?: boolean;
enforceMFA?: boolean;
fallback?: ReactNode;
}User
src/types.ts
export interface User {
readonly id: string;
readonly publicId: string;
readonly displayName: string;
readonly email: string;
readonly iconUrl: string | null;
readonly mfaEnabled: boolean;
readonly mfaVerified: boolean;
readonly linkedProviders: readonly SocialProvider[];
readonly createdAt: string;
}AuthError
src/types.ts
export interface AuthError extends Error {
readonly code: string;
readonly status: number;
readonly retryAfterSec?: number;
}AuthStatus
src/types.ts
export type AuthStatus =
| 'idle'
| 'authenticating'
| 'authenticated'
| 'mfa_required'
| 'unauthenticated'
| 'error';SocialProvider
src/types.ts
export type SocialProvider = 'google' | 'twitch' | 'x';LoginResult
src/types.ts
export type LoginResult = { kind: 'authenticated'; user: User } | { kind: 'mfa_required' };MFASetupResult
src/types.ts
export interface MFASetupResult {
readonly secret: string;
readonly qrCodeDataURL: string;
readonly recoveryCodes: readonly string[];
}LoginFormProps
src/types.ts
export interface LoginFormProps {
login: (input: { identifier: string; password: string }) => Promise<LoginResult>;
loginWithSocial: (provider: SocialProvider) => void;
register: (input: { email: string; password: string; publicId: string }) => Promise<User>;
providers?: readonly SocialProvider[];
onRegisterClick?: () => void;
className?: string;
style?: CSSProperties;
}RegisterFormProps
src/types.ts
export interface RegisterFormProps {
register: (input: { email: string; password: string; publicId: string }) => Promise<User>;
login?: (input: { identifier: string; password: string }) => Promise<LoginResult>;
loginWithSocial?: (provider: SocialProvider) => void;
providers?: readonly SocialProvider[];
onLoginClick?: () => void;
onSuccess?: (user: User) => void;
className?: string;
style?: CSSProperties;
}MFAChallengeProps
src/types.ts
export interface MFAChallengeProps {
verify: (input: { code?: string; recoveryCode?: string }) => Promise<void>;
attempts: number;
cancel: () => void;
className?: string;
style?: CSSProperties;
}MFASetupWizardProps
src/types.ts
export interface MFASetupWizardProps {
onComplete?: () => void;
className?: string;
style?: CSSProperties;
}SocialSignupPublicIdFormProps
src/types.ts
export interface SocialSignupPublicIdFormProps {
user: User;
submit: (publicId: string) => Promise<void>;
skip: () => void;
className?: string;
style?: CSSProperties;
}AuthErrorFallbackProps
src/components/AuthErrorFallback.tsx(src/types.ts からは export されていないが利用者向けの型として転載)
export interface AuthErrorFallbackProps {
error: AuthError | null;
onRetry?: () => void;
className?: string;
style?: CSSProperties;
}ProfileEditorProps
src/components/ProfileEditor.tsx(src/types.ts からは export されていないが利用者向けの型として転載)
export interface ProfileEditorProps {
className?: string;
style?: CSSProperties;
}AuthContextAuthenticated
src/hooks/useAuth.ts(useAuth の戻り値型)
export interface AuthContextAuthenticated {
status: 'authenticated';
user: User;
logout: () => Promise<void>;
refreshProfile: () => Promise<User>;
updatePublicID: (next: string) => Promise<User>;
updateIcon: (file: File) => Promise<User>;
setupMFA: () => Promise<MFASetupResult>;
enableMFA: (code: string) => Promise<User>;
disableMFA: (code: string) => Promise<User>;
connectSocial: (provider: SocialProvider) => void;
disconnectSocial: (provider: SocialProvider) => Promise<User>;
}AuthContextAll
src/hooks/useAuthStatus.ts
export type AuthContextAll =
| { status: 'idle' | 'authenticating'; user: null }
| { status: 'authenticated' | 'mfa_required'; user: User | null }
| { status: 'unauthenticated' | 'error'; user: null; error?: AuthError };AuthStatusActions
src/hooks/useAuthStatus.ts
export interface AuthStatusActions {
login: (input: { identifier: string; password: string }) => Promise<LoginResult>;
loginWithSocial: (provider: SocialProvider) => void;
register: (input: { email: string; password: string; publicId: string }) => Promise<User>;
logout: () => Promise<void>;
verifyMFA: (input: { code?: string; recoveryCode?: string }) => Promise<void>;
cancelMFA: () => void;
completeSocialCallback: (
provider: SocialProvider,
params: { state: string; code: string },
) => Promise<User>;
confirmSocialSignupPublicId: (publicId: string) => Promise<User>;
skipSocialSignupPublicId: () => void;
}UseAuthStatusReturn
src/hooks/useAuthStatus.ts(useAuthStatus の戻り値型)
export type UseAuthStatusReturn = AuthContextAll &
AuthStatusActions & {
mfaAttempts: number;
needsPublicIdSetup: boolean;
};ErrorCode
src/ErrorCodes.ts(ErrorCodes の value union)
export const ErrorCodes = {
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',
USER_NOT_FOUND: 'USER_NOT_FOUND',
ACCOUNT_LOCKED: 'ACCOUNT_LOCKED',
EMAIL_NOT_VERIFIED: 'EMAIL_NOT_VERIFIED',
PUBLIC_ID_ALREADY_EXISTS: 'PUBLIC_ID_ALREADY_EXISTS',
PUBLIC_ID_RESERVED: 'PUBLIC_ID_RESERVED',
PUBLIC_ID_FORMAT_INVALID: 'PUBLIC_ID_FORMAT_INVALID',
EMAIL_INVALID: 'EMAIL_INVALID',
PASSWORD_TOO_SHORT: 'PASSWORD_TOO_SHORT',
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
TOKEN_INVALID: 'TOKEN_INVALID',
TOKEN_REVOKED: 'TOKEN_REVOKED',
TOKEN_MALFORMED: 'TOKEN_MALFORMED',
MFA_REQUIRED: 'MFA_REQUIRED',
MFA_NOT_ENABLED: 'MFA_NOT_ENABLED',
MFA_ALREADY_ENABLED: 'MFA_ALREADY_ENABLED',
TOTP_CODE_INVALID: 'TOTP_CODE_INVALID',
RECOVERY_CODE_INVALID: 'RECOVERY_CODE_INVALID',
CLIENT_INVALID: 'CLIENT_INVALID',
CLIENT_NOT_FOUND: 'CLIENT_NOT_FOUND',
SOCIAL_PROVIDER_INVALID: 'SOCIAL_PROVIDER_INVALID',
SOCIAL_AUTH_FAILED: 'SOCIAL_AUTH_FAILED',
INVALID_REQUEST: 'INVALID_REQUEST',
MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
FILE_TOO_LARGE: 'FILE_TOO_LARGE',
FILE_FORMAT_INVALID: 'FILE_FORMAT_INVALID',
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
METHOD_NOT_ALLOWED: 'METHOD_NOT_ALLOWED',
NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
NETWORK_ERROR: 'NETWORK_ERROR',
} as const;
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];ライセンス
MIT