4S にログイン済みのユーザーがイベントサイトへ来たとき、パスワードもフォームも見せずに本人を引き継ぐのが session-handoff である。4s.link 側が一度きりの code を発行(mint)し、イベントサイトはそれを consume してトークンを得る。
役割分担 — mint は 4S 側、SDK は consume だけ
mint は Cognito 認証済みの 4s.link ドメインからしか呼べない(POST /auth/session-handoff/mint は 4S backend の責務)。イベントサイトからは物理的に mint できないので、SDK に mint 関数は無い。SDK が実装するのは consume 以降だけである。
| # | 実行場所 | 手段(実 export / エンドポイント) |
|---|---|---|
| 1. mint | 4s.link(Cognito 済) | POST /auth/session-handoff/mint — SDK 対象外 |
| 2. redirect | 4s.link → event site | ?handoff=<code> を付けて遷移 |
| 3. consume | event site(ブラウザ) | consumeHandoff(code) → POST /auth/session-handoff/consume |
| 4. 本人確定 | event site(サーバ) | verify4SUser(config, idToken)(第9章) |
| 5. セッション化 | event site(サーバ) | createSessionCodec().sign(第9章) |
クエリ名 handoff は仕様で固定(HANDOFF_QUERY として export)。
consumeHandoff — code をトークンに換える
@event/fours-sdk/client の consumeHandoff は認証ヘッダを付けず POST する。4S 側は Origin / Referer だけで発行元を検査するため、余計な Bearer は不要。
"use client";
import {
consumeHandoff, readHandoffCode, stripHandoffFromUrl, saveTokens,
} from "@event/fours-sdk/client";
const code = readHandoffCode(); // URL の ?handoff=<code>
if (code) {
const r = await consumeHandoff(code); // POST /auth/session-handoff/consume
stripHandoffFromUrl(); // 成否に関わらず ?handoff を URL から除去
if (r.ok && r.tokens) saveTokens(r.tokens);
}
consumeHandoff は HTTP ステータスを ConsumeResult.error に畳む:
| HTTP | ConsumeResult.error | 意味 |
|---|---|---|
| 200 | —(ok: true) | CognitoTokens 取得 |
| 403 | forbidden | イベント origin が allowlist 外 |
| 404 | expired | code 失効 / 不明 |
| 410 | already-consumed | 使用済み(one-time) |
| fetch 例外 | network | 到達不可 |
| その他 | unknown | 想定外 |
readHandoffCode は URL を読むだけ(書き換えない)、stripHandoffFromUrl は ?handoff だけを history.replaceState で落とす(他のクエリ / hash は残す)。code が URL 履歴に残り続けるのを防ぐため、consume 後は必ず呼ぶ。
allowlist — 制御は 4S backend 側
403(forbidden)は 4S backend の SESSION_HANDOFF_ALLOWED_ORIGINS にイベントサイトの origin が登録されていないときに返る。この allowlist は 4S 側の env であり、SDK は検査に関与しない(consume のリクエストが 4S に弾かれるだけ)。新しいイベントドメイン(例 xdeal.4s.link)を handoff 対象にするには、4S 運営に origin 登録を依頼する必要がある。SDK 側の設定だけでは有効化できない点に注意。
サーバ側のエラーコード — handoffErrorFromStatus
サーバ経由で consume を扱う場合(route handler 等)は handoffErrorFromStatus が同じ 4 状態を型付き FoursErrorCode に写す。client の ConsumeResult.error とは語彙が別なので混同しない。
| HTTP | FoursErrorCode |
|---|---|
| 400 | handoff_missing |
| 403 | handoff_origin_not_allowed |
| 404 | handoff_expired |
| 410 | handoff_consumed |
これらは FoursError.code に載り、UI がステータス別に文言を分岐できる(「リンクの有効期限が切れました」など)。
use4SSession — 端末セッションを丸ごと面倒みる hook
手で consume を書かず、use4SSession フックに任せるのが標準。mount 時に上記フローを自動実行する。
"use client";
import { use4SSession } from "@event/fours-sdk/client";
function Header() {
const { status, user, signOut, loginUrl, loginWithPassword } = use4SSession();
// status: "loading" | "anonymous" | "authed"
if (status === "authed") return <span>{user?.name}</span>;
return <a href={loginUrl()}>4Sでログイン</a>;
}
内部の順序は (1) ?handoff があれば consume → 保存 → URL 除去、(2) 保存済みトークンが有効なら /users/me で本人確定して authed、(3) 失効時は refresh、不可なら anonymous。ページ遷移は自動で起こさない — ログインは必ずユーザーのボタン操作(loginUrl())で。Use4SSessionOptions で refreshEndpoint(既定 /api/4s-session/refresh)と loginEndpoint(既定 /api/4s-session/login)を差し替えられる。StrictMode の二重実行は consumedRef でガード済み。
サーバ route — login proxy と Cognito refresh
hook が叩く 2 つの endpoint は factory を 1 行 mount するだけ:
// app/api/4s-session/login/route.ts
import { createFoursConfig, createLoginProxyHandler } from "@event/fours-sdk";
export const POST = createLoginProxyHandler(createFoursConfig());
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
// app/api/4s-session/refresh/route.ts
import { createCognitoRefreshHandler } from "@event/fours-sdk";
export const POST = createCognitoRefreshHandler();
createLoginProxyHandlerはPOST /auth/loginへ server→server で転送し CORS を回避、パスワードは保存せず転送のみ。@を含めば email、無ければ slug として送る。404 と 401 を 401 に統一してユーザー名列挙オラクルを塞ぐ。createCognitoRefreshHandlerはregion/clientIdを正規表現で厳格検証し、宛先を常にcognito-idp.<region>.amazonaws.comに固定(SSRF 防止)。Cognito は新しい RefreshToken を返さないため旧 token を保持し続ける。
consume の後 — cookie セッションへ
handoff はあくまで「トークンを渡す」段階まで。得た idToken を サーバで verify4SUser に通して本人を確定し、createSessionCodec で httpOnly cookie に畳むのが推奨の永続化(詳細は第9章)。consumeHandoff が保存する localStorage トークン(4s-cognito-tokens-v1)は現行 3 アプリと同じ legacy adapter で、loadTokens / saveTokens / clearTokens / getStoredIdToken / isIdTokenExpired / decodeJwt(表示用の無検証 decode)を提供する。どちらのセッションモデルでも consume 自体はこの client module を使う。