OAuth + OIDCを「自分で扱う」意味とセキュリティリスク― Webアプリ開発者とIdPプロバイダ、それぞれの視点から
OAuth + OIDCを「自分で扱う」意味とセキュリティリスク
関連記事
― Webアプリ開発者とIdPプロバイダ、それぞれの視点から
前回の記事 OAuthとは何か?― 既存の認証方式と比較して理解する「OAuthが解いている問題」 では、OAuthとOIDCが何を解決する仕組みなのかを整理しました。
本記事では一歩踏み込んで、
- Webアプリ開発者として、OAuth + OIDCをどう使うのか
- IdPプロバイダとして、OAuth + OIDCをどう実装するのか
- OAuth + OIDCの標準仕様がカバーしないセキュリティリスクは何か
を扱います。
実装のチェックリストや検証項目を読む前に、まず「OIDCフローが完了した時点でアプリがどんな状態になっているか」を確認しておきます。この土台を持っていないと、何を守るために何を実装しているのかが分かりにくくなります。
Part 1:Webアプリ開発者の視点
「OAuth + OIDCを使う」とは何をすることか
Webアプリ開発者が「OAuth + OIDCを使う」と言うとき、実際には2つの選択があります。
| パターン | 内容 | 例 |
|---|---|---|
| A. 既存のIdPを利用する | Google/GitHub/Microsoftなど外部のIdPと連携する | 「Googleでログイン」 |
| B. 自前のIdPを構築する | 自社内にOIDC対応の認証サーバーを立てる | Keycloak、Auth0などを使うか、自作する |
多くのWebアプリはパターンAから始まります。
パターンBは「マイクロサービス構成での社内統一認証」や「BtoBのSSOサービス提供」が典型的なユースケースです。
パターンA:外部IdP連携(OAuth + OIDC)
ログイン完了時点でのアプリの状態
OIDCのコールバック処理が終わった時点で、アプリのサーバーは以下の情報を持ちます。
| アプリが保持するもの | 由来 | アプリ側での扱い |
|---|---|---|
sub(ユーザー外部ID) |
IDトークン |
users テーブルの外部キー |
email / name
|
IDトークン | プロフィールの初期値 |
| セッションID | アプリが発行 | セッションストア + HTTP-only Cookie |
| アクセストークン | OAuthで取得 | サーバーサイドのみ保持(外部API呼び出し用) |
ここで重要な分離があります:
-
subでユーザーをDBに登録・照合するのはOIDCの結果を使う部分 - セッションIDはアプリが独自に発行するもの。以降のリクエスト認証はこのセッションIDで行う
- アクセストークンはGoogle等の外部APIを呼ぶための鍵。ブラウザには渡さない
OIDCはセッション確立の「入口」であり、確立後はアプリ独自のセッション管理が担います。
IDトークンをそのままセッションとして使い回すのは誤りです(詳細はPart 2 セクション4)。
何を実装するか
外部IdP(例:Google)との連携では、開発者が実装するのは「OAuth認可コードフロー」のクライアント側です。
1. 認可リクエスト(Authorization Request)の生成
□ state パラメータ(CSRF対策)を生成してセッションに保存
□ nonce パラメータ(リプレイ攻撃対策)を生成してセッションに保存
□ scope=openid を含める(OIDCを有効にする)
□ PKCE用の code_verifier / code_challenge を生成する(SPAやモバイルの場合)
□ ユーザーをIdPの認可エンドポイントへリダイレクト
2. コールバックエンドポイントの処理
□ state をセッションと照合する(不一致なら処理中断)
□ 認可コードをトークンエンドポイントへ送り、ID + アクセストークンを取得
□ IDトークンの署名をIdPの公開鍵で検証する
□ iss(発行者)が期待するIdPのURLと一致するか確認する
□ aud(対象受信者)が自アプリのclient_idと一致するか確認する
□ exp(有効期限)が切れていないか確認する
□ nonce をセッションと照合する
□ sub をキーにユーザーをDBに登録 or 照合する
3. セッション確立
□ アクセストークンはサーバーサイドにのみ保存する(クライアントに露出しない)
□ IDトークンをそのままセッション管理のトークンとして使わない
□ アプリ独自のセッションIDを発行してブラウザにHTTP-only Cookieで返す
IDトークンは「認証の証明書」であり、APIアクセスのトークンではありません。
セッション管理には別途セッションID(CookieやJWTセッション)を発行してください。
なぜ自前でやる意味があるか
next-auth や passport.js などのライブラリを使えば、ほぼ上記を自動化できます。
では自前で理解する意味は?
実際の開発現場で発生するのは「ライブラリが想定していないケース」です。
- IdPから返ってくるクレームをどうDBスキーマに対応させるか
- 複数のIdPを同一ユーザーに紐づける(アカウント連結)
- テナント分離のあるBtoBアプリでの認証設計
- 障害発生時にトークンや認可フローのどこで失敗したかを読む
フローを理解していないと、ライブラリが吸収している問題が「なぜ起きているのか」分からなくなります。
パターンB:自前IdPの構築
どういう場合に必要か
- 社内で複数のWebアプリを持ち、SSOを実現したい
- BtoBのSaaSで顧客企業ごとに認証プロバイダを分けたい
- 認証フロー自体をカスタマイズしたい(MFA必須化など)
まずKeycloakやAuth0などのOSS・SaaSを検討する のが現実的です。それでも要件が合わない場合に自前実装を考えます。
OIDCにおけるDiscoveryの役割
クライアントがIdPと連携するには「どのURLに認可リクエストを送るのか」「トークンはどこで取得できるのか」「公開鍵はどこにあるのか」を事前に知る必要があります。
OIDCはこれを Discoveryドキュメント(/.well-known/openid-configuration)で解決しています。クライアントはこのURLにアクセスするだけで、IdPとの接続に必要なすべての情報を自動取得できます。IdP実装者は、このドキュメントに記載した「約束」を守るエンドポイントを用意する義務があります。
自前IdPが持つべき機能
必須エンドポイント:
- 認可エンドポイント GET /authorize
ユーザーをIdPのログイン画面へ誘導し、認証後に認可コードを発行する
- トークンエンドポイント POST /token
クライアントが認可コードを送り、IDトークン・アクセストークンを受け取る
- JWKSエンドポイント GET /.well-known/jwks.json
IDトークンの署名に使った公開鍵を公開する(クライアントが署名検証に使う)
- Discoveryエンドポイント GET /.well-known/openid-configuration
上記エンドポイントのURLや対応仕様をまとめたメタデータを返す
発行物:
- アクセストークン(JWT or opaque)
- IDトークン(JWT、署名必須)
- リフレッシュトークン(オプション)
Discoveryドキュメントの例
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "email", "profile"]
}
クライアントはこのURLを見るだけで、どのエンドポイントにどう接続すればいいか分かります。
つまりIdP実装者は、このドキュメントの「約束」を守る義務があります。
IDトークンの署名
IDトークンは RS256(RSA + SHA-256) で署名するのが標準です(HS256は共有秘密鍵のため避ける)。
秘密鍵(IdPだけが持つ)→ IDトークンに署名
公開鍵(JWKSエンドポイントで公開)→ クライアントが署名を検証
鍵ローテーション(定期的に鍵ペアを更新)も考慮が必要です。JWTには kid(Key ID)を含め、クライアントが正しい公開鍵を選択できるようにします。
MFA(多要素認証)と AMR クレーム
自前のIdPでMFAを実装する場合、「ユーザーがどの認証方式を使ったか」をIDトークンに記録する標準的な方法があります。
AMR(Authentication Methods References)クレーム:
{
"sub": "1234567890",
"email": "tanaka@example.com",
"amr": ["pwd", "otp"],
"acr": "urn:mace:incommon:iap:silver",
"exp": 1700000000
}
| クレーム | 意味 | 値の例 |
|---|---|---|
amr |
使用した認証方式のリスト |
["pwd"](パスワードのみ)、["pwd", "otp"](パスワード + ワンタイムパスワード)、["webauthn"](WebAuthn/パスキー) |
acr |
認証コンテキストクラス(認証強度の分類) | IAP仕様またはサービス独自の文字列 |
主な amr 値:
| 値 | 認証方式 |
|---|---|
pwd |
パスワード認証 |
otp |
ワンタイムパスワード(TOTP/HOTP) |
sms |
SMS認証 |
hwk |
ハードウェアキー(FIDO2等) |
webauthn |
WebAuthn / パスキー |
これらのクレームをIDトークンに含めることで、クライアントアプリは「このユーザーはMFAで認証されているか」を判断できます。利用シーンはPart 2「その他のセキュリティリスク」の「認証強度(AMR/ACR)の未検証」で説明します。
具体例:自作WebアプリにOIDCログインを組み込む
ここまでの内容を踏まえて、実際に自社でWebアプリを開発する場面でOIDCがどう機能するかを具体的に見ておきます。
社内ツール群(勤怠管理・経費申請)を自社開発していて、それぞれのアプリが個別にID/パスワード管理をしている状態を考えます。「一度ログインすればどのツールも使えるようにしたい(SSO)」という要件が出てきたとき、OIDC対応の認証サーバーを自前で立てることになります。
登場人物:
| 名称 | 役割 | 具体例 |
|---|---|---|
| 認証サーバー(IdP) | 自作する認証専用アプリ | auth.company.internal |
| 勤怠管理アプリ | IdPに認証を委譲するWebアプリ | kintai.company.internal |
| 経費申請アプリ | 同上 | expense.company.internal |
| ユーザー | 社員 | 田中さん(tanaka@company.com) |
ログインの流れと情報の動き:
①ユーザーが勤怠管理アプリにアクセス
[田中さんのブラウザ] → GET https://kintai.company.internal/dashboard
→ 未ログインなので、アプリがIdPへリダイレクト
②認可リクエスト(勤怠管理アプリ → IdP)
[ブラウザ] → GET https://auth.company.internal/authorize
?client_id=kintai-app ← 勤怠管理アプリの識別子
&redirect_uri=https://kintai.company.internal/callback
&response_type=code
&scope=openid email profile
&state=abc123 ← CSRF対策トークン(アプリがセッションに保存)
&nonce=xyz789 ← リプレイ攻撃対策(同上)
③IdPがログイン画面を表示 → 田中さんがID/パスワードを入力
[IdPのログイン画面でのみ認証情報を入力 ← 各アプリには渡らない]
④IdPが認可コードを発行してリダイレクト
[IdP] → 302 Redirect to:
https://kintai.company.internal/callback
?code=AUTH_CODE_XXXXXX ← 短命(数分)の使い捨てコード
&state=abc123 ← ②で送ったstateが返ってくる
⑤アプリがコードをトークンに交換(サーバー間通信)
[勤怠管理アプリのサーバー] → POST https://auth.company.internal/token
grant_type=authorization_code
code=AUTH_CODE_XXXXXX
redirect_uri=https://kintai.company.internal/callback
client_id=kintai-app
client_secret=SECRET_YYYY ← このアプリだけが知るシークレット
[IdP → アプリのサーバーへ返却]
{
"id_token": "eyJhbGci...", ← 田中さんの情報を含むJWT
"access_token": "at_ZZZZ", ← IdPのAPIを呼ぶための鍵
"token_type": "Bearer",
"expires_in": 3600
}
⑥アプリがIDトークンを検証・ユーザー特定
IDトークンをデコードすると:
{
"sub": "user-001", ← 田中さんの内部ID(DBキー)
"email": "tanaka@company.com",
"name": "田中太郎",
"amr": ["pwd"],
"iss": "https://auth.company.internal", ← 発行者検証
"aud": "kintai-app", ← 自アプリ宛か確認
"exp": 1700003600, ← 有効期限確認
"nonce": "xyz789" ← ②で保存したnonceと照合
}
→ users テーブルで sub をキーに田中さんのレコードを照合 or 初回なら作成
⑦アプリが自前のセッションを発行
[アプリのサーバー]
- セッションID(ランダム文字列)を生成してセッションストアに保存
- access_token はセッションストアのみに保存(ブラウザには返さない)
- ブラウザへ HTTP-only Cookie でセッションIDだけを返す
Set-Cookie: session_id=SID_QQQQ; HttpOnly; Secure; SameSite=Lax
⑧以降のリクエストはセッションIDで認証(OIDCは関与しない)
GET /dashboard → Cookie: session_id=SID_QQQQ → セッションストアで照合
経費申請アプリへのアクセス時(SSO):
田中さんがすでにIdPにログイン済みの状態で経費申請アプリにアクセスすると、
IdPは「このブラウザはセッションがある」と判断し、
②〜③のログイン画面をスキップして④の認可コード発行に直接進む
→ 田中さんは再度パスワードを入力せずにログインできる(SSO)
この流れを通して、各アプリが直接パスワードを扱わないという構造が実現できます。認証情報はIdPだけが保持し、各Webアプリは「誰がログインしたか」という結果だけを受け取ります。
Part 2:OAuth + OIDCがカバーしないセキュリティリスク
OAuth + OIDCは 認可と認証のフロー自体 を標準化しています。しかし「フローが正しく動いている」ことと「アプリ全体が安全である」ことは別の話です。
以下は、OAuth + OIDCを正しく実装したとしても 別途対応が必要なセキュリティリスク です。
1. OAuth CSRFによるアカウント乗っ取り
リスク
state パラメータの検証を省略すると、攻撃者が被害者のアカウントに任意の外部アカウントを連結できます。
具体的な攻撃シナリオ
前提:myapp.com がGitHub連携機能を持っている
1. 攻撃者が自分のGitHubアカウントで myapp.com の「GitHub連携」を開始
2. リダイレクト途中でブラウザを止め、コールバックURL(code=xxx&state=yyy)を保存
3. 被害者にそのURLをクリックさせる(メール/SNSなどで誘導)
4. myapp.com が state を検証していなければ、
被害者のアカウントに攻撃者のGitHubアカウントが連結される
5. 次回「GitHubでログイン」を使えば、攻撃者が被害者のアカウントに侵入できる
対策
□ state パラメータを認可リクエスト時にランダム生成し、セッションに保存する
□ コールバック時に、受け取った state とセッションの state が一致するか検証する
□ 一致しない場合は処理を中断する
2. オープンリダイレクト攻撃
リスク
OAuth認可フローでは redirect_uri を使ってコールバック先URLを指定します。
IdPが redirect_uri を厳格に検証していないと、フィッシングサイトへ誘導できます。
具体的な攻撃シナリオ
正規のコールバックURL:
https://myapp.com/callback
攻撃者が細工したリクエスト:
https://idp.example.com/authorize?
client_id=myapp
&redirect_uri=https://evil.com/callback ← 書き換え
&response_type=code
IdP の redirect_uri 検証が前方一致だけであれば:
https://myapp.com.evil.com/callback
または
https://myapp.com/callback/../../../evil.com
このURLへ認可コードが送られる
→ 攻撃者がコードを入手してトークンと交換できる
対策
IdPとして実装する場合:
□ redirect_uri を登録制にし、完全一致(exact match)で検証する
□ ワイルドカードや前方一致での検証は使わない
クライアントアプリとして実装する場合:
□ redirect_uri にパラメータを動的に含めない
□ 自アプリのドメイン外にリダイレクトする処理を作らない
3. 認可コードの横取り(Code Interception)
リスク
認可コードはブラウザのURLに乗って返ってきます(例:?code=4/aBcD...)。
これが盗まれると攻撃者がアクセストークンを入手できます。
具体的な攻撃シナリオ
モバイルアプリでのカスタムURIスキーム乗っ取り:
正規アプリ:com.myapp://callback
悪意あるアプリ(同じカスタムスキームを登録):com.myapp://callback
認可コードを含むコールバックURLを悪意あるアプリが横取り
→ トークンに交換して正規ユーザーとして動作
対策
□ PKCE(Proof Key for Code Exchange)を必ず使う
- code_verifier(ランダム文字列)を生成する
- その SHA-256 ハッシュ(code_challenge)を認可リクエストに含める
- コードをトークンに交換するとき code_verifier を送る
- IdP側で code_challenge と照合する
→ コードを横取りしても code_verifier がないとトークンに交換できない
4. セッション管理の不備
リスク
OAuth + OIDCでユーザーを認証した後の セッション管理はOAuthの仕様外 です。
IDトークンをそのままセッション管理に使うのは誤用であり、様々なリスクがあります。
具体的な問題
問題①:IDトークンをセッショントークンとして使う
❌ アンチパターン:
Cookie: id_token=eyJhbGci... ← IDトークンをCookieに保存して認証トークン代わりにする
問題:
- IDトークンは有効期限が短い(通常1時間以下)
- クライアントへのIDトークン露出がないように設計されている
- セッション無効化(強制ログアウト)ができない
問題②:ログアウト後もトークンが有効
OIDCには id_token_hint を使ったログアウトフロー(RP-Initiated Logout)がありますが、
アクセストークンのリボーク(無効化)は実装しないと残り続けます。
対策
□ 認証後は自アプリのセッション(セッションID or 独自JWT)を発行する
□ IDトークンはユーザー特定にのみ使い、セッションとは分離する
□ アクセストークンはサーバーサイドのみで保持し、クライアントに返さない
□ ログアウト時はアクセストークンをリボークし、セッションを削除する
□ リフレッシュトークンはHTTP-only Cookieに保存する(JSから読めないように)
その他のセキュリティリスク
上記以外にも、実装時に意識すべきリスクがあります。
-
IDトークン検証の不備:
alg: none攻撃(署名なしJWTの受け入れ)やaud検証漏れ(別アプリ宛トークンの流用)。使用アルゴリズムをコード側でハードコードし、audは必ず自アプリのclient_idと照合する。 -
Implicit Flowの残存:URLフラグメントにトークンを返す旧方式で、ブラウザ履歴・Refererヘッダ経由でトークンが漏洩する。Authorization Code Flow + PKCEに移行し、トークンはlocalStorageに保存しない。
-
スコープの過剰付与:
repo admin:orgなど不要な権限まで要求すると、漏洩時の被害が拡大しフィッシングアプリとの区別もつかなくなる。必要最小限のスコープのみを要求する(最小権限の原則)。 -
認証強度(AMR/ACR)の未検証:決済・パスワード変更などの高リスク操作で、パスワードのみのログインとMFAログインを区別していないと、MFA回避状態で操作が実行できてしまう。
amrクレームを確認し、MFA未実施なら再認証へリダイレクトする(Step-up Authentication)。
参考資料
-
OAuth 2.0 Security Best Current Practice(RFC 9700)
OAuth 2.0実装における現在のセキュリティベストプラクティス集。 -
RFC 7636 - PKCE
認可コード横取り攻撃への対策。 -
OpenID Connect Core 1.0
IDトークンの検証要件が定義されています(Section 3.1.3.7)。 -
OAuth 2.0 Threat Model and Security Considerations(RFC 6819)
OAuth 2.0の脅威モデルと対策の網羅的なリスト。 -
RP-Initiated Logout 1.0
OIDCのログアウトフロー仕様。 -
RFC 8176 - Authentication Method Reference Values
IDトークンのamrクレームに使用する認証方式の識別子(pwd、otp、webauthn等)の標準値一覧。 -
OpenID Connect Core 1.0 - acr / amr Claims
IDトークンのacr(認証コンテキスト)とamr(認証方式リスト)クレームの定義。
Discussion