仕組みから学ぶ Web
発展読了目安 16#OAuth#OIDC#認可#SSO

OAuth 2.0 と OIDC — 「代理で使わせる」と「誰かを証明する」は別物

認可と認証を混同すると穴が開く。認可コードフローがなぜ遠回りに見える手順を踏むのか、一歩ずつ理由がある。

この記事の進み方

What — 認可コードフローを一歩ずつ

OAuth の手順は一見遠回りです。しかし1つずつに理由があります。

Figure認可コードフロー + PKCE
利用者ブラウザアプリあなたのサービス認可サーバーGoogle などリソースAPI「◯◯でログイン」を押す認可サーバーへリダイレクトログインして同意する認可コードを付けて戻す認可コードを渡すコード + client_secret + code_verifierアクセストークン + ID トークンアクセストークンで API を呼ぶ
1/8
「◯◯でログイン」を押すアプリは検証用のランダム値(code_verifier)を作り、そのハッシュ(code_challenge)を用意する。

トークンがブラウザを経由しない点が核心。ブラウザには「引換券」だけを渡し、実際のトークンはサーバー間の裏側の通信で受け取る。

各ステップが防いでいるもの

仕掛け防いでいる攻撃無いとどうなるか
認可コードを経由するトークンの漏洩トークンが URL に乗り、履歴・Referer・ログに残る
client_secret別アプリによるコードの横取り盗んだコードを誰でもトークンに交換できる
PKCE(code_verifier)同上(secret を持てないアプリ向け)モバイルアプリや SPA でコード横取りが成立する
stateCSRF攻撃者のアカウントに被害者を紐づけられる
nonce(OIDC)ID トークンの再利用過去に取得したトークンを流用できる
aud の検証別サービス向けトークンの流用冒頭の事故が成立する

OAuth と OIDC の違い

Compare何を得るための仕組みか

「このアプリに、私の代わりにこれをさせてよい」という許可を与える仕組み。

得られるもの: アクセストークン

→ 「このトークンの持ち主は
    カレンダーの読み取りをしてよい」

誰のものかは、トークン自体からは分からない
答える問い
何をしてよいか
利用者の身元
保証しない
宛先の検証
トークン単体では不可
用途
外部 API の代理利用
  • アクセストークンは「切符」。持っている人が使えるが、誰の切符かは書かれていない。
  • これでログイン判定をすると、他サービス向けの切符でも通ってしまう。

OAuth は「代理で使う許可」、OIDC は「誰であるかの証明」。ログイン機能を作るなら OIDC。OAuth のアクセストークンで本人確認をしてはいけない。

確認 — ここまで読めたか

OAuth 2.0 が本来扱っているのはどちらですか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜパスワードを渡さない形にしたのか

OAuth 以前、外部サービスに自分のデータを使わせる方法はパスワードを渡すことでした。「連絡先をインポートします、メールのパスワードを入力してください」という画面が実在しました。

この方式の問題は明らかです。

  • 権限を絞れない。 連絡先を読みたいだけなのに、メールの送信も削除もできてしまう
  • 取り消せない。 使わせるのをやめたければ、パスワードを変えるしかない。他のサービスも全部止まる
  • 保存される。 渡した先がパスワードをどう保管しているか分からない

OAuth は「パスワードを渡さずに、限定的な権限だけを、いつでも取り消せる形で委譲する」ために作られました。

なぜトークンを直接渡さないのか

初期の OAuth 2.0 には、認可サーバーがトークンを直接ブラウザに返す Implicit フロー がありました。手順が短くて簡単です。

しかし、トークンが URL のフラグメントに乗るため、次の経路で漏れます。

  • ブラウザの履歴に残る
  • 拡張機能や他のスクリプトから読める
  • Referer ヘッダで外部に送られる可能性がある

現在 Implicit フローは非推奨です。代わりに、ブラウザには一度きりの引換券(認可コード)だけを渡し、実際のトークンはサーバー間の直接通信で受け渡す形になりました。

PKCE がなぜ後から追加されたのか

client_secret は、サーバーサイドのアプリなら安全に保管できます。しかしモバイルアプリや SPA は、配布した時点で中身が読まれます。secret を埋め込んでも秘密になりません。

secret を使えないと、認可コードを盗んだ攻撃者がトークンに交換できてしまいます。そこで PKCE は、secret の代わりに、その場で作った使い捨ての値を使います。

  1. アプリがランダム値 code_verifier を作る
  2. そのハッシュ code_challenge を、最初のリダイレクトに乗せる
  3. トークン交換時に code_verifier そのものを送る
  4. 認可サーバーがハッシュを計算し、最初の code_challenge と一致するか確認する

コードを盗んだ攻撃者は code_verifier を知らないので、交換できません。事前に「答えのハッシュ」を預けておき、後から「答え」を見せるという構造です。

現在は、サーバーサイドアプリでも PKCE を併用するのが推奨です。

確認 — ここまで読めたか

他サービス向けに発行されたアクセストークンでログインされてしまう問題。何を確認していれば防げましたか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

この実装のどこに穴があり、どう直しますか?

与えられた条件
  • SaaS の管理画面に「Google でログイン」を実装した
  • ① Google の認可画面へリダイレクトする(scope は openid email profile)
  • ② 戻ってきた認可コードを、client_secret と一緒にトークンエンドポイントへ送る
  • ③ 返ってきた access_token で https://www.googleapis.com/oauth2/v3/userinfo を叩く
  • ④ レスポンスの sub をキーに、自社 DB のユーザーを引いてログインさせる
  • ⑤ state パラメータは「実装が面倒だったので」省略している
  • id_token も返ってきているが、使っていない
この軸で考える
  • · ④でログイン判定に使っているものは、認証の証明になっているか
  • · ⑤を省略したことで、どんな攻撃が成立するか
  • · 返ってきているのに使っていないものに、どんな価値があるか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「アクセストークンでプロフィールを取ればログインできる」という実装に、どこが危ういかを説明してください

与えられた条件
  • 相手の実装は実際に動いている
  • 相手は OAuth と OIDC の違いを意識していない
  • 直し方まで示したい

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。