Skip to content

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 はパスワードを一度も見ないままアクセストークンを受け取ります。

このページで触れる仕様
  • RFC 8628 — OAuth 2.0 Device Authorization Grant
  • RFC 6749 — OAuth 2.0 Authorization Framework(用語)
用語の補足
  • 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 を要求し、ユーザに移動先を表示し、ユーザが別画面で承認する間トークンエンドポイントをポーリングします。承認が届くと次のポーリングでトークンが返ります。コードを発行する承認を待つDeviceTV · CLI · コンソールOPgo-oidc-providerUserスマホ · 別画面POST /device_authorizationclient_id · scope200 · device_code, user_codeverification_uri · intervalURL とコードを画面に表示op.example.com/deviceuser_code=ABCD-EFGHPOST /token · ポーリングgrant_type=…:device_code400 · authorization_pendingGET /device · user_codeログイン · 同意 · 承認device_code → subjectPOST /token · 次のポーリング200 · access_tokenid_token? · refresh_token?
デバイスが保持するのは 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_tokendevice_code が expires_in(既定 600 秒)を超えた、または以前のポーリングがすでに引き換えたポーリングを停止。再試行はフローからやり直す
200 { access_token, ... }ユーザが承認通常の token 応答として処理

デバイスは interval(既定 5 秒。1 秒の揺らぎは許容)より速くポーリングできません。それより早いポーリングには slow_down が返り、5 回目の違反でレコードは access_denied で終了します。

device_code レコードは閉じた状態遷移をたどり、トークンエンドポイントの応答は状態で決まります。

レコードの状態その状態になる契機次のポーリングへの応答
PendingPOST /device_authorization で作成されたauthorization_pending
Approvedverification ページが 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 のヘルパを呼びます。

go
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 を発行するまでポーリングします。

sh
(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 入門 — 「ユーザが別チャネルにいる」の概念的な兄弟。コード表示を使わない方式。