Device Code(RFC 8628)
device-authorization grant — 通称「device code」「device flow」 — は、ブラウザを動かせない、または パスワードを打つのが現実的でない クライアントのための grant です。スマート TV、ゲーム機、CLI ツール、IoT 機器、POS 端末などが該当します。
一番身近な例は、新しい TV で Netflix にサインインするときの手順です。TV 画面に短いコード(ABCD-EFGH)と URL(netflix.com/tv)が表示され、ユーザはスマホでその URL を開いてコードを入力し、承認します。TV はパスワードを一度も見ないままアクセストークンを受け取ります。
このページで触れる仕様
用語の補足
- device_code — OP が発行する、デバイスがポーリングに使う長い不透明な識別子。ユーザには見せない。
- user_code — デバイス画面に表示し、ユーザが verification ページに打ち込む短い人間可読コード(
BDWP-HQPK等)。 - verification_uri — デバイス画面に表示する URL(
https://op.example.com/device)。ユーザはスマホで開く。 - verification_uri_complete —
user_codeを埋め込み済みの URL。デバイスが QR コードを描画できれば、ユーザはスキャンするだけで打鍵が要らなくなる。 - interval — デバイスが
/tokenをポーリングする間隔(秒)。OP がslow_downを返すとこの値が引き上げられる。
フローの動き方
device_code だけで、パスワードは一度も見ません。短い user_code は、人間が文字を打てるキーボードで入力し直すためにあります。ポーリングは承認が届くまで interval 秒ごとに繰り返されます。デバイス側はパスワードを一切持ちません。ユーザもデバイスには何も打ちません。2 つの面が短い user_code を介して OP で合流します。
ポーリング応答
トークンエンドポイント(/token) はポーリングごとに以下のいずれかを返します:
| 応答 | 意味 | デバイスのふるまい |
|---|---|---|
400 authorization_pending | ユーザがまだ承認(または拒否)していない | interval 秒待って再度ポーリング |
400 slow_down | ポーリングが速すぎた | その後のすべての要求で interval を 5 秒延ばす(RFC 8628 §3.5)。OP は新しい interval を LastPolledAt と一緒に原子的に永続化するので、マルチレプリカ構成でも元に戻らない |
400 access_denied | ユーザが verification ページで 拒否 した、組み込み側が失効させた、またはデバイスが slow_down を 5 回無視した(poll_abuse) | ポーリングを停止し「サインインがキャンセルされました」と表示 |
400 expired_token | device_code が expires_in(既定 600 秒)を超えた、または以前のポーリングがすでに引き換えた | ポーリングを停止。再試行はフローからやり直す |
200 { access_token, ... } | ユーザが承認 | 通常の token 応答として処理 |
デバイスは interval(既定 5 秒。1 秒の揺らぎは許容)より速くポーリングできません。それより早いポーリングには slow_down が返り、5 回目の違反でレコードは access_denied で終了します。
device_code レコードは閉じた状態遷移をたどり、トークンエンドポイントの応答は状態で決まります。
| レコードの状態 | その状態になる契機 | 次のポーリングへの応答 |
|---|---|---|
| Pending | POST /device_authorization で作成された | authorization_pending |
| Approved | verification ページが ApproveUserCode を呼んだ | トークン発行。レコードは Consumed になる |
| Denied | ユーザが拒否した、組み込み側が失効させた、またはロックが発動した(poll_abuse、またはレコード指定の verify ヘルパによる user_code_lockout) | access_denied |
| Consumed | ポーリングが承認を引き換えた | expired_token |
引き換えは Approved から Consumed への原子的な遷移 1 回で行われるため、同時に届いた 2 つのポーリングの両方にトークンが渡ることはありません。負けた側は expired_token になります。
user_code は構造的に brute-force 可能
user_code は短いから使い物になります。長くしたら誰も入力しません。これは原理的に総当たりされ得るということでもあり、ユーザが打つより速く /device を呼び出せる攻撃者が勝ってしまいます。本ライブラリは op/devicecodekit を同梱しています。最初の手入力では、opaque なサーバ側 ceremony key を渡して VerifyUserCodeByAttemptKey を呼び、ユーザの認証と同意の後に ApproveUserCode を呼んでください。raw user_code を attempt key に使ってはなりません。Deps はミューテックスを含むため保持し、必ずポインタで渡してください。既定のリミッタはプロセスローカルなので、マルチインスタンス構成では共有のアトミックな AttemptLimiter が必要です。
OP が提供するものと自前で作るもの
op.WithDeviceCodeGrant(op.DeviceCodeGrant{...}) は /device_authorization をマウントし、discovery に device_authorization_endpoint を載せ、device grant を有効な集合に加えます。verification ページは提供しません。このページは自前でホストし(既定の URL は <issuer>/device、変更は DeviceCodeGrant.VerificationURI で指定します)、ユーザを認証したうえで devicecodekit のヘルパを呼びます。
deps := &devicecodekit.Deps{DeviceCodes: st.DeviceCodes(), Grants: st.Grants()} // 1 つ保持し、ポインタで渡す
ok, err := devicecodekit.VerifyUserCodeByAttemptKey(ctx, deps, ceremonyKey, typedCode)
// ok && err == nil なら同意画面を出し、その後
err = devicecodekit.ApproveUserCode(ctx, deps, typedCode, subject, authTime)
// 拒否なら devicecodekit.DenyUserCode(ctx, deps, typedCode, devicecodekit.DenyReasonUserDenied)デバイスに伝える expires_in と interval は、DeviceCodeGrant.TTL と DeviceCodeGrant.PollInterval で設定します。ゼロなら既定値(10 分と 5 秒)のままで、負の値は op.New が拒否します。
この grant は承認時にエンドユーザを解決するため、op.New にはユーザストアが必要です。op.WithUserStore か、ログインフローの op.PrimaryPassword の store のどちらかを指定してください。
承認の引き換え時に、そのクライアントに対する同意の grant が記録されます。このため device flow のトークンは RevokeSubject や grant management の対象になり、introspection と /userinfo では公開(pairwise)subject を返します。同じクライアントが同じ scope で後から /auth を呼ぶと、同意画面は省略されます。devicecodekit.Revoke(ctx, deps, deviceCode, reason) は、その grant の下に発行されたアクセストークンとリフレッシュトークンへ、Deps に設定したトークンストア(AccessTokens または GrantRevocations、OpaqueAccessTokens、RefreshTokens)を通じて連鎖して失効させます。Deps.Grants も設定しないと、grant レコード自体が残ります。
使うべきとき
device flow を選ぶのは、デバイス側に以下のいずれかの制約があるときです。
- ブラウザがない — セットトップボックス、スマート TV、音声アシスタント
- キーボードがない / 入力が困難 — TV リモコン、ゲームコントローラの D-pad
- CLI ツール で Web サーバを立てない種類のもの(
gcloud auth login、gh auth login、kubectl oidc-loginなど) - 画面を持たない 自動化の文脈で、プロビジョニング時に一度だけペアリングする運用
ブラウザが使えるクライアント(通常の SPA、カスタム URL スキームを持つネイティブアプリなど)には不要です。authorization_code + PKCE のほうが短く、安全で、UX も豊かです。RFC 8628 §3 自身も、Device Code フローを標準的なフローが現実的でないときの フォールバック として位置づけています。
動かしてみる
examples/31-device-code-cli は単一バイナリで RFC 8628 の往復を実演します。OP を立ち上げ、user_code パネルと verification_uri_complete のショートカットを表示し、数秒後にブラウザ承認をシミュレートして、OP が access_token + id_token を発行するまでポーリングします。
(cd examples/31-device-code-cli && GOWORK=off go run -tags example .)example は役割別ファイルに分割されています(op.go で OP の組み立て、cli.go でデバイス側のポーリング、device.go でブラウザ承認のシミュレーション、probe.go で self-verification)。各面を独立に読めます。
続きはこちら
- ユースケース: Device Code の組み込み —
op.WithDeviceCodeGrant、devicecodekit.VerifyUserCodeByAttemptKey、検証ページの契約、デバイス登録解除時に発行済みトークンを連鎖失効させる手順。 - 認可コード + PKCE — クライアントがブラウザを開けるなら、まずこちら。
- CIBA 入門 — 「ユーザが別チャネルにいる」の概念的な兄弟。コード表示を使わない方式。