
2026年現在、安全確保支援士試験に鋭意チャレンジ中です。
どうしてもOAuth2.0認可コードグラントが覚えられなくて整理のために記事にしました。
情報処理安全確保支援士 令和5年春期 午後Ⅱ 問2を題材に、OAuth 2.0認可コードグラントとPKCE(RFC 7636)の連携シーケンス全10ステップをHTTP電文例とともに詳しく解説します。
安全確保支援士試験や高度試験にチャレンジしている同志の方の参考になればと~
① 認可フェーズ
「認可フェーズ」は利用者の同意確認を行います。
1.サービス要求 ([スマホアプリ]→[Webサーバ])
アプリがWebサーバへ「記事を投稿し、サービスTへも送信してほしい」と HTTP POST を送ります。
実際の投稿では、日記の本文データに加え、「サービスTにも連携投稿したい」という指示(フラグ)がデータ部(JSONなど)に含まれます。(推測)
POST /api/v1/diaries HTTP/1.1
Host: diary.example.com
Authorization: Bearer <これがのちに盗まれる新日記サービス自体のログインセッショントークン>
Content-Type: application/json
{
"title": "本日の活動記録",
"content": "本日はセキュリティ試験の勉強を行いました。",
"share_to_service_t": true
}- パラメータ:
title:日記のタイトルcontent:日記の本文share_to_service_t:サービスTへの同時投稿フラグ(true)
- 送信内容:日記の本文データに加え、「サービスTにも連携投稿したい」という指示(フラグ)がデータ部(JSONなど)に含まれます。(推測)
2.リダイレクト( [Webサーバ] → [スマホアプリ])
新日記サービスのWebサーバは記事データを受信しますが、サービスTへ代理投稿するためのアクセストークンをまだ所持していません。そのため、会員に対してサービスTの認可画面へアクセスするよう指示を返します。
HTTP/1.1 302 Found
Location: https://service-t.example.com/oauth/authorize?response_type=code&client_id=abcd1234&redirect_uri=diaryapp%3A%2F%2Fcallback- パラメータ(Locationヘッダ内):
response_type=code:認可コードグラント方式の指定client_id=abcd1234:サービスTから新日記サービスへ事前発行された識別子redirect_uri=diaryapp://callback:認可完了時に戻る先となるカスタムURLスキーム
- 処理内容:HTTPレスポンスヘッダの
LocationにサービスTの認可エンドポイントURLを指定し、ステータスコード302 Foundを返します。
認可要求を送信する直前に、端末上で動作するスマホアプリ(パブリッククライアント)の内部メモリでのみ以下の暗号処理を実行します。
- 検証コード(
code_verifier)の生成推測困難なランダム文字列(43〜128文字)を動的に生成し、アプリのメモリ領域に保持します。 - チャレンジコード(
code_challenge)の算出検証コードを暗号学的ハッシュ関数(SHA-256)でハッシュ化し、Base64urlエンコードを行ってチャレンジコードを算出します。
実際のネイティブアプリ開発においては、WebサーバがHTTP 302を返してアプリ内通信ライブラリが自動追従してしまうと、アプリ側でcode_challengeを付与できなくなります。そのため、Webサーバが認可エンドポイントURLをJSONデータとして返却するか、あるいは302リダイレクトをアプリ側で検知して外部ブラウザ(ASWebAuthenticationSessionやCustom Tabs)を起動する制御を行います。
3.認可要求 ([スマホアプリ] →[認可サーバ])
スマホアプリはアプリ内ブラウザ等を開き、サービスTの認可サーバへ認可要求を送信します(設問3(2) 表8のリクエスト[ p ]に該当)。
GET /oauth/authorize?response_type=code&client_id=abcd1234&redirect_uri=diaryapp%3A%2F%2Fcallback&code_challenge=E9Melhoa2OwvFrGMTJguCH5rtx64LxU4b...&code_challenge_method=S256 HTTP/1.1
Host: service-t.example.com- 送信パラメータ:
response_type=code:認可コードグラントの指定client_id:事前発行された識別子redirect_uri:認可後に戻るカスタムURLスキーム(例:diaryapp://callback)code_challenge:先ほど生成したハッシュ値(チャレンジコード)code_challenge_method=S256:変換方式(SHA-256)の明示- ※
state:CSRF(クロスサイトリクエストフォージェリ)防止用の推測困難なランダム文字列(※本試験設問では省略されていますが、RFC 6749 Section 10.12にて推奨されているパラメータです)
- 認可サーバ側の動作:受け取ったチャレンジコードを一時保存します。
4.認可同意処理 ([スマホアプリ] → [認可サーバ])
サービスTの認可サーバが画面を提示し、会員本人がアクセス権の付与を確認します。
(以下はGemini生成のダミーです)
アクセス権限の要求
新日記サービス が、あなたのアカウントに対する以下の権限を求めています:
- 記事の投稿 (
write:posts)
- 処理内容:会員がサービスTにログインしていない場合はログイン認証を行い、認可画面で「許可する」を選択します。
モバイル端末では、カスタムURLスキーム(例: diaryapp://)が複数のアプリによって重複登録された場合、OSが認可コードを含むリダイレクトを不正なアプリへ渡してしまう「認可コード横取り攻撃」のリスクが存在します。
PKCE(RFC 7636)を導入すると、認可要求時に送信したcode_challengeに対応する元の秘密情報code_verifierがトークン要求時に提示されない限り、認可サーバはアクセストークンを発行しません。仮に認可コードが横取りされても、攻撃者はアプリ内部メモリにあるcode_verifierを知り得ないため、不正取得を防止できます。
② 引き換えフェーズ
「引き換えフェーズ」では、同意に基づき発行された一時的な認可コード(引き換え券)を回収し、安全にバックエンドへ引き渡します。
5.リダイレクト( [認可サーバ] → [スマホアプリ])
認可サーバは同意を確認すると、一時的な「認可コード(code)」を払い出し、指定された redirect_uri(カスタムURLスキーム)へリダイレクトします。
HTTP/1.1 302 Found
Location: diaryapp://callback?code=5810f68ad195469d85f59a6d06e51e90- パラメータ(Locationヘッダ内):
code=5810f68ad195469d85f59a6d06e51e90:認可サーバが発行した一時的な認可コード本体
- セキュリティ上の着眼点:端末内に悪意のある別アプリが存在する場合、このリダイレクトを横取りされるリスクがあります。しかし、攻撃者は手元に正規アプリのメモリ内にある検証コード(
code_verifier)を持たないため、コード単体を奪われても悪用できません。
6.認可コードの受け渡し ([スマホアプリ] → [Webサーバ])
スマホアプリは、認可サーバから受け取った「認可コード」と、手元に保持していた「検証コード」をセットにして、新日記サービスのWebサーバへ送信します。
POST /api/v1/oauth/callback HTTP/1.1
Host: diary.example.com
Authorization: Bearer <これがのちに盗まれる新日記サービス自体のログインセッショントークン>
Content-Type: application/json
{
"code": "5810f68ad195469d85f59a6d06e51e90",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}- パラメータ:
code:認可サーバから受け取った認可コードcode_verifier:スマホアプリのメモリに保持していた検証コード原本
- 処理内容:端末側のスマホアプリからバックエンドのWebサーバへ、認可コードと検証コードの原本を引き渡します。
③ 発行・実行フェーズ
「発行・実行フェーズ」では、サーバ間の直接通信(バックチャネル)により、安全にアクセストークンを取得して代理処理を実行します。
7.アクセストークン要求( [Webサーバ] → [認可サーバ])
新日記サービスのWebサーバ(コンフィデンシャルクライアント)は、サービスTの認可サーバへ直接接続し、アクセストークンを要求します(設問3(2) 表8のリクエスト[ q ]に該当)。
POST /oauth/token HTTP/1.1
Host: service-t.example.com
Authorization: Basic YWJjZDEyMzQ6UEBzc3dvcmQ=
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=5810f68ad195469d85f59a6d06e51e90&redirect_uri=diaryapp%3A%2F%2Fcallback&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk- パラメータ:
Authorization: Basic ...:新日記サービス自体のクライアント認証ヘッダ(IDとシークレット)grant_type=authorization_code:認可コードグラントによるトークン要求の指定code:提示する認可コード(ステップ5・6)redirect_uri:認可要求時と同一のコールバックURIcode_verifier:スマホアプリから預かった検証コード原本(ステップ6)
- 処理内容:新日記サービスのWebサーバ(コンフィデンシャルクライアント)が認可サーバへ直接接続し、アクセストークンを要求します(表8のリクエスト番号 [ q ])。
認可サーバは受け取った code_verifier を自身でSHA-256ハッシュ化し、Base64urlエンコードします。その演算結果が、ステップ3で事前保存していた code_challenge と一致するか照合します。一致した場合のみ、正当な端末からの要求であると判定します。
8.アクセストークン応答 ([認可サーバ] → [Webサーバ])
検証に成功した認可サーバは、新日記サービスのWebサーバ宛てにアクセストークンを発行します。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "write:posts"
}- パラメータ:
access_token:発行されたアクセストークンtoken_type:トークン形式(Bearerトークン)expires_in:トークンの有効秒数(3600秒)scope:認可された操作範囲(write:posts)
- 処理内容:Webサーバは発行されたアクセストークンをデータベース等のセキュアな領域に保管します。
パターン1:PKCE検証失敗・認可コード無効(invalid_grant)
提示された検証コード(code_verifier)のハッシュ値が事前登録されたチャレンジコードと一致しない場合(偽アプリによる横取り攻撃時など)、または認可コード(code)自体の有効期限切れ・再利用時に返送されます。
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"error": "invalid_grant",
"error_description": "PKCE verification failed or invalid authorization code."
}パラメータ:
error:OAuth 2.0で定義されたエラー識別子(invalid_grant)error_description:人間が読めるエラーの詳細説明(任意)
処理内容:
- 認可サーバは、受け取った
code_verifierを自身でSHA-256ハッシュ化およびBase64urlエンコードした結果が、ステップ3で事前保存していたcode_challengeと一致しないことを検知します。 - 要求を不正と判定してアクセストークンの発行を拒否し、Webサーバへエラーを返答します。
パターン2:クライアント認証失敗(invalid_client)
新日記サービスのWebサーバが提示した Authorization: Basic ヘッダ内の認証情報(クライアントIDまたはクライアントシークレット)に誤りがある場合に返送されます。
HTTP/1.1 401 Unauthorized
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"error": "invalid_client",
"error_description": "Client authentication failed."
}パラメータ:
error:クライアント認証エラーを示す識別子(invalid_client)error_description:エラー理由の詳細テキスト
処理内容:
- 認可サーバはクライアント認証情報の照合に失敗したため、HTTPステータスコード
401 Unauthorized(または400 Bad Request)を返し、処理を中断します。
RFC 6749/RFC 7636における留意点
- ステータスコード:トークンエンドポイントにおけるパラメータ不備や認可コードの検証失敗は、原則として
400 Bad Requestを用いることがRFC 6749 Section 5.2で規定されています。クライアント認証そのものに失敗した場合は401 Unauthorizedが返されることがあります。 - キャッシュの禁止:エラーレスポンスであっても、認証・認可に関する応答情報が中継機器(プロキシ等)にキャッシュされないよう、
Cache-Control: no-storeおよびPragma: no-cacheヘッダの付与が義務付けられています。
9.記事投稿 ([Webサーバ] → [リソースサーバ])
新日記サービスのWebサーバは、取得したアクセストークンをヘッダに付与し、サービスTのリソースサーバ(投稿API)へ記事を代理投稿します。
POST /api/v1/posts HTTP/1.1
Host: api.service-t.example.com
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
Content-Type: application/json
{
"status": "【新日記サービスより同時投稿】本日の活動記録:本日はセキュリティ試験の勉強を行いました。"
}- パラメータ:
Authorization: Bearer ...:ステップ8で取得したアクセストークンstatus:サービスT(SNS)へ投稿する本文データ
- 処理内容:Webサーバは取得したアクセストークンをヘッダに付与し、サービスTのリソースサーバ(投稿API)へ記事を代理投稿します。リソースサーバは添付されたアクセストークンの署名や有効期限を検証し、正当であれば投稿処理を実行します。
10.記事投稿結果取得 ([リソースサーバ]→ [Webサーバ])
サービスTのリソースサーバからWebサーバへ投稿完了の通知が返されます。
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "post-987654",
"created_at": "2026-09-23T16:00:00Z"
}- パラメータ:
id:投稿された記事の固有識別子created_at:投稿作成日時
- 処理内容:新日記サービスのWebサーバは投稿完了を確認し、ステップ1で保留されていたスマホアプリへの応答(日記保存およびSNS連携完了通知)を返して全シーケンスが終了します。(以下はGemini生成のダミー通知です)
