verify4SUser — なりすまし防止の要
client が送ってくる idToken を、サーバが 4S GET /users/me に Bearer で投げる。200 なら本人であり、返ってきた id(sub) / slug / name が「正典」 — client 申告の name / avatar は一切信用しない。JWKS の自前検証は持たず、検証は 4S に委ねる(4S が唯一の検証者)。
const user = await verify4SUser(config, idToken);
// => { id, slug, name, email?, avatar? } | null
実装上の防御(3実装の分岐をこの方式に統一):
- 入力が空 / 4096 文字超 /
.を含まない(JWT 形でない)→ 4S を叩かず即 null {data}ラップと素 JSON の両対応- 成功時のみ 60 秒キャッシュ。失効・障害の null をキャッシュすると正規ユーザーを 60 秒弾いてしまうため、null は決してキャッシュしない
- キャッシュは上限 1000 件で全クリア(メモリ暴走防止)
jose httpOnly cookie セッション
推奨デフォルトの認証モデルは spikes 由来の jose HS256 httpOnly cookie である。createSessionCodec は sign / verify の純粋なコーデックだけを提供し、cookie の読み書き(next/headers)はアプリ側の責務に残す。
import { createSessionCodec } from "@event/fours-sdk";
const codec = createSessionCodec({ cookieName: "myevent_4s_session" });
// handoff consume → verify4SUser 成功後:
const token = await codec.sign({ fourSUserId: user.id, slug: user.slug });
cookieStore.set(codec.cookieName, token, codec.cookieAttributes);
// 以降のリクエスト:
const session = await codec.verify(cookieStore.get(codec.cookieName)?.value);
// => { fourSUserId, slug } | null
設計上の不変条件:
- ペイロードは最小の
{ fourSUserId, slug }のみ。氏名 / avatar / 生 idToken / PII はセッションに載せない - HS256 / 既定 7 日。署名鍵は
SESSION_SECRET - 本番で
SESSION_SECRET未設定は throw(fail-closed)。dev のみ警告つき固定鍵にフォールバック cookieAttributesはhttpOnly/secure(本番)/sameSite: "lax"/path: "/"を返し、アプリはそのまま使う
isResourceOwner — 所有権ガード
Server Action 冒頭で必ず通す純関数。3条件のいずれかで所有と判定する:
resource.founderSlug === session.slugresource.meta.ownerFourSUserId === session.fourSUserId(sub は不変。最も堅牢)resource.meta.editorSlugsにsession.slugが含まれる
slug は 4S 側でユーザーが変更できる可変キーなので、新規リソースの所有権は原則 ownerFourSUserId(sub)で記録する。
middleware は「境界」であって「認可」ではない
CVE-2025-29927(x-middleware-subrequest ヘッダで middleware をスキップできた Next.js 脆弱性)を踏まえ、統治原則を固定する:
- middleware(
createAuthGateMiddleware)は未ログインを/loginへ倒す境界のみを担う - 認可の再検査は各 server action / route handler の冒頭で必ず行う。共有パスワード realm なら
realm.requireFromValue(cookieValue)(第37章)、4S セッションならcodec.verify()+isResourceOwner()
// server action の標準形
"use server";
export async function updateLp(slug: string, patch: LpPatch) {
const session = await codec.verify((await cookies()).get(codec.cookieName)?.value);
const lp = await loadLp(slug);
if (!isResourceOwner(session, lp)) throw new Error("forbidden");
// ...
}
なお edge で動く middleware から @event/core の barrel を import すると contentStore の node:fs / node:crypto が edge bundle に混入する。middleware 関連は @event/core/auth subpath から import する(第46章の落とし穴カタログ参照)。
legacy adapter — localStorage tokenStore
xdeal / ivs の現行方式(Cognito トークンを 4s-cognito-tokens-v1 キーで localStorage 保持 + client 側 refresh)は @event/fours-sdk/client に legacy adapter として温存している。loadTokens / saveTokens / isIdTokenExpired / decodeJwt(表示用の無検証 decode。本人判定は常にサーバの /users/me)を提供する。新規イベントは jose cookie を既定とし、client 側トークン保持が必要な場合のみ opt-in する。