OAuth 2.0 と OIDC — 「代理で使わせる」と「誰かを証明する」は別物
認可と認証を混同すると穴が開く。認可コードフローがなぜ遠回りに見える手順を踏むのか、一歩ずつ理由がある。
先に読んでおくとよい記事
この記事の進み方
What — 認可コードフローを一歩ずつ
OAuth の手順は一見遠回りです。しかし1つずつに理由があります。
トークンがブラウザを経由しない点が核心。ブラウザには「引換券」だけを渡し、実際のトークンはサーバー間の裏側の通信で受け取る。
各ステップが防いでいるもの
| 仕掛け | 防いでいる攻撃 | 無いとどうなるか |
|---|---|---|
| 認可コードを経由する | トークンの漏洩 | トークンが URL に乗り、履歴・Referer・ログに残る |
| client_secret | 別アプリによるコードの横取り | 盗んだコードを誰でもトークンに交換できる |
| PKCE(code_verifier) | 同上(secret を持てないアプリ向け) | モバイルアプリや SPA でコード横取りが成立する |
| state | CSRF | 攻撃者のアカウントに被害者を紐づけられる |
| nonce(OIDC) | ID トークンの再利用 | 過去に取得したトークンを流用できる |
| aud の検証 | 別サービス向けトークンの流用 | 冒頭の事故が成立する |
OAuth と OIDC の違い
「このアプリに、私の代わりにこれをさせてよい」という許可を与える仕組み。
得られるもの: アクセストークン
→ 「このトークンの持ち主は
カレンダーの読み取りをしてよい」
誰のものかは、トークン自体からは分からない- 答える問い
- 何をしてよいか
- 利用者の身元
- 保証しない
- 宛先の検証
- トークン単体では不可
- 用途
- 外部 API の代理利用
- –アクセストークンは「切符」。持っている人が使えるが、誰の切符かは書かれていない。
- –これでログイン判定をすると、他サービス向けの切符でも通ってしまう。
OAuth は「代理で使う許可」、OIDC は「誰であるかの証明」。ログイン機能を作るなら OIDC。OAuth のアクセストークンで本人確認をしてはいけない。
OAuth 2.0 が本来扱っているのはどちらですか?
Why — なぜパスワードを渡さない形にしたのか
OAuth 以前、外部サービスに自分のデータを使わせる方法はパスワードを渡すことでした。「連絡先をインポートします、メールのパスワードを入力してください」という画面が実在しました。
この方式の問題は明らかです。
- 権限を絞れない。 連絡先を読みたいだけなのに、メールの送信も削除もできてしまう
- 取り消せない。 使わせるのをやめたければ、パスワードを変えるしかない。他のサービスも全部止まる
- 保存される。 渡した先がパスワードをどう保管しているか分からない
OAuth は「パスワードを渡さずに、限定的な権限だけを、いつでも取り消せる形で委譲する」ために作られました。
なぜトークンを直接渡さないのか
初期の OAuth 2.0 には、認可サーバーがトークンを直接ブラウザに返す Implicit フロー がありました。手順が短くて簡単です。
しかし、トークンが URL のフラグメントに乗るため、次の経路で漏れます。
- ブラウザの履歴に残る
- 拡張機能や他のスクリプトから読める
- Referer ヘッダで外部に送られる可能性がある
現在 Implicit フローは非推奨です。代わりに、ブラウザには一度きりの引換券(認可コード)だけを渡し、実際のトークンはサーバー間の直接通信で受け渡す形になりました。
PKCE がなぜ後から追加されたのか
client_secret は、サーバーサイドのアプリなら安全に保管できます。しかしモバイルアプリや SPA は、配布した時点で中身が読まれます。secret を埋め込んでも秘密になりません。
secret を使えないと、認可コードを盗んだ攻撃者がトークンに交換できてしまいます。そこで PKCE は、secret の代わりに、その場で作った使い捨ての値を使います。
- アプリがランダム値
code_verifierを作る - そのハッシュ
code_challengeを、最初のリダイレクトに乗せる - トークン交換時に
code_verifierそのものを送る - 認可サーバーがハッシュを計算し、最初の
code_challengeと一致するか確認する
コードを盗んだ攻撃者は code_verifier を知らないので、交換できません。事前に「答えのハッシュ」を預けておき、後から「答え」を見せるという構造です。
現在は、サーバーサイドアプリでも PKCE を併用するのが推奨です。
他サービス向けに発行されたアクセストークンでログインされてしまう問題。何を確認していれば防げましたか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
この実装のどこに穴があり、どう直しますか?
- SaaS の管理画面に「Google でログイン」を実装した
- ① Google の認可画面へリダイレクトする(scope は openid email profile)
- ② 戻ってきた認可コードを、client_secret と一緒にトークンエンドポイントへ送る
- ③ 返ってきた access_token で https://www.googleapis.com/oauth2/v3/userinfo を叩く
- ④ レスポンスの sub をキーに、自社 DB のユーザーを引いてログインさせる
- ⑤ state パラメータは「実装が面倒だったので」省略している
- id_token も返ってきているが、使っていない
- · ④でログイン判定に使っているものは、認証の証明になっているか
- · ⑤を省略したことで、どんな攻撃が成立するか
- · 返ってきているのに使っていないものに、どんな価値があるか
「アクセストークンでプロフィールを取ればログインできる」という実装に、どこが危ういかを説明してください
- 相手の実装は実際に動いている
- 相手は OAuth と OIDC の違いを意識していない
- 直し方まで示したい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。