v1
PoC / 設計素案

インタビュールーム 認証設計

「誰であるか」をテナント経由の委譲信頼で確定し、短命の署名付き JWT に載せて配布する認証モデル。参加者は基盤に一度もログインしない。

対象: 参加者認証(テナント認証モデル) 署名: EdDSA(非対称・kid ローテーション) 基盤コア: Cloudflare Workers・Hono

登場人物

マルチテナント(OEM)前提で「基盤」「テナント」「エンドユーザー」を分けて捉える。

呼称役割
基盤(インタビュー基盤)発行サービス・検証(リゾルバ)を内包するプラットフォーム本体。
テナント基盤の利用者。tenant 0 = minedia-www(自社)、tenant N = 他社(OEM)。
エンドユーザーパネリスト / モデレーター / オブザーバー(ブラウザ)。

§認証モデル(委譲信頼)

認証は1種類ではなく2層。分けて捉えないと「他社の参加者をどう認証するか」が分からなくなる。

🔐

基盤は「他社テナントの参加者」を認証しない。認証するのは「テナント(他社サーバー)」だけ。参加者の身元はテナントが保証し、基盤はそれを署名付きトークンの形で受け取る(委譲信頼)。

誰が誰を方法基盤の関与
1階:機械認証他社サーバー → 基盤の発行APIAPI key基盤が発行・検証
2階:人の認証参加者 → 他社サービス他社任意(自社ログイン/ログインなし)一切関与しない
①テナント登録・API key発行 ②参加者が自社でログイン(任意) ③API keyで発行依頼 ④基盤がJWTを署名・発行 ⑤テナントがJWTを渡す ⑥JWTでルーム入室
🌐

Twilio Video / LiveKit(自社secretでトークンをmint)・Stripe(secret keyでAPIを叩く)・S3 presigned URLと同型の標準パターン。基盤は、LiveKitが自分たちに提供しているのと同じ関係を他社テナントに提供する。

§発行モデル(Model A / B)

「誰が JWT に署名するか」で2モデルある。

観点Model A:集中発行(基盤が署名)Model B:テナント自己署名
署名鍵基盤のみ保持テナントが自分の鍵を保持
iss(発行者)基盤各テナント
失効制御強い(発行を止めれば即失効)jti 失効インフラが必須
向くテナント数少数(当面 tenant 0 のみ)多数(OEM 増加期)
現状資産との近さ高い(gid:nil 発行が原型)新規実装

PoC は Model A(集中発行)で開始。現状資産に最も近く、失効制御を基盤が握れる。外販/OEM の規模が見えたら Model B(テナント自己署名)を選択肢として開く。検証(リゾルバ)が両モデルを同じ Context に正規化するため、後から差し替え可能。

前方互換契約 — Model A → B へ移行しても設計は破綻しない

「後から差し替え可能」は願望ではなく、以下の不変条件(前方互換契約)で担保する。Model A は「発行者レジストリの特殊例」として実装するため、B への移行は「テナント発行者の鍵をレジストリに登録する」というデータ追加であり、検証コード・API 契約の破壊的変更を伴わない。

