OAuth は「認可」であって「認証」ではない

「OAuth でログイン」という言い方をよく聞きます。ですが OAuth 2.0(RFC 6749)は 認可(Authorization)のための仕組みであって、 認証(Authentication)の仕組みではありません。 認可の道具をログインに流用すると、なりすましの穴が空くことがあります。 ログインを正しくやりたいなら、OAuth 2.0 の上に立つ OpenID Connect(OIDC)を使います。

まずは下のデモで、認可コードフローが何を受け渡ししているか、 そして「アクセストークンでログイン」がどうやってなりすましを通してしまうかを操作して確かめてください。

① 認可コードフローを 1 ステップずつ追う

クライアント(サードパーティアプリ)が、ユーザーの連絡先を パスワードを受け取らずに読むまでの流れです。 各ステップが認証認可か、 クライアントが何を受け取ったのかに注目してください。

ユーザーリソースオーナー(ユーザー本人)
クライアントクライアント(サードパーティアプリ)
認可サーバー認可サーバー(例: Google の認可エンドポイント)
リソースサーバーリソースサーバー(API を持つサーバー)

「▶ 再生」または「次へ」でフローを開始してください。

ステップ 2 でユーザーがパスワードを入れる相手は認可サーバーであって、 クライアントではありません。クライアントが最後に手にするのは アクセストークン(連絡先 API を叩く許可証)であって、 「この人が誰か」の証明書ではない、という点が要点です。

② 「トークンでログイン」がなりすましを通す様子

ログインの判定方式を切り替えて、同じトークン置換攻撃の結末を見比べます。 攻撃者は、被害者 user-X 向けに発行された別アプリ(アプリA)用のトークンを手に入れ、 それをサービスB(アプリB)に突きつけます。

方式を選び、「正規ログイン」か「攻撃」を実行すると、結果がここに出ます。

素朴方式で「攻撃」を実行すると、aud(受け手)を確かめないため、 被害者向けのトークンをそのまま受け入れ、攻撃者が user-X になりすませます(赤)。 OIDC 方式は ID Token の aud が自分(アプリB)宛でないことを検出して拒否します(緑)。

認証と認可は別物 ―― パスポートとルームキー

まず言葉を分けます。認証(Authentication, AuthN)は「あなたは誰か」を確かめることです。 空港でパスポートを見せて本人だと示すのが認証です。一方 認可(Authorization, AuthZ)は「あなたは何をしてよいか」を与えることです。 ホテルのルームキーがこれにあたります。

ルームキーは、持っている人が誰であろうと、その部屋のドアを開けます。 フロントは鍵を渡すときに本人確認をしますが、鍵そのものは「この鍵を持つ者は入室してよい」という 許可を表すだけで、「持ち主が誰か」は語りません。落とした鍵を拾った人も部屋に入れてしまいます。

認証(AuthN)「あなたは誰か」を確かめる。例: パスポート提示で本人確認。
認可(AuthZ)「あなたは何をしてよいか」を与える。例: ルームキー。誰のものかは問わない。
アクセストークンまさにルームキー型(bearer token)。「持っている者が使える」許可証。「誰か」は語らない。

OAuth が発行するアクセストークンは、この「ルームキー型」です。仕様上は bearer token(持参人式トークン, RFC 6750)と呼ばれ、 「それを提示した者にアクセスを認める」性質を持ちます。だからトークン単体は「いま本人がそこにいる」ことを保証しません。

OAuth が本来解いている問題 ―― 権限の委譲

OAuth が生まれた動機は、ログインではなく権限の委譲です。 具体例で考えます。年賀状を作るアプリに、Google の連絡先を読み込ませたいとします。 ですが Google のパスワードをそのアプリに渡したくはありません。パスワードを渡せば、連絡先だけでなく メールも Drive も、何もかも触れてしまうからです。

OAuth はこの問題を解きます。ユーザーは Google(認可サーバー)に対して 「この年賀状アプリに、連絡先の読み取りだけを許可する」と同意します。 アプリが受け取るのは連絡先 API を叩くためのアクセストークンだけで、パスワードは渡りません。 これは「自分のリソースへのアクセス権を、パスワードを渡さずに第三者へ委譲する」という、 まさに認可の問題です。ログイン(認証)の問題ではありません。

