v1
PoC / 設計素案

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

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

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

登場人物

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

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

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

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

🔐

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

誰が誰を方法基盤の関与
1階:機械認証テナント(サーバー) → 基盤登録済み公開鍵による署名検証(鍵はオンボーディング時にプッシュ登録)基盤が鍵レジストリを管理し検証
2階:人の認証参加者 → 他社サービス他社任意(自社ログイン/ログインなし)一切関与しない
①テナント登録・鍵ペア生成→公開鍵をプッシュ登録 ②参加者が自社でログイン(任意) ③テナントが自分の秘密鍵でJWTを署名 ④フラグメント付きリダイレクトで受け渡し ⑤基盤が登録済み公開鍵で検証 ⑥ルーム入室
🌐

LiveKit / Twilio Video(自社secretでアクセストークンをmint、プラットフォーム側は署名検証のみ)と同型の標準パターン。基盤は、LiveKitが自分たちに提供しているのと同じ関係を他社テナントに提供する。ただし JWT に載せるのはロールラベルのみで、capability(canPublish 等)は載せない — 「ルームの中で何ができるか」は基盤側(RoomPolicy)が専有する(認可設計)。

§トークン種別

「テナント自己署名の参加 JWT → 基盤が検証 → LiveKit アクセストークン」の二段トークン構造。

資格情報主体寿命用途
テナント署名鍵(Ed25519 鍵ペア)テナント(サーバー)長命・kid ローテーション参加 JWT の署名。秘密鍵はテナントが保管し、公開鍵のみ基盤の鍵レジストリに登録
API key(テナント資格情報)テナント(サーバー)長命・ローテーション可能管理 API(Room 作成等のコントロールプレーン)を叩く資格。テナントごとに発行・失効可能
Room Access Token(参加 JWT)参加者(ブラウザ)短命(5〜15分の入室猶予+ルーム時間窓)入室資格。core の API への Bearer 提示
LiveKit アクセストークン参加者(ブラウザ)短命メディア接続。基盤が参加 JWT を検証し RoomPolicy から grants を導出して発行(二段目)

署名鍵・API key はサーバーに閉じブラウザには出さない。参加 JWT はブラウザに出るが短命でルーム単位にスコープされる。LiveKit の grants(canPublish 等)は基盤だけが書ける — テナントがどんな JWT を署名しても、メディア層の能力は基盤の導出結果に限定される。

§JWT クレーム設計

テナントが自分の鍵で署名するクレーム構成。iss はテナント自身(tenants.uuid)で、鍵解決は (iss, kid) の複合キーで行う。

// Header
{ "alg": "EdDSA", "typ": "JWT", "kid": "t0-2026-07" }

// Payload
{
  "iss": "0190a1a0-...",      // 発行者 = テナント自身(tenants.uuid・表示名ではない)
  "aud": "interview-app",     // 検証側 = 基盤
  "tenant": "t_0",           // テナント識別子
  "room": "room_12345",      // Room の MT 中立 ID(Room 作成時に取得済み)
  "participant": "p_abc123", // 参加者の安定 ID
  "role": "panelist",       // 基盤定義語彙のラベルのみ(capability は載せない)
  "name": "山田太郎",       // サーバー確定の表示名
  "exp": 1730000900,         // 短命(検証側が上限も強制)
  "nbf": 1730000000,
  "jti": "uuid-v4"           // 失効・再生防止
}
🪪

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

role はラベルであって能力ではない。語彙(panelist / moderator / observer / minedia_observer)は基盤定義の閉じた enum で、未知の値は検証時に 401。ラベルが「何をできるか」への変換は基盤の RoomPolicy が専有する(認可設計)。

nbf / exp の導出はテナントの裁量(tenant 0 はルームの時間窓から導出)。検証側は nbf ≤ now ≤ exp の判定と上限強制のみを行う。

§入室フロー(To-Be)

鍵登録 → 発行 → 検証 → 入室の 4 フェーズ。鍵登録はオンボーディング時に 1 回、検証は core への毎リクエストで実行される。入室導線(テナント内認証と role の確定方法)はアクターごとに異なるため、タブで切り替える。パネリストタブが全フェーズを描いた詳細版で、モデレーター・見学者・運用オペレーター・即席調査の各タブは、この図との差分(入室導線と role の確定・grants の違い)だけを描き、共通部分(鍵登録・検証 STEP)は簡略表示している。即席調査は User ドメインを持たない identity 不要の形式で、role を grant トークンから確定する。図中の「基盤」は、テナントからの redirect 先となるインタビュールーム(ルーム SPA)と、検証フェーズを実施する Hono(基盤コア API)の 2 レーンに分けて描く。

