🎢

OAuth 2.0 / OIDCがよくわかっていなかったので、遊園地のたとえで理解してみた

に公開

— 年パス・代理店・預かり札(session ID)を軸に —

本記事は、OAuth 2.0(認可)と OIDC(認証)を物理的な遊園地のたとえで整理し、実装時に迷いがちな「誰が何を持つのか」「何のための仕組みか」を、年パス・代理店・預かり札という3つの柱で明確にします。読了後には、上記のシーケンス図が無理なく読み解けるはずです。


1. 世界観と対応表

この遊園地は、年パスを持っていれば本来は自分で直接アトラクションに入ることもできます
ただし、代理店を通すことで次のようなメリットがあります。

  • 複数のアトラクション利用手続きを一括で行える
  • 利用履歴や予約状況を代理店が管理してくれる
  • 特定の割引や優先入場など、代理店経由限定の特典がある

今回はこうした利便性や特典を活かすため、あえて代理店経由でアトラクションを利用するケースを想定します。
OAuth/OIDC で言えば「ユーザーが自分の権利(年パス)の一部を、特定のクライアントアプリ(代理店)に委任し、そのアプリ経由でサービスを利用する」という構図です。

登場人物(役割の対応)

実世界(遊園地) OAuth/OIDC の役割 要点
ゲスト(来園者) ユーザー / リソース所有者 すでに年パス=自分の権利を持つ本人
代理店(園と提携した窓口) クライアント(Next.jsなどのBackend for Frontendサーバー) ゲストの委任で手続きを代行。トークンは金庫に保管し、ゲストには預かり札だけ渡す
チケット発行所 認可サーバー / Identity Provider ログイン確認と同意を取り、デイリーパス引換券→デイリーパス(AT)・本人確認書類(IDT)・再発行クーポン(RT)を発行
アトラクション入口 リソースサーバー / API 提示されたデイリーパス(AT)とスタンプ(scope)で入場可否を判定

証票(チケット類の対応)

証票 OAuth/OIDC の実体 説明
年パス ユーザーの元々の権利 何にアクセス可能かの“大枠”。今回、その権利の一部だけを代理店に委任する前提
デイリーパス引換券 認可コード(code) 短命・一回限り。後段でデイリーパスに交換する権利
デイリーパス アクセストークン(AT) 期限内は何度でも使用可。scope(スタンプ)の範囲で利用可能
本人確認書類 IDトークン(IDT) 「誰か」をクライアントが理解するための署名付き情報。利用開始時のみ有効で、通常は再利用しない
再発行クーポン リフレッシュトークン(RT) デイリーパス期限切れ時に新しいデイリーパス をもらう権利
預かり札 session ID(クッキー) 代理店が AT/IDT/RT を金庫(DB)保管する代わりに、ゲストは預かり札だけ持つ

方針:AT/IDT/RT は常に代理店(BFF)側に保管し、ゲスト(ブラウザ)にはsession ID(預かり札)のみ。以降の利用時は、札を提示すると代理店が裏で AT を提示してくれる設計にします。

OAuth 2.0 / OIDC用語整理(辞書・定義集)

  • AT(Access Token / アクセストークン)
    API(リソースサーバー)にアクセスするための通行証。期限内であれば何度でも利用可能。scopeによって利用範囲が制限される。

  • IDT(ID Token / IDトークン)
    OIDC特有のトークン。ユーザーが「誰であるか」をクライアントに証明する。署名付きのJWT形式で、sub(ユーザーID)、iss(発行者)、aud(対象クライアント)などの情報が含まれる。通常は初回の本人確認時だけ利用される。

  • RT(Refresh Token / リフレッシュトークン)
    アクセストークンの期限切れ時に新しいATを発行するためのトークン。長期利用可能だが、漏えいリスクが高いため公開領域(ブラウザやモバイル)に保存してはいけない

  • BFF(Backend For Frontend)
    フロントエンド専用に用意した薄いバックエンド。トークンや認証状態を安全に管理し、フロントはsession IDだけを持つ。XSSなどによるトークン窃取リスクを低減する。

  • IdP(Identity Provider / アイデンティティプロバイダ)
    ユーザー認証とトークン発行を行う外部サービス(Google, Auth0, AWS Cognito など)。OAuth 2.0 では認可サーバーとして機能する。

  • scope(スコープ)
    アクセストークンで許可される操作範囲やリソースの種類。例: read:useremailphotos:write など。

  • session ID(セッションID)
    サーバー上のセッションデータを識別するための一意なID。HttpOnlyクッキーとしてブラウザに渡される。トークン本体はサーバー側にのみ保持。