認可コードフローを追う

上のデモAで再生したのが認可コードフロー(Authorization Code Flow)です。 登場人物は4者います。

  • リソースオーナー: ユーザー本人。リソース(連絡先など)の持ち主。
  • クライアント: サードパーティアプリ。リソースを使いたい側。
  • 認可サーバー: 同意を取り、トークンを発行する(例: Google の認可エンドポイント)。
  • リソースサーバー: API を持つサーバー。トークンを見てアクセスを許す。

流れはこうです。

  1. クライアントがユーザーを認可サーバーへリダイレクトする(欲しい権限を scope で指定)。
  2. ユーザーが認可サーバーで認証し、「このアプリに連絡先の読み取りを許可しますか?」に同意する。
  3. 認可サーバーが認可コード(一度きりの引換券)をリダイレクトでクライアントに返す。
  4. クライアントが裏側(サーバー間通信)で認可コードをアクセストークンに交換する(client_secret を提示)。
  5. クライアントがアクセストークンを添えてリソースサーバーの API を叩く。

ここで大事なのは、各ステップが「認証」か「認可」かの切り分けです。 ステップ2でユーザーが認証するのは認可サーバーとユーザーの間の話であって、 クライアントはその認証に立ち会っていません。クライアントが最終的に受け取るのは アクセス許可(トークン)であり、「この人が誰かの証明」ではありません。 この一点が、次の落とし穴につながります。

なぜ「OAuth でログイン」が書けてしまうのか

アクセストークンを手にすると、多くの IdP では /userinfo のようなエンドポイントを叩けます。 すると、そのトークンに紐づくメールアドレスやユーザー ID が返ってきます。だから、つい次のような実装を書けてしまいます。

// アンチパターン: 認可を認証に流用する
const token = await exchangeCodeForAccessToken(code)
const profile = await fetch('/userinfo', { token })   // ← メールや sub が取れる
loginAs(profile.sub)                                   // ← 取れたから本人とみなす

「トークンが取れた → /userinfo が引けた → この人はログイン成功」という筋です。 動いてしまうので、たちが悪いところです。ですがこれは 認可(アクセス許可)を認証(本人確認)に流用しているアンチパターンです。 トークンが取れたことは「このトークンで API を叩いてよい」ことしか意味せず、 「いま、この人が本人として認証された」ことを意味しません。

バッドケース: トークン置換でなりすます

素のアクセストークンには、「誰のために発行されたか(受け手 = audience)」を検証する手段が伴わないことがあります。 この隙を突くのがトークン置換(混乱した代理人 / confused deputy 問題)です。デモBで再生したのがこれです。

  1. 攻撃者が、悪意あるアプリ A を用意する。正規ユーザーをアプリ A で Google 認可させ、アプリ A 向けのアクセストークンを手に入れる(あるいは何らかの方法で漏れたトークンを入手する)。
  2. 攻撃者は、そのアクセストークンを、別の正規サービス B(「Google のアクセストークンでログインできます」式の実装)に突きつける。
  3. サービス B は「このトークンで /userinfo が引けた。中の sub はユーザー X だ。よし、ユーザー X としてログインさせよう」と誤認する。

こうして、攻撃者がユーザー X になりすましてサービス B にログインできてしまいます。 要点はこうです ―― 「アクセストークンを持っている」=「本人が今ここで認証した」ではありません。 そのトークンは別アプリ向けのものかもしれませんし、過去に漏れたものかもしれません。 ルームキーを拾った人が部屋に入れてしまうのと同じ構図です。

誤った前提トークンで /userinfo が引けた = 本人がいま認証された、とみなす。
抜けている検証そのトークンが自分(サービス B)宛に発行されたものか(aud)を確認していない。

正しい解: OpenID Connect と ID Token の検証

ログインをやりたいなら、OAuth 2.0 の上に認証レイヤを乗せた仕様 ―― OpenID Connect(OIDC Core 1.0)を使います。OIDC は同じ認可コードフローに乗せつつ、 アクセストークンとは別に ID Token(JWT 形式)を発行します。

