Browser SDK (@auth-platform/browser)#

@auth-platform/browser は SPA 向けの Public Authentication API/OIDC クライアントである。Authorization Code + PKCE、トークンのメモリ保持、Hosted Login へのトップレベル遷移を提供する。

インストールと初期化#

pnpm add @auth-platform/browser
import { createBrowserAuthClient } from "@auth-platform/browser";

export const authClient = createBrowserAuthClient({
  baseUrl: import.meta.env.VITE_AUTH_API_BASE_URL,
  issuer: import.meta.env.VITE_AUTH_ISSUER,
  clientId: import.meta.env.VITE_AUTH_CLIENT_ID,
  redirectUri: import.meta.env.VITE_AUTH_REDIRECT_URI,
});

baseUrl は Public Authentication API の origin であり、Interaction Domain の origin を指定する。SDK は /v1/... を path に付加するため、baseUrl に /v1 は含めない。既定 Interaction Domain の本番環境での例は https://env-<environmentId の env_ 以降>.login.zeroword.smartcrab.ai である。GET /v1/environments/{environmentId} の interaction_domain から Interaction Domain を、issuer から Canonical Issuer を取得する。SPA の request は cross-origin になるため、web_spa Client の redirect_uris に callback URI を、allowed_origins に SPA の origin (例: http://localhost:5173) を登録する。issuer を省略した場合は、初回の token/authorize 操作時に GET /v1/config?client_id=... から取得して cache する。redirectUri は Client に登録した URI と完全一致させる。ブラウザーに埋め込む clientId は public identifier であり、client secret は SPA に含めない。

BrowserAuthClientConfig 既定値 説明
baseUrl: string 必須 Interaction Domain の Public Authentication API origin。例: https://env-<environmentId の env_ 以降>.login.zeroword.smartcrab.ai。/v1 は含めない
clientId: ClientId 必須 web_spa クライアントの ID
redirectUri: string 必須 登録済み callback URI
scope?: string openid profile email 空白区切りの OIDC scope
resource?: readonly string[] なし token grant に含める RFC 8707 resource indicator。各値を個別の resource parameter として送る
issuer?: string /v1/config から遅延取得 https://<issuer-host>/e/<environmentId> 形式の canonical issuer
fetch?: FetchPort global fetch request transport の差し替え
webAuthn?: WebAuthnBrowserPort navigatorWebAuthnPort WebAuthn ceremony の差し替え
redirect?: RedirectPort window.location.assign トップレベル navigation の差し替え
refreshTokenStorage?: RefreshTokenStoragePort なし refresh token 永続化の明示的 opt-in
now?: () => number Date.now epoch milliseconds の時計差し替え

refresh token を使う (refreshSession、restoreSession、期限切れ時の自動 refresh) には、scope に offline_access を含め、Client の allowed_scopes にも登録する。既定の scope (openid profile email) では refresh token が発行されず、5 分の access token 有効期限後に再認可が必要になる。

不正な baseUrl、clientId、未対応 token を含む scope、空の redirectUri は factory 呼び出し時に TypeError となる。network や wire data による期待される失敗は throw せず、Outcome<T, BrowserAuthError> として返る。

Vite + React の Hosted Login 例#

下記は http://localhost:5173/callback を redirect_uris に、http://localhost:5173 を allowed_origins に登録した web_spa Client を使う例である。VITE_AUTH_API_BASE_URL には https://<Interaction Domain> (末尾に /v1 を付けない)、VITE_AUTH_ISSUER には Canonical Issuer を設定する。Environment の interaction_domain と issuer は Management API の GET /v1/environments/{environmentId} から取得する。Vite の環境変数はブラウザーへ公開されるため、client secret を設定してはならない。

pnpm add @auth-platform/react react react-dom

TypeScript を使う場合は @types/react と @types/react-dom も開発依存に加える。

.env.local:

VITE_AUTH_API_BASE_URL=https://<Interaction Domain>
VITE_AUTH_ISSUER=https://id.zeroword.smartcrab.ai/e/<environmentId>
VITE_AUTH_CLIENT_ID=<clientId>
VITE_AUTH_REDIRECT_URI=http://localhost:5173/callback

src/main.tsx:

import { createRoot } from "react-dom/client";
import { useEffect, useRef, useState } from "react";
import { AuthProvider, useAuth, useAuthClient, useUser } from "@auth-platform/react";
import { createBrowserAuthClient } from "@auth-platform/browser";