パネリスト:リマインドメールの入室 URL(/users/interviews/:id/room)から。テナントが Devise・fixed・本人一致・時間窓を検証して自己署名し、フラグメント付きリダイレクトで受け渡す。検証成功後は入室前設定(音声・画面テスト)→「入室する」で LiveKit トークンを発行する。この図が全アクター共通の詳細版で、他タブはこの図との差分だけを描く。

クリックで拡大 🔍
パネリストの入室フロー。テナント自己署名方式の participant JWT ライフサイクルシーケンス図。基盤はインタビュールーム(ルーム SPA)と Hono(基盤コア API)の 2 レーンに分けて描く。事前準備フェーズ:テナントが Ed25519 の鍵ペアを生成し公開鍵 JWK だけを基盤へプッシュ登録(オンボーディング時に1回)。発行フェーズ:テナントサーバーが入室経路を検証してから自分の秘密鍵で JWT を署名し、connect_url へのフラグメント付きリダイレクトで受け渡す(基盤への往復なし)。redirect 先はインタビュールームで、起動した SPA が JWT を回収して sessionStorage に退避し URL から除去する。検証フェーズ:SPA が Bearer JWT で Hono の API を呼び、Hono が鍵解決(iss, kid)→署名検証→クレーム検証→テナント越境チェック(STEP2.5)→失効チェック(Participant.status・Room.status)を実施して 200 を返す。入室フェーズ:入室前画面から「入室する」で LiveKit トークンを Hono に要求し、RoomPolicy から grants を導出してメディア接続、実査中画面へ遷移する。検証失敗時は理由別のエラー画面を表示する。

🖼️ 画面遷移のイメージ:Pencil 共有リンク(入室フローの画面デザイン)

§署名方式

🔑 非対称署名(EdDSA / Ed25519)

署名鍵(秘密鍵)はテナントが保持し、基盤(検証側)には公開鍵だけを登録する。基盤は署名鍵を持たない検証専用ゲートで、テナント間の鍵は互いに分離される(漏洩の影響を隔離)。

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

鍵解決は常に (iss, kid) の複合キー。/.well-known/jwks.json は基盤自身(platform issuer)の鍵のみを公開し、テナント公開鍵はプッシュ登録で鍵レジストリに保存される(基盤がテナントの URL へ取りに行くプル型ではない)。

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

公開鍵の保管 — 鍵レジストリ(jwks_keys テーブル)

テナント公開鍵はプッシュ登録で DB(jwks_keys テーブル)に保存する。テナント自己署名では鍵の登録・ローテーションが実行時に発生するため、静的な設定ファイルでは成立しない。検証((iss, kid) 解決)と JWKS エンドポイントの源泉はこのテーブルに一本化する。鍵レジストリは本方式の信頼のルートそのもの(不正な公開鍵の挿入=任意発行者のトークン偽造)であり、登録経路は管理画面(/internal/admin)のみに絞り、①登録チャネルの強い認証 ②鍵登録・ローテーション・失効の監査ログ ③可能なら帯域外確認、を必須とする。

// jwks_keys — 公開鍵のみを保存。unique [issuer, kid](kid 単独解決は禁止)
{
  "issuer": "0190a1a0-...",      // tenants.uuid(基盤自身の鍵は "interview-platform")
  "kid": "t0-2026-07",
  "alg": "EdDSA",
  "use": "sig",
  "public_jwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." },
  "tenant_id": 0,               // 所有スコープ(基盤自身の鍵は null)
  "active": true
}
🔐

レジストリに置いてよいのは公開鍵のみ。JWK に d(秘密鍵パラメータ)を含まないことが安全条件で、登録口は受領時に d の混入を拒否する。テナントの秘密鍵(PEM / d 付き JWK)はテナント側のシークレット管理(tenant 0 は Rails credentials / AWS Secrets Manager)にのみ保管する。誤って秘密鍵を提出・登録した場合は即ローテーションで対応する。

レジストリ改竄への防御は、書き込み資格情報の分離・操作監査・全変更アラート・失効 runbook の4層(エンティティ設計 §3.18 参照)。テナントのセルフサービス登録口(外販時)はこの4層の発効を前提に開放する。PEM ↔ JWK の変換手順も同節に記載。

鍵ペアの作成 — 管理画面でのブラウザ内生成