ID Token は署名付きの「この人は認証された」という証明書で、次のクレーム(中身)を持ちます。

クレーム意味クライアントが検証すること
iss発行者期待する IdP(発行者)と一致するか
subユーザーの一意識別子これを本人 ID として使う
audこのトークンの受け手自分のクライアント IDと一致するか
exp失効時刻まだ切れていないか
iat発行時刻古すぎないか(必要に応じて)
nonceリプレイ防止用の値認可リクエスト時に送った値と一致するか

クライアントは ID Token の署名を検証し、iss が期待する発行者か、 aud が自分のクライアント ID か、exp が切れていないか、 nonce が一致するかを確かめてから、はじめて「この人は認証された」と判断します。 署名付きなので、途中で中身が書き換えられれば検知できます。

これで先のトークン置換が弾けます。攻撃者が突きつけるトークンはアプリ A 向けに発行されたものなので、 aud が「アプリ B のクライアント ID」と一致しません。サービス B は 「これは自分宛ではない」と検出して拒否します。デモB の OIDC 方式が緑で弾いたのは、この aud 検証です。

アクセストークン vs ID トークン ―― 用途の取り違え

混同しやすいので整理します。アクセストークンID トークンは、宛先も役割も違います。

観点アクセストークンID トークン
役割API を叩くための鍵「この人は認証された」という証明書
宛先(受け手)リソースサーバークライアント自身
クライアントの扱い中身を解釈しなくてよい(不透明でよい)署名・iss・aud・exp・nonce を検証してから使う
何を表すか何をしてよいか(許可)誰であるか・いつ認証したか(本人性)
取り違えるとログイン判定に流用 → なりすましの穴API アクセスに流用 → 用途違いで通らない/危険

取り違えると事故ります。ID トークンを API のアクセスに使うのも、 アクセストークンをログイン判定に使うのも、どちらも用途違いです。 前者は宛先が違うため通らない/危険で、後者がまさに前章で見たなりすましの穴です。

「Google でログイン」と「カレンダー連携」を言い分ける

実務で言葉を分けると、設計の取り違えが減ります。

Google でログイン実体は OIDC(認証)。ID Token を検証して「誰か」を確かめる。
カレンダー連携を許可実体は OAuth(認可・委譲)。アクセストークンで「何をしてよいか」を委ねる。

日常会話では両方まとめて「OAuth」と呼ばれがちです。ですがログイン用途は、正確には OIDC です。 「Sign in with Google」は認証、「Connect your Google Calendar」は委譲 ―― この区別を持っておくと、 「ログインなのにアクセストークンで本人確認しようとしていないか」に気づけます。

グッドケース / バッドケースのまとめ

バッドケース 自前で「アクセストークン → /userinfo → その ID でログイン」。aud 検証なし。 トークン置換でなりすまし可能。
グッドケース OIDC を使い、ID Token の署名・issaudexpnonceを検証してからセッションを張る。 素性のわかった IdP(Identity Provider)に本人確認を委ねる。

まとめ ―― 認可で本人確認をしない

OAuth 2.0 は認可のフレームワークです。アクセストークンは「持っている者が使える」許可証で、 「誰であるか」を語りません。だから「トークンが取れたからログイン成功」とみなす実装は、 認可を認証に流用しており、トークン置換でなりすまされる余地を残します。

ログインがやりたいなら、OAuth の上に立つ OIDC を使い、 発行された ID Token を検証してから「この人は認証された」と判断します。 aud が自分宛かを確かめるだけで、別アプリ向けトークンの突きつけは弾けます。 認可の道具で本人確認をしない ―― これが一行のまとめです。

出典・参考

  • RFC 6749 — The OAuth 2.0 Authorization Framework
  • RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage
  • OpenID Connect Core 1.0(ID Token のクレームと検証手順)
  • oauth.net «OAuth 2.0» / «User Authentication with OAuth 2.0»(OAuth は認証ではない、の解説)
  • Auth0 / Okta による解説(Access Token と ID Token の違い、ID Token を API 認可に使わない)

本ページは上記の一次仕様・解説を、操作しながら学べる教材として再構成したものです。 デモのトークン検証・なりすまし判定は、要点を伝えるために簡略化したモデルです。