const client = createBrowserAuthClient({
  baseUrl: import.meta.env.VITE_AUTH_API_BASE_URL,
  issuer: import.meta.env.VITE_AUTH_ISSUER,
  clientId: import.meta.env.VITE_AUTH_CLIENT_ID,
  redirectUri: import.meta.env.VITE_AUTH_REDIRECT_URI,
});

function App() {
  const auth = useAuth();
  const authClient = useAuthClient();
  const profile = useUser();
  const handledCallback = useRef(false);
  const [message, setMessage] = useState("");

  useEffect(() => {
    if (window.location.pathname !== "/callback" || handledCallback.current) return;
    handledCallback.current = true;
    void auth.handleRedirectCallback(window.location.href).then((result) => {
      window.history.replaceState(null, "", "/");
      setMessage(result.ok ? "ログインした" : result.error.type);
    });
  }, [auth.handleRedirectCallback]);

  if (auth.isSignedIn) {
    return <main>
      <p>{profile.user?.email ?? "プロフィールを読み込み中"}</p>
      <button onClick={() => void authClient.signOut()}>ログアウト</button>
      {message && <p role="status">{message}</p>}
    </main>;
  }

  return <main>
    <button onClick={() => {
      void authClient.beginHostedAuthorization().then((result) => {
        if (!result.ok) setMessage(result.error.type);
      });
    }}>ログイン</button>
    {message && <p role="status">{message}</p>}
  </main>;
}

createRoot(document.getElementById("root")!).render(
  <AuthProvider client={client}><App /></AuthProvider>,
);

beginHostedAuthorization() は /oauth2/authorize URL を作成してトップレベル遷移する。callback では新しい SPA instance が同じ tab の sessionStorage から短命 state を一度だけ取り出し、PKCE verifier とともに code を交換する。callback URL の query を処理した後に history から取り除き、code を UI や log に残さない。callback 処理を実行する component は一度だけ呼び出す。

トークンと callback state の保管#

  • Access token は client closure 内のメモリだけに保持され、snapshot、localStorage、sessionStorage、cookie には入らない。
  • Refresh token も既定ではメモリだけに保持する。refreshTokenStorage を渡した場合に限り、その port に refresh token を保存する。保存先はアプリが選択するため、平文の localStorage は使わない。refresh token は rotation/replay detection 対象だが、storage 上の窃取リスクをなくすものではない。
  • PKCE callback の再開に必要な { state, nonce, codeVerifier, redirectUri, createdAt } だけを、state を key にして sessionStorage に保存する。entry は callback 時に一度だけ消費され、10 分を超えた entry は拒否される。sessionStorage が使えない Node / SSR 環境では同じ client のメモリ内コピーにフォールバックする。
  • silent iframe による token renewal は行わない。reload 後に token を再利用するには refreshTokenStorage と restoreSession() を明示的に使うか、既存 Hosted Session に対して beginHostedAuthorization() でトップレベル再認可する。

Storage/navigation port signatures#

export interface RedirectPort {
  assign(url: string): void;
}

export interface RefreshTokenStoragePort {
  load(): Promise<string | null> | string | null;
  save(refreshToken: string): Promise<void> | void;
  clear(): Promise<void> | void;
}

createBrowserAuthClient(config: BrowserAuthClientConfig): BrowserAuthClient を使う。refreshTokenStorage は refresh token 自体を受け取る port であり、pending PKCE state の保存とは別である。

getSnapshot() が返す情報は status, accessTokenExpiresAt, scope, hasRefreshToken に限られ、生 token を含まない。getAccessToken() は access token を返す唯一のメソッドであり、期限切れの token は refresh token があるときだけ先に更新する。並行した refresh は一つに集約される。

BrowserAuthClient API#

以下の表では R<T> を Promise<Outcome<T, BrowserAuthError>> の略記として使う。Outcome は { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: E } である。

Config、transaction、Email OTP#

Signature 動作
getPublicConfig(): R<PublicConfigResponse> GET /v1/config?client_id=... を取得する
createTransaction(input?: CreateTransactionInput): R<BrowserTransaction> PKCE S256、state、nonce を生成して native_return transaction を作る。input は scope?, resource?, state?, nonce?
getTransaction(transactionId: string): R<TransactionGetResponse> transaction status を取得する
cancelTransaction(transactionId: string): R<TransactionCancelResponse> transaction を cancel する
startEmailOtp(transactionId: string, input: EmailStartRequest): R<EmailChallengeResponse> Email OTP を開始する
resendEmailOtp(transactionId: string, input: EmailResendRequest): R<EmailChallengeResponse> challenge の code を再送する
verifyEmailOtp(transactionId: string, input: EmailVerifyRequest): R<EmailVerifyResponse> challenge code を検証する。token は返さず、成功後に transaction を complete する