不変条件なぜ B で壊れないか
検証側は Model A / B を区別する分岐を持たない基盤(iss=interview-platform)はレジストリに登録された「最初の発行者」にすぎない。B の追加=鍵行の登録で、検証コードの変更ゼロ
鍵解決は常に (iss, kid) の複合キーテナント間で kid が衝突しても解決が壊れない
iss の値域を確定(基盤=固定文字列/テナント=tenants.uuidテナント改名の影響を受けず、基盤を騙る iss は鍵解決の時点で 401
発行権限チェック(STEP2.5)をA の時点から常設A では恒真の assertion(追加コストは比較1回)。B のテナント越境(iss ≠ room 所有者)をコード追加なしで拒否。minedia_observer はテナント発行者の JWT に含まれたら 401
Participant は「入室成功時点までに存在」という不変条件A は発行 API の副作用で生成、B は初回検証成功時に upsert。生成契機が変わっても検証パイプラインは同一
room_connect_url は Room の属性(発行 API レスポンスは複製)B では発行 API を叩かないが、Room 取得 API から同じ値を得られる
exp の上限を検証側で強制B でテナントが exp を自由設定しても無期限トークンを作れない(jti ブラックリストの TTL も有界化)

§トークン種別

「他社サーバー → 基盤の発行API → 参加トークン → 他社フロント」の二段構え。

トークン主体寿命用途
API key(テナント資格情報)テナント(サーバー)長命・回転式発行API を叩く資格。テナントごとに発行・失効可能
Room Access Token(参加 JWT)参加者(ブラウザ)短命(5〜15分の入室猶予+ルーム時間窓)入室・ルーム接続

API key は人間/サーバーには露出するがブラウザには出さない。参加 JWT はブラウザに出るが短命でルーム単位にスコープされる。

§JWT クレーム設計

Model A(基盤が集中発行)を前提としたクレーム構成。

// Header
{ "alg": "EdDSA", "typ": "JWT", "kid": "interview-2026-05" }

// Payload
{
  "iss": "interview-platform",  // 発行者 = 基盤
  "aud": "interview-app",     // 検証側 = 基盤
  "tenant": "t_0",           // テナント識別子
  "room": "room_12345",      // Room の MT 中立 ID
  "participant": "p_abc123", // 参加者の安定 ID
  "role": "panelist",       // panelist/moderator/observer/minedia_observer
  "name": "山田太郎",       // サーバー確定の表示名
  "exp": 1730000900,         // 短命
  "nbf": 1730000000,
  "jti": "uuid-v4"           // 失効・再生防止
}
🪪

role / name はトークンに封入し、クライアント申告を信用しない。役割を指定するのはテナントのサーバーのみで、署名後は参加者側で変更できない。

nbf / exp はルームの時間窓から導出し、「時間窓=失効ポリシー」を明示クレーム化する。

§入室フロー(To-Be)

発行フェーズ(mint)と検証フェーズ(verify)の2部構成。タブで Model A / B のシーケンスを切り替えられる。

Model A:基盤が署名し、基盤自身が入室時に検証する。発行はテナント → 基盤の API 往復。

クリックで拡大 🔍
participant JWT のライフサイクル(発行 → 検証)シーケンス図。発行フェーズ:パネリストが入室URLをクリックし、minedia-www が基盤へ発行を依頼、基盤が秘密鍵でJWTを署名して返す。検証フェーズ:パネリストがJWTを提示し、基盤が公開鍵で署名検証(STEP1)、クレーム検証(STEP2)、jtiブラックリスト照会とParticipant.statusの検証(STEP3・kicked/bannedでないことを確認)を経て、全STEP成立でLiveKitアクセストークンを発行する。

§署名方式

🔑 非対称署名(EdDSA / Ed25519)

検証側には公開鍵だけを配ればよく、秘密鍵は基盤の発行サービスに閉じられる。OEM で検証ノードが増えても秘密鍵を撒かずに済む。

🔄 kid でキーローテーション

/.well-known/jwks.json を基盤が公開し、検証側がキャッシュ取得する。

ライブラリは jose を採用(jsonwebtoken は EdDSA 非対応のため)。

公開鍵の保管 — Model A 段階は JWK 設定ファイル(config-as-code)

Model A 段階では、検証用の公開鍵を DB(jwks_keys テーブル)ではなくリポジトリ内の設定ファイルに JWK 形式で直書きする。鍵の変更が必ず PR レビュー(承認ゲート)と git 履歴(監査ログ)を通り、DB への書き込み経路そのものが存在しないため、レジストリ改竄(不正な公開鍵の挿入=任意発行者のトークン偽造)を構造的に防げる。

// config/jwks.json — 公開鍵のみ。JWKS エンドポイントと検証(iss, kid 解決)の源泉
{
  "keys": [
    {
      "issuer": "interview-platform",
      "kid": "interview-2026-05",
      "alg": "EdDSA",
      "use": "sig",
      "public_jwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
    }
  ]
}
🔐

設定ファイルに置いてよいのは公開鍵のみ。公開鍵はそもそも JWKS エンドポイントで公開する前提のデータなので、リポジトリに置いても機密性の問題はない(git に置く目的は秘匿ではなく完全性)。JWK に d(秘密鍵パラメータ)を含まないことが安全条件で、秘密鍵(PEM / d 付き JWK)は AWS Secrets Manager にのみ保管する。誤ってコミットした場合は履歴削除ではなく即ローテーションで対応する。

ローテーション自動化や Model B 解凍で jwks_keys テーブル運用へ切り替える際は、書き込み資格情報の分離・操作監査・全変更アラート・失効 runbook の4層を発効させる(エンティティ設計 §3.18 参照)。PEM ↔ JWK の変換手順も同節に記載。

§失効・BAN

短命トークンが第一防御。強制退室・BAN・誤発行に備え多層で守る。

内容
短い exp数分〜ルーム終了までの短命トークンが第一防御。
jti ブラックリスト失効済み jti を TTL 付きで記録し、検証時に参照する。
Kick / BanParticipant.status(active/kicked/banned)を検証。世代カウンタは持たず、単一フィールドで判定する。
API key 失効テナント単位の全停止はハンドシェイク層で行い、新規発行を止める。
🛡️

ban 済み参加者は、既発行の jti が生きていても Participant.status の検証で再入室できない。ルーム全体を止めたい場合は Room.status(open/closed)で代替する。

§API key との関係

🏢 テナントに1:N

API key はテナントに紐づく資格情報。ローテーション・失効が可能で、権限範囲(scope)は任意。

✅ ルーム発行権の認可

基盤の発行API は API key からテナントを確定し、そのテナントがそのルームへの発行権を持つかを認可してから JWT を発行する。

🏠 minedia-www も1テナント

tenant 0 として自身の API key を持ち、自テナントのパネリストの参加 JWT を取得する。