ガイド 第8

ログインレス session-handoff

mint/consume・allowlist・Cognito

@event/fours-sdk

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. mint4s.link(Cognito 済)POST /auth/session-handoff/mintSDK 対象外
2. redirect4s.link → event site?handoff=<code> を付けて遷移
3. consumeevent 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/clientconsumeHandoff は認証ヘッダを付けず 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 に畳む:

HTTPConsumeResult.error意味
200—(ok: trueCognitoTokens 取得
403forbiddenイベント origin が allowlist 外
404expiredcode 失効 / 不明
410already-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 とは語彙が別なので混同しない。

HTTPFoursErrorCode
400handoff_missing
403handoff_origin_not_allowed
404handoff_expired
410handoff_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())で。Use4SSessionOptionsrefreshEndpoint(既定 /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();
  • createLoginProxyHandlerPOST /auth/loginserver→server で転送し CORS を回避、パスワードは保存せず転送のみ。@ を含めば email、無ければ slug として送る。404 と 401 を 401 に統一してユーザー名列挙オラクルを塞ぐ。
  • createCognitoRefreshHandlerregion / clientId を正規表現で厳格検証し、宛先を常に cognito-idp.<region>.amazonaws.com に固定(SSRF 防止)。Cognito は新しい RefreshToken を返さないため旧 token を保持し続ける。

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 を使う