はじめに
送信者限定アクセストークンを DPoP (RFC 9449) で実現する場合、クライアントはサーバへ送信するリクエストに DPoP proof JWT (Section 4) を含めます。
その DPoP proof JWT のペイロードには次のクレーム群を含めなければなりません。
| クレーム | 説明 |
|---|---|
jti |
DPoP proof JWT の一意識別子 |
htm |
リクエストの HTTP メソッド |
htu |
リクエストのターゲット URI |
iat |
DPoP proof JWT 生成時刻 |
これらに加え、DPoP proof JWT をアクセストークンと一緒に用いる場合、すなわち、クライアントが保護リソースにアクセスする際は、ath クレームも含めなければなりません。
| クレーム | 説明 |
|---|---|
ath |
アクセストークンの SHA-256 ハッシュの base64url 表現 |
また、サーバが nonce を要求する場合、nonce クレームも必須となります。
| クレーム | 説明 |
|---|---|
nonce |
サーバから提供された最新の nonce |
本記事ではこの nonce について詳細に見ていきます。
{
"jti": "XdPQLwZIH9pmlMhD",
"htm": "GET",
"htu": "https://rs.example.com/resource/1",
"iat": 1755096729,
"ath": "-7JjHPHFqtqwNHk7dPoU4nL7SgIgLn8vdCiDq-LfovY",
"nonce": "ZgmFr7UWLrJX0fHB"
}
nonce の意義
DPoP 仕様の Section 2. Objectives は、XSS コードが未来のタイムスタンプを持つ DPoP proof JWT を生成し、時間経過後に別所でそれを悪用する攻撃について言及しています。この攻撃に対し、もしもサーバが、サーバの提供する時間制限のあるデータを DPoP proof JWT に含めることを要求すれば、作り置きされた DPoP proof JWT を用いた不正アクセスの影響をいくらか抑えることができます。
DPoP proof JWT を生成するのはクライアントなので、そのペイロードに含む時刻関連クレームの値はクライアントが自由に選べます。そのため、DPoP proof JWT の有効期間をサーバ側で制御したい場合、サーバが提供する時間制限のあるデータを DPoP proof JWT に含めることを要求する必要があります。この目的のために nonce クレームが使われます。
nonce の提供方法
DPoP proof JWT に nonce が入っていることを要求するサーバは、DPoP proof JWT に nonce が含まれていない場合、DPoP-Nonce HTTP ヘッダを含むエラー応答を返します。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="use_dpop_nonce"
DPoP-Nonce: ZgmFr7UWLrJX0fHB
RFC 6750 仕様に従うリソースサーバであれば、エラー情報は WWW-Authenticate HTTP ヘッダに書かれており、エラーコードは use_dpop_nonce となっているはずです。
このエラーを受け取ったクライアントは、DPoP-Nonce ヘッダの値を取り出し、その値を nonce クレームの値として含む DPoP proof JWT を作成します。
そして、リソースリクエストの再送時にその新しい DPoP proof JWT をリクエストに含めます。
nonce 有効期限切れの判定
クライアントは、提供された nonce を以降のリソースリクエストでも使い続けます。しかし、その nonce もいつかは有効期限切れとなり、それが原因でサーバはエラー応答を返します。
エラー応答を受け取った際、クライアントはそのエラーの原因が「nonce の有効期限切れ」だと分かれば、そのエラー応答に含まれる最新の nonce を用いて DPoP proof JWT を作り直し、リクエストを再送するでしょう。
しかし、その判定方法はそれほど自明ではありません。というのは、それ専用のエラーコード (例えば dpop_nonce_expired というようなエラーコード) を DPoP 仕様で定め忘れたからです。
ですので、クライアント側としては、次の条件が成り立つときに「nonce 有効期限切れがエラーの原因」と判定することになります。
- レスポンスのステータスコードが 401 Unauthorized である
- 自分が現在保持している nonce の値とエラー応答に含まれる
DPoP-Nonceヘッダの値が異なる - エラーコードが
invalid_dpop_proofである
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="invalid_dpop_proof"
DPoP-Nonce: knvXdyXM3FK3QGEK
リクエスト再送ロジック
DPoP nonce を使う場合、定期的にサーバがエラー応答を返すのが期待動作となるので、クライアント側ではそれに対応するリクエスト再送処理を実装しなければなりません。結果としてエラー処理が煩雑になります。
参考までにリクエスト再送の擬似コードを掲載します。
// 現在クライアントが保持している nonce 値を得る。この nonce 値は DPoP proof
// JWT に埋め込まれる。もしもこのクライアントがこれまでにサーバから nonce の
// 提供を一度も受けていない場合、getDpopNonce() メソッドは null を返す。
String dpopNonce = getDpopNonce();
// リソースリクエストを行う。(DPoP proof JWT の生成は invoke メソッド内で行われる)
ResponseInfo rinfo = invoke(request, httpMethod, path, body, mediaType, dpopNonce);
// レスポンスの中に DPoP-Nonce HTTP ヘッダが含まれていれば、そのヘッダの値で
// このクライアントが保持する nonce 値を更新する。レスポンスが DPoP-Nonce
// ヘッダを含んでいなければ、updateDpopNonceIfSupplied() メソッドは
// nonce 値の更新を行わず、null を返す。
String updatedDpopNonce = updateDpopNonceIfSupplied(rinfo.getResponse());
// レスポンスが DPoP nonce エラーを示している場合
if (isDpopNonceError(rinfo.getResponse(), dpopNonce, updatedDpopNonce))
{
// 最新の nonce 値を用いて DPoP proof JWT を作り、リクエストを再送する。
rinfo = invoke(request, httpMethod, path, body, mediaType, updatedDpopNonce);
}
// レスポンスのステータスコード
int status = rinfo.getResponse().getStatus();
// リクエストが成功した場合
if (200 <= status && status < 300)
{
return rinfo;
}
// リクエストが失敗した場合
throw callError(httpMethod, path, rinfo);
DPoP nonce 関連の設定項目 (Authlete 固有)
Authlete には次の DPoP nonce 関連の設定項目があります。
| 設定項目 | 説明 |
|---|---|
Service.dpopNonceDuration |
DPoP nonce の有効秒数 |
Service.dpopNonceRequired |
サーバが DPoP nonce を要求するかどうか |
Client.dpopNonceRequired |
当クライアントに対して DPoP nonce を要求するかどうか |
おわりに
DPoP については過去に動画や文書で説明させていただいております。是非ご覧ください。
【動画】 OAuth・OpenID 標準仕様による徹底的な API 保護 (17:22〜)
【文書】 標準仕様による徹底的な API 保護