Passkey、Social、code exchange#

Signature 動作
getPasskeyRegistrationOptions(transactionId: string): R<PasskeyRegistrationOptionsResponse> transaction の登録 ceremony options を取得する
verifyPasskeyRegistration(transactionId: string, input: PasskeyRegistrationVerifyRequest): R<TransactionPasskeyRegistrationVerifyResponse> credential を検証する
startPasskeyRegistration(transactionId: string, input?: { readonly name?: string }): R<TransactionPasskeyRegistrationVerifyResponse> options → authenticator → verify を実行する
getPasskeyAuthenticationOptions(transactionId: string, input?: PasskeyAuthenticationOptionsRequest): R<PasskeyAuthenticationOptionsResponse> 認証 ceremony options を取得する。既定は request body {}
verifyPasskeyAuthentication(transactionId: string, input: PasskeyAuthenticationVerifyRequest): R<PasskeyAuthenticationVerifyResponse> assertion credential を検証する
startPasskeyAuthentication(transactionId: string, input?: PasskeyAuthenticationOptionsRequest): R<PasskeyAuthenticationVerifyResponse> discoverable credential を既定に options → authenticator → verify を実行する
startSocialSignIn(transactionId: string, provider: SocialProvider): R<SocialStartResponse> social sign-in を開始し、成功時に返却された URL へトップレベル遷移する
completeTransaction(transactionId: string): R<TransactionCompleteResponse> transaction を完了し、one-time authorization code を得る。response の redirect URI が設定値と異なる場合は invalid_response
exchangeAuthorizationCode(input: { readonly code: string; readonly state: string }): R<AuthenticatedSession> state に結び付く PKCE verifier で code を token endpoint に交換する。state は一度だけ消費する
createHostedAuthorizationUrl(): R<HostedAuthorization> { url, state } を作る。URL は {issuer}/oauth2/authorize
beginHostedAuthorization(): R<{ readonly state: string }> 上記 URL を作り、redirect.assign(url) でトップレベル遷移する
handleHostedAuthorizationCallback(callbackUrl: string): R<AuthenticatedSession> authorize/social callback URL の OAuth error を解析するか、code/state を token に交換する

Session、状態、/v1/me#

Signature 動作
getSnapshot(): BrowserAuthSnapshot 生 token を含まない現在の auth snapshot を返す
subscribe(listener: BrowserAuthListener): () => void snapshot 更新を購読し、戻り値で解除する
isPasskeySupported(): boolean WebAuthn runtime capability を返す。false の場合 passkey UI を隠す
getAccessToken(): Promise<Outcome<string, BrowserAuthError>> access token を返し、期限切れ時は可能なら refresh する
refreshSession(): R<AuthenticatedSession> refresh を明示実行する。並行要求は dedupe される
restoreSession(): R<AuthenticatedSession> opt-in refresh storage から token を読み、rotation して新しいメモリ内 session を作る
signOut(): Promise<Outcome<{ readonly signedOut: true }, BrowserAuthError>> refresh token の revoke を best-effort で行い、ローカル session を消す
getMe(): R<MeUser> Bearer 認証で現在 user を取得する
updateMe(input: MeUpdateRequest): R<MeUpdateResponse> profile を更新する
listMyPasskeys(): R<MePasskeysListResponse> 自分の passkeys を列挙する
getMyPasskeyRegistrationOptions(): R<MePasskeyRegistrationOptionsResponse> /v1/me 配下の passkey 登録 options を得る
verifyMyPasskeyRegistration(input: MePasskeyRegistrationVerifyRequest): R<MePasskeyRegistrationVerifyResponse> /v1/me 配下の passkey credential を検証する
registerMyPasskey(input?: { readonly name?: string }): R<MePasskeyRegistrationVerifyResponse> options → authenticator → verify を実行する
deleteMyPasskey(passkeyId: string): R<MePasskeyDeleteResponse> passkey を削除する
listMyIdentities(): R<MeIdentitiesListResponse> link 済み identity を列挙する
linkMyIdentity(provider: SocialProvider, input: MeIdentityLinkRequest): R<MeIdentityLinkResponse> identity link を開始する
unlinkMyIdentity(identityId: string): R<MeIdentityUnlinkResponse> identity を unlink する
listMySessions(): R<MeSessionsListResponse> device session を列挙する
revokeMySession(sessionId: string): R<MeSessionRevokeResponse> 指定 session を revoke する
revokeAllMySessions(): R<MeSessionsRevokeAllResponse> 現在の session を含む全 session を revoke する
startMyEmailChange(input: MeEmailChangeStartRequest): R<MeEmailChangeStartResponse> email change の OTP flow を開始する
verifyMyEmailChangeOld(input: MeEmailChangeVerifyOldRequest): R<MeEmailChangeVerifyOldResponse> 現在の email 側 OTP を検証する
verifyMyEmailChangeNew(input: MeEmailChangeVerifyNewRequest): R<MeEmailChangeVerifyNewResponse> 新しい email 側 OTP を検証する
deleteMyAccount(): R<MeDeleteResponse> 自分の account を削除する