2. まずは「素の流れ」だけ(セキュリティの仕組みは後段)

ゴール:年パス → 代理店 → チケット発行所 → 代理店がトークン保管 → 預かり札 → 入場の因果を掴む

  1. 代理店で委任の開始(scope 選択)
    ゲストは代理店の窓口で「このアトラクション群だけ使えるようにしてください」と依頼します。
    scopeゲストが選び、同意の対象が定まります(年パス権利の一部委任)。

  2. チケット発行所でログインと同意(認可)
    チケット発行所でゲストが本人確認(ログイン)を行い、代理店に対するscope 同意を行います。

  3. デイリーパス引換券の発行(認可コード)
    チケット発行所はデイリーパス引換券を発行し、代理店の固定窓口に届けます(あらかじめ登録している戻り先)。

  4. デイリーパス引換券→デイリーパス・本人確認書類・再発行クーポンへ交換
    代理店はデイリーパス引換券を使い、デイリーパス/本人確認書類/再発行クーポン を受領。
    デイリーパス/本人確認書類/再発行クーポンは金庫(DB)に保管し、ゲストには預かり札(session ID)だけ渡します。

  5. アトラクション利用(デイリーパス提示は代理店が代行)
    ゲストがアトラクション入口で預り札を提示すると、代理店が金庫から デイリーパス を取り出してから、飛んできて入口係(API)にデイリーパスを提示
    入口係は scope を確認して可否を判定します。

  6. デイリーパス期限切れ時
    代理店が再発行クーポン新しいデイリーパスを取得。ゲストは預り札のまま継続利用できます(再ログイン不要)。

  7. 代理店利用の終了(ログアウト)
    代理店は預り札を回収して金庫の中身を破棄(セッション削除・RT 失効)します。

この「素の流れ」だけで、誰が何を持つかが明確になります:
ユーザー=預り札のみ、代理店=デイリーパス(AT)/本人確認書類(IDT)/再発行クーポン(RT)を金庫保管、入口=デイリーパス(AT)の検証だけ


3. 実際の技術

ここまでのたとえ話を踏まえて、OAuth 2.0 / OIDC の実際の技術を紹介します。

  • デイリーパス=AT(アクセストークン)
    実際のリクエストでは Authorization: Bearer <AT> として API に提示。API は署名や有効期限、scopeを検証する。

  • 本人確認書類=IDT(IDトークン)
    初回の本人確認・ユーザー登録時にのみ利用。アプリケーション内で永続的に使い回すことはしない。

  • 再発行クーポン=RT(リフレッシュトークン)
    長期的な利用継続のために重要だが、漏えいリスクが高い。必ずサーバー側に保管し、ブラウザやモバイル側には置かない。

  • scope=スタンプ
    権限を必要最小限に絞る。委任範囲外の操作はAPI側で拒否される。

  • 預かり札=session ID
    フロントエンドが保持する唯一の情報。HttpOnly + Secure + SameSite 属性を付けたクッキーで配布し、XSSから守る。


4. 「こうなったら?」への防御の仕組み

素の流れに対し、現実世界では以下の不正が想定されます。対応する仕組みを、遊園地のたとえで導入します。

A. 他人のデイリーパス引換券が混入したら?

  • 脅威:攻撃者が自分のアカウントで発行した デイリーパス引換券(認可コード) を用意し、被害者にそのリンクを踏ませる。
    被害者のブラウザはログイン済みなので、クライアント(代理店)はそれを正規のものと勘違いし、
    攻撃者のアカウント用のデイリーパス(AT)・本人確認書類(IDT)・再発行クーポン(RT) を発行してしまう。
    結果、被害者のセッション(預かり札)には攻撃者のトークンが結び付けられ、
    以降の操作はすべて 攻撃者のアカウントとして処理される
    → 被害者は「自分のアカウントで操作している」と思い込むが、実際には攻撃者のアカウント上で動作しているため、
    攻撃者は被害者の行動や入力を 盗み見たり、勝手に利用 できてしまう。

  • 対策state(受付番号)
    代理店は出発時に 受付番号 をチケット発行所に伝えて控え、
    戻ってきた デイリーパス引換券 に付いている番号と 一致確認 を行う。
    → 一致しなければ「別の人の引換券が紛れ込んだ」と判断して破棄。
    → つまり 「この引換券は最初に自分が依頼したリクエストに対応しているか?」を保証する仕組み
    → 仮に攻撃者が自分用に発行した引換券と受付番号を被害者に踏ませても、被害者側のセッションにはその受付番号が存在しないため、不一致として弾かれ安全

