プロンプトとスコープ
これは OAuth プロトコルおよびセキュリティ設計の問題です。RFC 8628 は、テキスト入力やブラウザの実行が容易でないスマート TV、メディアコンソール、プリンターなどの類似デバイスを対象としています。デバイスは device_code と user_code を要求します。ユーザーは別のブラウザでサインインして認可を行い、元のデバイスはトークンエンドポイントをポーリングします。このようなデバイスは通常パブリッククライアントであり、ファームウェア内に長期有効なクライアントシークレットを隠すことはできません。この演習は、システムブラウザを利用できるネイティブアプリ向けの認可コードフローを置き換えるものではありません。
面接官がテストしていること
- デバイスコード、ユーザーコード、検証 URI、アクセストークン、リフレッシュトークンの有効期間の区別。
authorization_pending、slow_down、expired_token、access_deniedの状態へのマッピング。- 制限付きポーリング、バックオフ、冪等性、並行性保護の設計。
- ユーザーコードのフィッシング、デバイスのなりすまし、総当たり推測、パブリッククライアントのリスクの認識。
最初に確認すべき明確化事項
デバイスが URI とコードを表示できるか、HTTPS リクエストを実行できるか、信頼できる時計を保持できるか、そして認可サーバーが OIDC サインインをサポートしているかを確認します。要求されるリソース、デバイスバインディング、コードの有効期間とポーリング間隔を設定する主体、キャンセルの要否を明確にします。デバイスにすでにシステムブラウザがある場合は、Device Flow を強制するのではなく、RFC 8252 の認可コードフローを優先します。
30秒での回答
2 つのエンドポイントを実装します。デバイス認可エンドポイントは、短命な device_code、人間が入力する user_code、verification_uri、有効期限、最小間隔を返します。トークンエンドポイントはデバイスコードのみを受け入れ、トークンまたは標準エラーを返します。デバイスコードのハッシュ、ステータス、クライアント、有効期限を保存します。サーバーから提供された間隔でポーリングします。authorization_pending では継続し、slow_down では間隔を延長し、成功、有効期限切れ、拒否、または恒久的な無効化で停止します。フィッシングを減らすため、ユーザーページにデバイス情報を表示します。
ステップバイステップの解決策
1. 参加者と単一の認可セッションのモデリング
参加者は、制約のあるデバイス、認可サーバー、ユーザーブラウザ、リソースサーバーです。デバイス認可エンドポイントは、高エントロピーな device_code、入力しやすい user_code、クライアント、要求スコープ、作成日時、有効期限、ポーリング状態を含むセッションを作成します。データベースの読み取りによって直接トークンが発行されないよう、デバイスコードの一方向ダイジェストのみを保存します。ユーザーコードは個別にインデックス化し、試行回数を制限します。
2. デバイス認可レスポンスの設計
少なくとも device_code、user_code、verification_uri、expires_in、interval を返します。verification_uri_complete は便利なリンクを提供できますが、デバイスとユーザーページの両方に同じショートコードを表示し、目の前にあるデバイスを確認するようユーザーに求めます。コードをログ、アナリティクス、スクリーンショットから保護します。サーバーはこのレスポンスにアクセストークンを含めてはなりません。
3. ユーザーのインタラクションとバインディングの処理
検証 URI を開いた後、ユーザーはサインインし、クライアント名、要求スコープ、デバイス情報を確認します。同意ページでは、デバイスが自分のものであることをユーザーに確認させる必要があり、これにより攻撃者のデバイスを認可してしまうリスクを軽減します。承認されるとセッションは AUTHORIZED に変更されます。ブラウザ経由でデバイスにトークンが送信されることはありません。拒否は終端の DENIED 状態となり、次回のポーリングで明示的に報告されます。
4. トークンエンドポイントのステートマシンの定義
トークンエンドポイントは、標準エラーを用いてデバイスの状態を遷移させます。
poll(device_code):
session = lookup(hash(device_code))
if session is missing: return invalid_grant
if now >= session.expiresAt: return expired_token
if session.status == DENIED: return access_denied
if session.status != AUTHORIZED: return authorization_pending
atomically mark CONSUMED
return issueAccessAndRefreshTokens(session)1 つのデバイスコードの正常な引き換えは、2 つのデバイストレッドが 2 組のトークンセットを受け取ることがないよう、アトミックでなければなりません。コードが 1 回限りの使用である場合、重複リクエストは invalid_grant またはプロトコルで規定された消費済みエラーを返します。失敗した再試行によって新しい認可セッションが作成されてはなりません。
5. ポーリングバックオフとサーバー保護の適用
デバイスはレスポンスの interval より早くポーリングしてはなりません。authorization_pending の後はその間隔を維持し、slow_down の後は少なくとも 5 秒延長します。デバイス側で合計期限、ネットワークタイムアウト、ジッターを追加します。サーバー側ではデバイスコード、クライアント、IP ごとにレート制限を行います。中間者が状態を再生しないよう、トークンレスポンスはキャッシュを禁止する必要があります。高負荷時には、ポーリングごとに高コストなバックグラウンドタスクを作成するのではなく、再試行可能なエラーを返します。
6. 有効期限切れ、キャンセル、復旧の処理
expired_token、access_denied、恒久的な invalid_grant は終端として扱います。ローカルのデバイスコードを消去し、ポーリングを停止します。デバイスのキャンセルアクションはセッションを CANCELLED としてマークし、その後のリクエストで復活させることはできません。ネットワーク障害時には、誤ったデバイスを認可しかねない複数のセッションを作成するのではなく、期限を保持して同じコードを再開します。再起動後、デバイスがセッション状態を安全に永続化できない場合は、最初からやり直してユーザーに通知します。
7. パブリッククライアントの保護とフィッシング耐性の確保
デバイスはクライアントシークレットを保護できないと想定します。スコープを最小限に抑え、リフレッシュトークンをローテーションおよび失効させます。ユーザーコードは入力しやすい短さにしつつ、オンラインでの推測に耐えられる十分なランダム性を持たせます。コードおよび IP ごとに失敗した試行をレート制限します。デバイス名、モデル、またはワンタイムコードを表示し、画面と照合するようユーザーに促します。ログにはセッション ID、結果、ハッシュ化された識別子のみを含め、未加工のデバイスコードやトークンは絶対に含めません。
8. フローの計装とテスト
デバイス認可の作成、完了、完了までの時間、セッションあたりのポーリング数、slow_down 率、有効期限切れと拒否、トークン引き換えの競合を計測します。重複引き換え、タイムアウト、クロックスキュー、ネットワーク再試行、推測、並行ポーリング、ユーザーによる拒否、サーバー再起動、スロットリングをテストします。エンドツーエンドのテストでは、承認されたデバイスと最終的なトークンセッションが一致することを証明する必要があります。2 つのエンドポイントがそれぞれ個別に 200 を返すだけでは不十分です。
模範回答
使い捨ての RFC 8628 セッションを作成します。デバイスエンドポイントは、高エントロピーな device_code、短いユーザーコード、検証 URI、有効期限、最小ポーリング間隔を返します。サーバーはハッシュ化されたコードと明示的なステータスを保存します。ユーザーはスマートフォンでサインインし、クライアント、スコープ、デバイス情報を確認して承認します。トークンエンドポイントはデバイスコードのみを受け入れます。authorization_pending では継続し、slow_down では少なくとも 5 秒追加し、有効期限切れ、拒否、または恒久的な無効化で終了します。並行ポーリングによって 2 組のトークンセットが発行されないよう、コードはアトミックに消費します。デバイスをパブリッククライアントとして扱い、スコープを最小限に抑え、リフレッシュトークンをローテーションし、推測を制限し、両方の画面でコードを照合するようユーザーに求めます。メトリクスとテストは、フローのバインディング、バックオフ、復旧、フィッシングの境界を網羅します。
よくある間違い
- デバイスコードを人間が入力するコードとして扱い、高価値な認証情報を UI やログに露出させる。
slow_downを無視し、トークンエンドポイントを固定の高頻度でポーリングする。- 有効期限切れや拒否を含むすべてのエラーを永久に再試行する。
- 静的なデバイスシークレットによってパブリッククライアントが機密クライアントになると見せかける。
- 承認後にブラウザの URL やページスクリプトを介してアクセストークンを直接送信する。
- デバイスコードをアトミックに消費できず、複数のリフレッシュトークンを発行してしまう。
- 承認されたデバイスがトークンセッションと一致することを証明せずに、ポーリングエンドポイントを個別にテストする。
フォローアップの質問
ユーザーコードの長さと有効期間はどのように選択しますか?
入力の利便性、オンライン推測のコスト、完了時間のバランスを取ります。紛らわしい文字を避け、失敗した試行回数と全体的なレートを制限し、ユーザーがスマートフォンを取り出し、サインインし、デバイスを確認するのに十分な長さにしつつ、無制限なフィッシングの機会を生み出さない有効期間にします。脅威モデルと観測された完了時間に基づいて数値を調整します。
悪意のあるクライアントがすべてのセッションを埋め尽くすのをどのように防ぎますか?
クライアント登録、IP、デバイスフィンガープリント、アカウントごとに作成レートを制限します。プリンシパルあたりの未完了セッション数を制限し、期限切れのセッションを非同期にクリーンアップします。セッションを作成する前にクライアントとスコープを検証します。不要なデバイスコードのクリーンアップ負荷をトークンエンドポイントに転嫁しないようにします。
オフラインになったデバイスは承認後にどのように復旧しますか?
再接続後にデバイスが同じコードでポーリングできるよう、有効期限までセッションを保持します。トークンセットまたは終端状態のいずれかを返します。レスポンスの喪失によって二重消費が発生しないよう、引き換え処理を冪等にします。有効期限が切れた後は、新しいコードで最初からやり直し、ユーザーに通知します。
verification_uri_complete のリスクは何ですか?
リンクにユーザーコードを含めることで入力の手間は軽減されますが、リンクの漏洩やリモートフィッシングのリスクが高まります。それでもショートコードを表示し、デバイス画面と照合するようユーザーに求める必要があります。完全なリンクをアナリティクス、Referer ヘッダー、履歴、サードパーティのリダイレクトに露出させないようにします。デバイスの機能と脅威モデルが許容する場合にのみ提供します。
なぜスマートフォンからデバイスへアクセストークンを転送しないのですか?
デバイス間でのトークン転送は、ブラウザ、QR コード、クリップボード、中間ページを介した露出を拡大し、受信側が正しいデバイスであることを証明するのが困難です。Device Flow では、デバイス自身が TLS 経由でコードを引き換えます。ユーザーページはサーバーの状態を変更するだけです。製品で必要な場合は、個別に認証された近距離無線チャネルを追加します。
ポーリングが正しくバックオフしていることをどのように検証しますか?
セッションレベルのポーリング回数、間隔の分布、slow_down 後の実際の間隔、終端レイテンシ、クライアント単位で集計された失敗を記録します。制御されたテストでポーリングの連続送信を注入し、slow_down によって同期されたリクエストの集中を起こすことなく、クライアントの間隔が少なくとも 5 秒延長されることを検証します。グローバルな間隔を緩和するのではなく、不正なクライアントを隔離します。