/v1/me の body/response 型は @auth-platform/browser から import できる contract type である。email 自体の変更には updateMe ではなく startMyEmailChange と verify の二段階を使う。

PKCE、WebAuthn、wire helpers#

Export Signature / 説明
generatePkcePair(): Promise<PkcePair> 43 文字の verifier と S256 challenge を生成する
generateState(): string 256-bit entropy の base64url state を生成する
generateNonce(): string 256-bit entropy の base64url nonce を生成する
base64UrlEncodeBytes(bytes: Uint8Array): string padding なし base64url encode
base64UrlDecodeToBytes(encoded: string): Uint8Array base64url を bytes に decode。不正 alphabet は Error
bufferToBase64Url(buffer: ArrayBuffer): string ArrayBuffer を base64url に encode
navigatorWebAuthnPort: WebAuthnBrowserPort navigator.credentials に接続する既定 port
WebAuthnBrowserPort isAvailable(): boolean; create(options: PasskeyRegistrationOptionsResponse): Promise<Outcome<PasskeyRegistrationCredential, WebAuthnFailureError>>; get(options: PasskeyAuthenticationOptionsResponse): Promise<Outcome<PasskeyAuthenticationCredential, WebAuthnFailureError>>
buildUrl(baseUrl: string, path: string, query?: Record<string, string>): string optional query を含む URL を作る
requestJson<T>(fetchPort: FetchPort, request: JsonRequest, schema: WireSchema<T>): Promise<Outcome<T, BrowserAuthError>> JSON transport と schema validation を行う
`parseProblemDetailsWire(value: unknown): ProblemDetailsWire null`
`parseOAuthErrorWire(value: unknown): OAuthErrorWire null`
ok<T, E>(value: T): Outcome<T, E> / err<T, E>(error: E): Outcome<T, E> Outcome の public constructors

FetchPort = (url: string, init: RequestInit) => Promise<Response>、HttpMethod = "GET" | "POST" | "PATCH" | "DELETE" である。JsonRequest は { readonly method: HttpMethod; readonly url: string; readonly body?: unknown; readonly accessToken?: string } を持つ。WireSchema<T> は safeParse(input: unknown) で成功時 { success: true; data: T } または失敗時 { success: false; error: unknown } を返す。 PkcePair は { readonly verifier: string; readonly challenge: string; readonly method: "S256" } である。

Error normalization#

全 client operation の失敗は BrowserAuthError の discriminated union になる。

type 主な fields / 意味
network_error message, retryable: true。fetch/body stream の失敗
invalid_response message, optional status。JSON、HTTP error shape、または成功 response schema が契約に合わない
rate_limited retryable: true, optional retryAfterSeconds, requestId。HTTP 429
problem_details RFC 9457 の problemType, code, title, status, optional detail, requestId
oauth_error allowlist 済み OAuth code, optional description, token endpoint 由来なら status
state_mismatch 未発行、期限切れ、または消費済みの state
not_authenticated 有効な token/refresh 経路がない
webauthn_error `reason: "not_supported"

parseProblemDetailsWire(value) と parseOAuthErrorWire(value) は untrusted JSON body を wire type に絞り込み、不正なら null を返す。non-2xx body の JSON parse に失敗した場合は invalid_response になる。JSON として読めた non-2xx response は 429 → Problem Details → allowlist 済み OAuth error → invalid_response の順で正規化する。2xx response も public contract schema で検証される。API failure を retry する際は network_error と rate_limited の retryable / Retry-After を参照し、無条件に retry しない。

関連資料#