テナント署名鍵の鍵ペアは管理画面(/internal/admin)のブラウザ内で WebCrypto により生成する。鍵材料は操作者のブラウザにのみ存在し、基盤のサーバー・ログ・DB を一切通らない(「基盤は検証専用ゲート」の原則を作成フローでも維持する)。openssl / HSM で自前生成するテナント向けには、公開鍵(JWK / SPKI PEM)の貼り付け登録を併設する。

①テナント選択 ②ブラウザ内で Ed25519 鍵ペア生成(WebCrypto) ③秘密鍵を PKCS#8 PEM でダウンロード(この場で1回だけ) ④公開鍵 JWK のみを登録 ⑤基盤が kid を確定
🔑

kid は RFC 7638 JWK サムプリントで確定する。鍵内容から一意に導出されるため、登録ミス・鍵の取り違えを構造的に防ぐ。クライアント側の表示はプレビューで、登録時にサーバーが再計算した値が正。

ダウンロードした秘密鍵の保管はテナント側の責務(tenant 0 は Rails credentials / AWS Secrets Manager。投入後はローカルの PEM ファイルを削除する)。ダウンロード → 登録の順序を UI が強制するため、途中離脱しても未登録の公開鍵行や迷子の秘密鍵は生じない。

ローテーション — 複数鍵並行有効・一方向遷移

ローテーションは複数鍵の並行有効で無停止に行う。status は active → retiring → revoked の一方向にのみ遷移し(retiring を飛ばした直接 revoke も可)、revoked は終端で復帰できない(再有効化が必要な場合は新規登録として扱う)。

①新鍵を生成・登録(active・旧鍵と並行有効) ②テナントが新 kid での署名へ切替 ③旧鍵を retiring(検証は継続) ④切替確認後に revoked
status検証意味
active現用鍵。新規署名にも使う
retiring移行中の旧鍵。検証は継続するが、新規署名には使わない意思表示
revoked401失効(終端)。失効理由の記録が必須

失効の実効遅延は検証側の鍵解決キャッシュ TTL(60 秒)が上限で、revoke 後最大 60 秒で全リクエストに反映される。テナント単位の全停止は、当該テナントの全鍵 revoke で行う。

監査と完全性監視 — 記録・即時通知・照合の3層

鍵レジストリは本方式の信頼のルートそのもの(不正な行挿入=任意発行者のトークン偽造)であるため、通常の操作ログより強い監査を課す。

内容
操作監査(記録)登録(registered)・retire(retired)・失効(revoked・理由必須)を専用の監査イベントテーブルに記録。状態変更とイベント INSERT は同一トランザクションで行い、アクター(管理画面の認証メール)を帰属させる。鍵行の物理削除は禁止(失効は status で表現し、行は消さない)
全変更の即時通知正規・不正を問わずすべての状態変更を即時に通知する(鍵操作は低頻度のため成立)。正規資格情報の悪用——イベントも正しく残るため照合では原理的に検知できない攻撃——は「身に覚えのない操作通知」として運用者が検知する。外販時は通知の宛先をテナント本人への操作通知に切り替える(自社運用期の「身に覚え照合」は他社の操作には成立しないため)
照合(改竄検知)1時間毎の Cron が監査イベントを「正」として全行を突合する。① registered イベントを持たない行=不正挿入 ② 公開鍵サムプリントが registered 記録と不一致=改ざん ③ revoked イベントがあるのに有効なまま=不正再有効化、の 3 ルールで検知し即通知する
⏱️

残存リスク(明示):DB 直接書き込みによる改竄の検知は照合周期(最大 1 時間)の遅延を持つ。監査イベントごと偽造できる攻撃者への耐性は、検証ランタイムの DB ロールを SELECT のみに分離するインフラ側の層で担保する。

§API key との関係

参加 JWT の発行はテナントの署名鍵で完結するため、API key の役割は管理 API(コントロールプレーン)専用になる。

🏢 テナントに1:N・管理 API 用

API key はテナントに紐づく資格情報で、Room 作成・録画取得などの管理 API を叩く資格。ローテーション・失効が可能で、権限範囲(scope)は任意。

✅ 発行権は検証時に強制(STEP2.5)

テナントは自分の鍵で任意のクレームを署名できるが、検証側が iss(発行者)と room の所有関係を毎回突き合わせるため、自テナントのルームにしか入室 JWT を通せない。

🏠 minedia-www も1テナント

tenant 0 として署名鍵と API key を持ち、自テナントの参加者の JWT を自己署名する。Devise は「入室の鍵」から tenant 0 内部の本人確認手段に降格する。