B. 引換券を途中で盗られたら?

  • 脅威認可コードの横取り(盗聴・リダイレクト経路での奪取)。
  • 対策PKCE(暗証番号付き引換券)
    代理店は出発時に、暗証番号を「ハッシュ化(変形)」したcode_challengeをチケット発行所に預けておきます。
    そして引換券を交換するときに「元の暗証番号(code_verifier)」を提示します。
    チケット発行所は両者を突き合わせることで「これは確かに最初に暗証番号を預けた代理店自身だ」と確認できる仕組みです。
    暗証番号がない第三者は交換できない

C. 本人確認書類を使い回されたら?

  • 脅威:攻撃者が昔に発行された本人確認書類(IDトークン)をコピーして持ち込み、
    「私はこの人です」と別の受付で提示してしまう。
    → クライアント(代理店)がそれを信じてしまうと、すでに期限切れや別リクエスト用の本人確認書類であっても、正規のユーザーとして処理されてしまう危険がある(リプレイ攻撃)。

  • 対策申請番号(nonce)
    認可リクエストごとに必ず 一度きりの申請番号 を振り、チケット発行所(IdP)はそれを本人確認書類に印字して返す。
    クライアント(代理店)は戻ってきた本人確認書類に含まれる番号と、自分が出発時に控えた番号を照合する。
    → 番号が違えば「これは今回発行した本人確認書類ではない」と判断して破棄。
    → つまり 「古い本人確認書類や他の依頼に紐づく書類は通さない」仕組み になる。

D. 引換券の届け先をすり替えられたら?

  • 脅威:引換券(認可コード)を代理店の窓口ではなく、偽の窓口に届けられてしまう。
  • 対策届け先窓口(Callback URL)の事前登録
    → チケット発行所は「引換券を渡す窓口は登録済みの正規の代理店カウンターだけ」と決めている。
    知らないカウンター(オープンリダイレクト先)には一切渡さない

E. 落とした券をいつまでも使われたら?

  • 脅威:落とした引換券やデイリーパス(トークン)を、誰かが期限なく何度でも使えてしまう

  • 対策期限の短縮と一回限りの制限

    • 引換券(認可コード)は 極短命 & 一度限り
    • デイリーパス(AT)も短命。
    • 再発行クーポン(RT)は使うたびに新しいものに更新(ローテーション)
      → 「落とした券をいつまでも使える」という状況をなくす。

5. BFF(代理店が守る)を実装視点で

  • ブラウザに預り札(session ID)だけ:AT/RT/IDT はHttpOnly Cookie からも参照不可(BFF 内部だけ)。
  • 預り札→金庫検索→AT 代理提示:リクエストごとに BFF が Authorization: Bearer AT を付与して API 呼び出し。
  • 期限管理:AT の exp を金庫に記録し、閾値で自動更新(RT)
  • 失効:ログアウト時はセッション削除 + RT 失効
  • 追加CSRF トークン(Cookie と別チャネル)、CORS / SameSite=LaxKMS で金庫を暗号化

これにより、XSS で AT/RT を抜かれる可能性を本質的に下げる(ブラウザに存在しない)。攻撃面は預り札の悪用に限定され、対処しやすくなります。


6. もう一度全体フローを見てみる

図は遊園地のたとえとを技術フローへ対応させたものです。
代理店=BFF がAT/RT/IDT を保管し、預り札(session ID)だけをブラウザに渡す前提です。


7. まとめ

  • 所有と委任の分離:ユーザーは年パスの一部を scope で委任
  • 保持の最小化AT/IDT/RT は BFF 保管、ユーザーは札だけ
  • 入場の単純化:API は AT の検証と scope 判定だけ。
  • 防御の段階化state(混入防止) / PKCE(横取り防止) / nonce(使い回し防止) / 短命化
  • 運用の平準化RT 更新・ローテーション失効金庫暗号化Cookie 属性と CSRF

この枠組みで考えると、OAuth 2.0 / OIDC の全体像を筋道立てて理解できるようになります。
まずは「ユーザーは預かり札だけを持ち、トークンはすべて BFF が管理する」という大枠を押さえ、
そこに scope・state・PKCE・nonce といった仕組みがそれぞれ何を守っているのかを少しずつ積み上げていくと、全体の設計意図が一貫して見えてくるはずです。

Discussion