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 しない。