JSPM

fuju-auth-react

0.1.2
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 25
  • Score
    100M100P100Q76180F

Unified authentication front-end for the Fuju ecosystem (React + TypeScript).

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 などと組み合わせられる。
  • トークンハンドリング内蔵。 アクセストークンはメモリ、リフレッシュトークンは HttpOnly Cookie、サイレントリフレッシュはタイマー、ログアウトはタブ間でブロードキャスト。
  • 依存が軽い。 peerDependenciesreact / react-dom のみ。ランタイム dependencies なし。

インストール

npm install fuju-auth-react

peerDependenciesreact ^18 || ^19react-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 は AuthDiagnosticEventsrc/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.tsxsrc/types.ts からは export されていないが利用者向けの型として転載)

export interface AuthErrorFallbackProps {
  error: AuthError | null;
  onRetry?: () => void;
  className?: string;
  style?: CSSProperties;
}

ProfileEditorProps

src/components/ProfileEditor.tsxsrc/types.ts からは export されていないが利用者向けの型として転載)

export interface ProfileEditorProps {
  className?: string;
  style?: CSSProperties;
}

AuthContextAuthenticated

src/hooks/useAuth.tsuseAuth の戻り値型)

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.tsuseAuthStatus の戻り値型)

export type UseAuthStatusReturn = AuthContextAll &
  AuthStatusActions & {
    mfaAttempts: number;
    needsPublicIdSetup: boolean;
  };

ErrorCode

src/ErrorCodes.tsErrorCodes の 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