- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- 標準リダイレクトが使えない状況
- Device Authorization Grant(RFC 8628)
- CIBA — Decoupled 認証
- DPoP(RFC 9449)— トークンを鍵に縛る
- フローの組み合わせと PAR
- セキュリティ上の考慮事項
- 実践:bash で作る device flow クライアント
- 選択ガイド
- 2026 年の文脈 — AI エージェントと CLI による device flow ルネサンス
- おわりに
- 参考資料
はじめに
OAuth の標準シナリオは明確です。ユーザーがブラウザでサービスにアクセスし、認可サーバーにリダイレクトされてログインし、サービスに戻ってきます。Authorization Code Flow + PKCE はこのシナリオでうまく機能し、OAuth 2.1 draft はこれを事実上唯一のユーザー向けフローとして整理しています。
問題は、ブラウザリダイレクトが不可能な状況が思った以上に多いことです。スマート TV にはキーボードがなく、CLI ツールにはブラウザがないかもしれず、コールセンターのオペレーターは顧客のデバイスに触れません。そして 2026 年現在、最もホットな事例 — AI エージェントがユーザーの代わりに API へアクセスする必要があるのに、エージェントプロセスの中でリダイレクトを受け取る方法がありません。MCP(Model Context Protocol)エコシステムで OAuth が標準の認可メカニズムとして定着し、Keycloak 26.6 が OAuth Client ID Metadata Document(CIMD)を実験的にサポートして MCP authorization server の役割を担えるようになったことで、「ブラウザのないデバイスの認証」問題は再び中心的な議題になりました。
本記事では Device Authorization Grant(RFC 8628)、CIBA、DPoP(RFC 9449)の三つのメカニズムをワイヤレベルで確認し、Keycloak の設定、セキュリティ上の考慮事項、選択ガイドを整理します。
標準リダイレクトが使えない状況
まず問題空間を整理します。リダイレクトベースのフローが行き詰まる典型的な状況は次のとおりです。
| 状況 | 制約 | 適切なメカニズム |
|---|---|---|
| スマート TV、ゲーム機、IoT | 入力手段が貧弱、ブラウザがないか不便 | Device Flow |
| CLI ツール、デーモンでのユーザー認証 | ローカルブラウザの有無が不確実 | Device Flow(または loopback リダイレクト) |
| コールセンターのオペレーターが顧客認証を必要とする | 認証する主体とリクエストする主体が別デバイス | CIBA |
| POS/キオスクでの顧客承認 | 顧客のスマートフォンで承認が必要 | CIBA |
| AI エージェントの代理アクセス | エージェントに UI がない | Device Flow + Token Exchange |
| トークン窃取への防御強化 | bearer トークンの限界 | DPoP(フローではなくトークンバインディング) |
注意点:DPoP は認証フローではなくトークン拘束メカニズムです。どのフローとも組み合わせ可能であり、三つを同じレイヤーのものと見なしてはいけません。
Device Authorization Grant(RFC 8628)
RFC 8628 は「入力が不便なデバイス」と「ユーザーのスマートフォン/PC ブラウザ」を分離するフローです。TV にコードを表示し、ユーザーはスマートフォンでそのコードを入力して承認します。
+----------+ +---------------+
| TV/CLI | | 認可サーバー |
| (デバイス)| | |
+----+-----+ +-------+-------+
| (1) POST /device_authorization |
| ------------------------------------>|
| (2) device_code + user_code + |
| verification_uri |
| <------------------------------------|
| |
| (3) 画面に表示: |
| 「example.com/device で |
| コード WDJB-MJHT を入力」 |
| |
| +----------+ (4) スマホの |
| | ユーザーの| ブラウザで |
| | スマホ | ---------------->|
| +----------+ コード入力+ログイン+同意
| |
| (5) その間デバイスはポーリング |
| POST /token (device_code) |
| ------------------------------------>|
| authorization_pending ... |
| <------------------------------------|
| (6) ユーザー承認完了後 |
| access_token を発行 |
| <------------------------------------|
v v
HTTP で見る全プロセス
ステップ 1、デバイスが device authorization エンドポイントを呼び出します。
POST /realms/prod/protocol/openid-connect/auth/device HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded
client_id=smart-tv-app&scope=openid%20profile
レスポンスです。
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "https://idp.example.com/realms/prod/device",
"verification_uri_complete": "https://idp.example.com/realms/prod/device?user_code=WDJB-MJHT",
"expires_in": 600,
"interval": 5
}
デバイスは user_code と verification_uri を画面に表示します(verification_uri_complete を QR コードで見せるのが UX 上一般的です)。そして interval 秒間隔でトークンエンドポイントをポーリングします。
POST /realms/prod/protocol/openid-connect/token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code
&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&client_id=smart-tv-app
ユーザーがまだ承認していなければ次のレスポンスが返ります。
{
"error": "authorization_pending",
"error_description": "The authorization request is still pending"
}
ポーリングが速すぎると slow_down エラーとともに間隔を広げる指示が来ます。ユーザーがスマートフォンでコードを入力しログイン/同意を完了すると、次のポーリングで正常なトークンレスポンスが返ってきます。
Keycloak の設定
Keycloak では Device Flow をクライアント単位で有効にします。
# クライアントに device authorization grant を有効化
kcadm.sh update clients/CLIENT-UUID -r prod \
-s 'attributes."oauth2.device.authorization.grant.enabled"="true"'
# realm レベルでコード寿命/ポーリング間隔を調整
kcadm.sh update realms/prod \
-s 'attributes."oauth2DeviceCodeLifespan"="600"' \
-s 'attributes."oauth2DevicePollingInterval"="5"'
CLI ツールなら public クライアント + PKCE の組み合わせが基本です。user_code 入力ページは Keycloak が標準で提供しており、テーマでカスタマイズできます。
CIBA — Decoupled 認証
CIBA(Client Initiated Backchannel Authentication)は OpenID Foundation の CIBA Core 仕様で定義された decoupled フローです。Device Flow とは逆方向です。Device Flow はユーザーがコードを持って認可サーバーへ「出向く」構造なら、CIBA は認可サーバーがユーザーの認証デバイスへ「やって来る」構造です。
典型的なシナリオはコールセンターです。オペレーターが顧客識別子(電話番号など)で認証リクエストを送ると、顧客のスマートフォンにプッシュ通知が表示され「オペレーターに口座照会を許可しますか?」と尋ね、顧客が承認するとオペレーターのシステムがトークンを受け取ります。POS での高額決済の承認、バックオフィス作業に対する管理者承認にも同じパターンが使われます。
+------------+ +-----------+ +----------+
| オペレーター| | 認可サーバー| | 顧客の |
| システム(CC)| | | | スマホ |
+-----+------+ +-----+-----+ +----+-----+
| (1) POST /ext/ciba/auth | |
| login_hint=顧客識別子 | |
| --------------------------------->| |
| (2) auth_req_id を返却 | |
| <---------------------------------| |
| | (3) 認証デバイスへプッシュ |
| | ------------------------->|
| | (4) 顧客が生体認証で承認 |
| | <-------------------------|
| (5) poll: POST /token | |
| grant_type=ciba, auth_req_id | |
| --------------------------------->| |
| (6) access_token | |
| <---------------------------------| |
v v v
バックチャネル認証リクエスト
POST /realms/prod/protocol/openid-connect/ext/ciba/auth HTTP/1.1
Host: idp.example.com
Authorization: Basic Y2FsbC1jZW50ZXI6czNjcjN0
Content-Type: application/x-www-form-urlencoded
login_hint=customer-01087654321
&scope=openid%20account%3Aread
&binding_message=CS-4711
&requested_expiry=120
binding_message は両方の画面(オペレーターと顧客のスマートフォン)に同時に表示される短いコードで、顧客が「今表示された承認リクエストが、自分が通話中のあの件である」と確認できるフィッシング防御の仕掛けです。レスポンスは次のとおりです。
{
"auth_req_id": "1c266114-a1be-4252-8ad1-04986c5b9ac1",
"expires_in": 120,
"interval": 5
}
poll、ping、push — 三つのトークン配送モード
CIBA はクライアントが結果を受け取る方式として三つのモードを定義します。
| モード | 動作 | クライアント要件 | 備考 |
|---|---|---|---|
| poll | クライアントがトークンエンドポイントを定期ポーリング | なし(最もシンプル) | Device Flow に類似したパターン |
| ping | 承認完了時に AS がクライアント通知エンドポイントへ通知、クライアントがトークンエンドポイントを呼び出す | コールバックエンドポイントが必要 | ポーリング負荷を削減 |
| push | AS がトークン自体をクライアントのコールバックへ直接配送 | コールバック + 強いセキュリティ要件 | FAPI-CIBA プロファイルでは禁止 |
poll モードのトークンリクエストは次のとおりです。
POST /realms/prod/protocol/openid-connect/token HTTP/1.1
Host: idp.example.com
Authorization: Basic Y2FsbC1jZW50ZXI6czNjcjN0
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba
&auth_req_id=1c266114-a1be-4252-8ad1-04986c5b9ac1
Keycloak での CIBA
Keycloak は poll と ping モードをサポートします。CIBA の核心的な難所は「ユーザーの認証デバイスへどうやって信号を送るか」ですが、Keycloak はこれを外部の認証エンティティに委任する構造を取ります。realm の CIBA ポリシーでバックチャネル認証方式を設定し、実際のプッシュ/承認処理は自社のモバイルアプリ + 認証サーバーが HTTP コールバックで連携します。
# realm CIBA ポリシー設定例
kcadm.sh update realms/prod \
-s 'attributes."cibaBackchannelTokenDeliveryMode"="poll"' \
-s 'attributes."cibaExpiresIn"="120"' \
-s 'attributes."cibaInterval"="5"'
# クライアントに CIBA grant を有効化
kcadm.sh update clients/CLIENT-UUID -r prod \
-s 'attributes."oidc.ciba.grant.enabled"="true"'
導入時に最も工数がかかるのは Keycloak の設定ではなく、認証デバイス側の実装(プッシュ受信、承認 UI、結果コールバック)である点を、あらかじめ計画に織り込んでおくべきです。
DPoP(RFC 9449)— トークンを鍵に縛る
DPoP は「このトークンは特定の鍵ペアの保有者だけが使える」を強制する sender-constraining メカニズムです。bearer トークンは盗めばそのまま使えますが、DPoP トークンは盗んでも秘密鍵がなければ無用の長物です。
proof JWT の構造
クライアントはリクエストごとに DPoP proof という JWT を作り、DPoP ヘッダーに載せて送ります。proof のヘッダーには公開鍵(jwk)が、ペイロードにはリクエストバインディング情報が入ります。
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
"y": "9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA"
}
}
{
"jti": "-BwC3ESc6acc2lTc",
"htm": "POST",
"htu": "https://idp.example.com/realms/prod/protocol/openid-connect/token",
"iat": 1781234567
}
- jti:proof の一意 ID。サーバーが再利用を検知するために使用
- htm/htu:この proof が有効な HTTP メソッドと URI。他のエンドポイントへの再利用を遮断
- iat:発行時刻。許容時間枠(通常数十秒)を外れると拒否
トークン発行時、認可サーバーは proof の公開鍵サムプリント(jkt)をアクセストークンの cnf クレームに刻みます。以降の API 呼び出しでは二つが一緒に送られます。
GET /api/accounts HTTP/1.1
Host: api.example.com
Authorization: DPoP eyJhbGciOiJFUzI1NiIs...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs...
リソースサーバーの検証手順は、(1) proof の署名を proof 内の公開鍵で検証、(2) その公開鍵のサムプリントとトークン cnf の jkt の一致を確認、(3) htm/htu/iat/jti の検証、(4) リソースサーバーが要求するなら proof の ath クレーム(アクセストークンのハッシュ)まで確認、という流れです。
リプレイ防御のレイヤー
DPoP のリプレイ防御は多層的です。htu/htm で別リクエストへの再利用を防ぎ、iat の時間枠で古い proof をふるい落とし、jti の追跡で同じ proof の再提出を捕まえます。さらにサーバーが DPoP-Nonce ヘッダーで独自発行の nonce を要求でき、この場合 proof にサーバー nonce が含まれなければならないため、事前生成型のリプレイが根本から遮断されます。nonce 要求時、サーバーは 400 レスポンスで use_dpop_nonce エラーとともに新しい nonce を返し、クライアントは proof を作り直して再試行します。クライアント SDK がこの再試行ループを処理するか必ず確認してください。
Keycloak での DPoP
Keycloak 26.x は DPoP をサポートしており、クライアント単位で強制できます。
# クライアントに DPoP バインディングを強制
kcadm.sh update clients/CLIENT-UUID -r prod \
-s 'attributes."dpop.bound.access.tokens"="true"'
有効化するとトークンエンドポイントが DPoP proof を要求し、発行されたトークンに cnf/jkt が含まれます。ゲートウェイ/リソースサーバー側の検証ロジック(特にプロキシの URL 書き換えによる htu 不一致の問題)もあわせて点検すべきです。
フローの組み合わせと PAR
三つのメカニズムは排他的ではありません。実践で有用な組み合わせは次のとおりです。
- Device Flow + DPoP:CLI ツールが device flow でトークンを取得しつつ DPoP でバインドすれば、トークンファイルが漏洩しても鍵ファイルなしには使用できません。
- CIBA + FAPI:金融分野の decoupled 承認は FAPI-CIBA プロファイルに従います(push モード禁止、署名付きリクエストなどの強化要件)。
- PAR + リダイレクトフロー:キオスクのようにリダイレクトは可能だがリクエストの完全性が重要な場合、PAR(RFC 9126)で認可リクエストをバックチャネル登録し、短い request_uri だけをフロントに送ります。PAR は device flow や CIBA のバックチャネルリクエストと「リクエストのバックチャネル化」という哲学を共有しています。
セキュリティ上の考慮事項
Device code フィッシング
Device Flow の最大の弱点はユーザー側の検証の欠如です。攻撃者が自分のデバイスでフローを開始し、user_code が含まれた verification_uri_complete のリンクを被害者に送って「セキュリティ確認のためログインしてください」と騙すと、被害者の承認によって攻撃者のデバイスがトークンを受け取ります。実際、device code フィッシングはここ数年、国家背景の攻撃グループの常套手段でした。
緩和策は次のとおりです。
- 同意画面にデバイス情報と警告を明示(「TV アプリから開始されたリクエストです。自分で開始していなければ拒否してください」)
- user_code の短い寿命と試行回数制限、レートリミット
- 機微なスコープには device flow での発行を禁止するクライアントポリシー
- 異常兆候の検知:デバイスリクエストの IP と承認 IP の地理的不一致のアラート
- ユーザー教育とバインディングコンテキストの表示(リクエスト時刻、位置)
CIBA のリスク — 承認疲れと無差別リクエスト
CIBA は攻撃者が任意のユーザーへ承認プッシュを送れる構造のため、MFA プッシュ疲労(fatigue)攻撃と同様のパターンが可能です。login_hint でユーザーを特定できるクライアントを厳格に制限し、binding_message の表示を義務化し、承認リクエストの頻度をユーザー単位で制限すべきです。requested_expiry は可能な限り短く設定します。
DPoP の限界
DPoP はトークン窃取には強いものの、鍵とトークンが同居するデバイス自体が侵害されれば防げません。また、proof 生成鍵を非抽出(non-extractable)ストレージ(WebCrypto、Secure Enclave、TPM)に置かなければ防御価値が急減します。時計のずれ(iat 検証失敗)とプロキシの URL 変形(htu 不一致)は最もよくある運用障害ポイントです。
実践:bash で作る device flow クライアント
概念を定着させるために、curl と jq だけで動作する最小の device flow クライアントを作ってみましょう。CI スクリプトや社内 CLI ツールの骨格としてそのまま使えます。
#!/usr/bin/env bash
set -euo pipefail
IDP="https://idp.example.com/realms/prod/protocol/openid-connect"
CLIENT_ID="internal-cli"
# 1. device authorization リクエスト
resp=$(curl -s -X POST "$IDP/auth/device" \
-d "client_id=$CLIENT_ID" -d "scope=openid profile")
device_code=$(echo "$resp" | jq -r .device_code)
user_code=$(echo "$resp" | jq -r .user_code)
verification_uri=$(echo "$resp" | jq -r .verification_uri)
interval=$(echo "$resp" | jq -r .interval)
echo "ブラウザで $verification_uri を開き、"
echo "コード [$user_code] を入力してください。"
# 2. 承認されるまでポーリング
while true; do
sleep "$interval"
token_resp=$(curl -s -X POST "$IDP/token" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=$device_code" \
-d "client_id=$CLIENT_ID")
error=$(echo "$token_resp" | jq -r '.error // empty')
case "$error" in
"") break ;; # 成功
authorization_pending) continue ;; # 待機中
slow_down) interval=$((interval + 5)) ;; # 間隔を拡大
expired_token)
echo "コードの有効期限が切れました。再試行してください。" >&2; exit 1 ;;
*) echo "エラー: $error" >&2; exit 1 ;;
esac
done
access_token=$(echo "$token_resp" | jq -r .access_token)
echo "トークン発行完了(先頭 20 文字): $(echo "$access_token" | cut -c1-20)..."
本番コードならここに三つ追加すべきです。第一に、トークンをディスクに保存する場合はパーミッション 600 のユーザー専用ディレクトリ(または OS キーチェーン)を使います。第二に、refresh token の更新ループと期限切れ処理を入れます。第三に、可能なら DPoP の鍵ペアを生成して発行段階からバインドします。expired_token の処理のように、ユーザーが席を外したときの UX(再試行の案内)も見落としやすい部分です。
選択ガイド
状況別の意思決定を表に整理します。
| 質問 | はいの場合 |
|---|---|
| ユーザーが同じデバイスのブラウザでログインできるか | Authorization Code + PKCE(必要なら PAR) |
| デバイスに入力/ブラウザがなく、ユーザーが別デバイスを使えるか | Device Flow |
| リクエストを開始する主体と承認するユーザーが別チャネルにいるか | CIBA |
| ユーザーの関与なしにシステム権限だけが必要か | Client Credentials |
| 別サービス呼び出しのためにユーザートークンを変換する必要があるか | Token Exchange(RFC 8693) |
| トークン窃取時の被害を遮断する必要があるか | 上記フロー + DPoP(または mTLS) |
2026 年の文脈 — AI エージェントと CLI による device flow ルネサンス
Device Flow は 2012 年頃に TV アプリのために設計された古いメカニズムですが、2026 年現在、採用が最も速く伸びているフローでもあります。理由は明確です。
- CLI/開発者ツールの標準パターン化:GitHub CLI をはじめとする主要な開発者ツールが device flow をデフォルトのログインとして採用したことで、「ターミナルにコードを表示してブラウザで承認」が開発者にとって馴染みのある UX になりました。
- AI エージェントの人間承認ループ:エージェントがユーザーの代わりに API へアクセスするには、どこかで人間の同意が必要です。エージェントプロセスにはブラウザがないため、device flow の「コードの受け渡し + 外部承認」構造が自然な合意点になります。MCP エコシステムでは、OAuth 認可サーバー(例:CIMD をサポートする Keycloak 26.6)とエージェントクライアントの組み合わせがこのパターンの上に構築されつつあります。
- 承認後の委任:エージェントが受け取ったユーザートークンを個別のツール/API 用に絞って使うには Token Exchange と組み合わせます。「device flow で最初の同意 → exchange で作業ごとの最小権限トークン」がエージェント認証の推奨骨格です。
- 非人間アイデンティティ(non-human identity)の急増:エージェントとワークロードが人間より多くなる環境において、人間の介入ポイントを明示的に設計するフロー(device flow、CIBA)と鍵ベースの拘束(DPoP)の組み合わせは、Zero Trust 原則の実務への翻訳と言えます。
古い仕様が新しい問題の答えになることは、標準の世界では珍しくありません。ただし device code フィッシングのように採用拡大とともに攻撃も増えているため、上記の緩和策をデフォルトとして敷いた上で始めるべきです。
おわりに
要約します。
- リダイレクトが使えない状況は例外ではなく日常です。Device Flow(同じユーザー、別デバイス)、CIBA(別の主体、別チャネル)で問題を分類してください。
- Device Flow はシンプルですが、フィッシング防御を設計に含める必要があります。user_code の寿命、レートリミット、同意画面の警告は基本です。
- CIBA は poll モードから始め、認証デバイス側の実装工数を過小評価しないでください。binding_message は常に使用します。
- DPoP はフローではなくトークン拘束です。どのフローにも追加でき、public クライアントのトークン窃取防御として最も費用対効果が高い手段です。
- Keycloak は三つのメカニズムをすべてサポートしているため、クライアントポリシーで「機微なクライアントには DPoP を強制」のようなガードレールを宣言的に敷けます。
- AI エージェント時代の認証骨格は「device flow で人間の同意を確保 → token exchange で最小権限を委任 → DPoP で拘束」です。
フローはあくまで道具です。核心は「誰が、どこで、何を使って承認するのか」を明確にした上で、それに合った標準を選ぶことです。
参考資料
- RFC 8628 — OAuth 2.0 Device Authorization Grant
- OpenID Connect Client-Initiated Backchannel Authentication (CIBA) Core
- RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- RFC 9126 — OAuth 2.0 Pushed Authorization Requests
- RFC 8693 — OAuth 2.0 Token Exchange
- RFC 9700 — Best Current Practice for OAuth 2.0 Security
- OAuth 2.1 draft (draft-ietf-oauth-v2-1)
- FAPI: Client Initiated Backchannel Authentication Profile
- Keycloak Documentation
- Keycloak Release Notes
- Keycloak 26.6.0 Released
- OpenID Connect Core 1.0