OAuth 2.0 DPoP バインドアクセストークン

RFC 9449 OAuth 2.0 所有証明の実証 (DPPoP とは) [IETF] (英語) は、アクセストークンの送信者制約のためのアプリケーションレベルのメカニズムです。

DPoP の主な目的は、認可サーバーによるアクセストークンの発行時に公開鍵にアクセストークンをバインドし、リソースサーバーでアクセストークンを使用する際にクライアントが対応する秘密鍵を所有していることを証明することを要求することにより、権限のないクライアントや不正なクライアントが漏洩または盗難されたアクセストークンを使用することを防ぐことです。

DPoP を介して送信者によって制約されるアクセストークンは、アクセストークンを所有するすべてのクライアントが使用できる一般的なベアラートークンとは対照的です。

DPoP は DPoP 証明 [IETF] (英語) の概念を導入します。これはクライアントによって作成され、HTTP リクエストのヘッダーとして送信される JWT です。クライアントは DPoP 証明を用いて、特定の公開鍵に対応する秘密鍵を所有していることを証明します。

クライアントがアクセストークンリクエストを開始すると、HTTP ヘッダーに DPoP 証明を添付します。認可サーバーは、アクセストークンを DPoP 証明に関連付けられた公開鍵にバインド(送信者制約)します。

クライアントが保護されたリソースリクエストを開始すると、HTTP ヘッダー内のリクエストに DPoP 証明が再度添付されます。

リソースサーバーは、アクセストークンにバインドされた公開鍵に関する情報を、アクセストークン(JWT)内から直接、トークンイントロスペクションエンドポイント経由で取得します。次に、リソースサーバーは、アクセストークンにバインドされた公開鍵が DPoP 証明内の公開鍵と一致することを検証します。また、DPoP 証明内のアクセストークンハッシュがリクエスト内のアクセストークンと一致することも検証します。

DPoP アクセストークンリクエスト

DPoP を用いて公開鍵に紐付けられたアクセストークンをリクエストするには、クライアントは認可サーバーのトークンエンドポイントにアクセストークンリクエストを行う際に、有効な DPoP 証明を DPoP ヘッダーに含めなければなりません(MUST)。これは、認可付与型(例: authorization_coderefresh_tokenclient_credentials など)に関係なく、すべてのアクセストークンリクエストに適用されます。

次の HTTP リクエストは、DPoP ヘッダーに DPoP 証明が含まれる authorization_code アクセストークンリクエストを示しています。

POST /oauth2/token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJraWQiOiJyc2EtandrLWtpZCIsInR5cCI6ImRwb3Arand0IiwiYWxnIjoiUlMyNTYiLCJqd2siOnsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJraWQiOiJyc2EtandrLWtpZCIsIm4iOiIzRmxxSnI1VFJza0lRSWdkRTNEZDdEOWxib1dkY1RVVDhhLWZKUjdNQXZRbTdYWE5vWWttM3Y3TVFMMU5ZdER2TDJsOENBbmMwV2RTVElOVTZJUnZjNUtxbzJRNGNzTlg5U0hPbUVmem9ST2pRcWFoRWN2ZTFqQlhsdW9DWGRZdVlweDRfMXRmUmdHNmlpNFVoeGg2aUk4cU5NSlFYLWZMZnFoYmZZZnhCUVZSUHl3QmtBYklQNHgxRUFzYkM2RlNObWtoQ3hpTU5xRWd4YUlwWThDMmtKZEpfWklWLVdXNG5vRGR6cEtxSGN3bUI4RnNydW1sVllfRE5WdlVTRElpcGlxOVBiUDRIOTlUWE4xbzc0Nm9SYU5hMDdycTFob0NnTVNTeS04NVNhZ0NveGxteUUtRC1vZjlTc01ZOE9sOXQwcmR6cG9iQnVoeUpfbzVkZnZqS3cifX0.eyJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20vb2F1dGgyL3Rva2VuIiwiaWF0IjoxNzQ2ODA2MzA1LCJqdGkiOiI0YjIzNDBkMi1hOTFmLTQwYTUtYmFhOS1kZDRlNWRlYWM4NjcifQ.wq8gJ_G6vpiEinfaY3WhereqCCLoeJOG8tnWBBAzRWx9F1KU5yAAWq-ZVCk_k07-h6DIqz2wgv6y9dVbNpRYwNwDUeik9qLRsC60M8YW7EFVyI3n_NpujLwzZeub_nDYMVnyn4ii0NaZrYHtoGXOlswQfS_-ET-jpC0XWm5nBZsCdUEXjOYtwaACC6Js-pyNwKmSLp5SKIk11jZUR5xIIopaQy521y9qJHhGRwzj8DQGsP7wMZ98UFL0E--1c-hh4rTy8PMeWCqRHdwjj_ry_eTe0DJFcxxYQdeL7-0_0CIO4Ayx5WHEpcUOIzBRoN32RsNpDZc-5slDNj9ku004DA

grant_type=authorization_code\
&client_id=s6BhdRkqt\
&code=SplxlOBeZQQYbYS6WxSbIA\
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb\
&code_verifier=bEaL42izcC-o-xBk0K2vuJ6U-y1p9r_wW2dFWIWgjz-

以下は、DPoP Proof JWT ヘッダーとクレームの表現を示しています。

{
  "typ": "dpop+jwt",
  "alg": "RS256",
  "jwk": {
    "kty": "RSA",
    "e": "AQAB",
    "n": "3FlqJr5TRskIQIgdE3Dd7D9lboWdcTUT8a-fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRvc5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4_1tfRgG6ii4Uhxh6iI8qNMJQX-fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2kJdJ_ZIV-WW4noDdzpKqHcwmB8FsrumlVY_DNVvUSDIipiq9PbP4H99TXN1o746oRaNa07rq1hoCgMSSy-85SagCoxlmyE-D-of9SsMY8Ol9t0rdzpobBuhyJ_o5dfvjKw"
  }
}
{
  "htm": "POST",
  "htu": "https://server.example.com/oauth2/token",
  "iat": 1746806305,
  "jti": "4b2340d2-a91f-40a5-baa9-dd4e5deac867"
}

次のコードは、DPoP Proof JWT を生成する方法の例を示しています。

  • Java

RSAKey rsaKey = ...
JWKSource<SecurityContext> jwkSource = (jwkSelector, securityContext) -> jwkSelector
		.select(new JWKSet(rsaKey));
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource);

JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
		.type("dpop+jwt")
		.jwk(rsaKey.toPublicJWK().toJSONObject())
		.build();
JwtClaimsSet claims = JwtClaimsSet.builder()
		.issuedAt(Instant.now())
		.claim("htm", "POST")
		.claim("htu", "https://server.example.com/oauth2/token")
		.id(UUID.randomUUID().toString())
		.build();

Jwt dPoPProof = jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));

認可サーバーが DPoP 証明を正常に検証すると、DPoP 証明からの公開鍵は発行されたアクセストークンにバインドされます (送信者によって制約されます)。

次のアクセストークンレスポンスでは、アクセストークンが DPoP 証明公開鍵にバインドされていることをクライアントに通知するために、token_type パラメーターが DPoP として表示されています。

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
 "access_token": "Kz~8mXK1EalYznwH-LC-1fBAo.4Ljp~zsPE_NeO.gxU",
 "token_type": "DPoP",
 "expires_in": 2677
}

公開鍵の確認

リソースサーバーは、アクセストークンが DPoP にバインドされているかどうかを識別し、DPoP 証明の公開鍵との紐付けを検証できなければなりません。紐付けは、リソースサーバーがアクセスできる方法で、公開鍵とアクセストークンを関連付けることによって実現されます。たとえば、公開鍵ハッシュをアクセストークンに直接埋め込む(JWT)、またはトークンイントロスペクションを利用するなどです。

アクセストークンが JWT として表現される場合、公開鍵 ハッシュは確認方法 (cnf) クレームの jkt クレームに含まれます。

次の例は、DPoP 証明公開鍵の JWK SHA-256 サムプリントである jkt クレームを含む cnf クレームを含む JWT アクセストークンのクレームを示しています。

{
  "sub":"[email protected] (英語)  ",
  "iss":"https://server.example.com",
  "nbf":1562262611,
  "exp":1562266216,
  "cnf":
  {
    "jkt":"CQMknzRoZ5YUi7vS58jck1q8TmZT8wiIiXrCN1Ny4VU"
  }
}

DPoP 保護リソースリクエスト

DPoP で保護されたリソースへのリクエストには、DPoP 証明と DPoP にバインドされたアクセストークンの両方を含める必要があります。DPoP 証明には、アクセストークンの有効なハッシュを含む ath クレームを含める必要があります。リソースサーバーは受信したアクセストークンのハッシュを計算し、それが DPoP 証明内の ath クレームと一致することを検証します。

DPoP バインドアクセストークンは、認証スキーム DPoP を使用した Authorization リクエストヘッダーを使用して送信されます。

次の HTTP リクエストは、Authorization ヘッダーに DPoP バインドアクセストークンが含まれ、DPoP ヘッダーに DPoP 証明が含まれる保護されたリソースリクエストを示しています。

GET /resource HTTP/1.1
Host: resource.example.com
Authorization: DPoP Kz~8mXK1EalYznwH-LC-1fBAo.4Ljp~zsPE_NeO.gxU
DPoP: eyJraWQiOiJyc2EtandrLWtpZCIsInR5cCI6ImRwb3Arand0IiwiYWxnIjoiUlMyNTYiLCJqd2siOnsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJraWQiOiJyc2EtandrLWtpZCIsIm4iOiIzRmxxSnI1VFJza0lRSWdkRTNEZDdEOWxib1dkY1RVVDhhLWZKUjdNQXZRbTdYWE5vWWttM3Y3TVFMMU5ZdER2TDJsOENBbmMwV2RTVElOVTZJUnZjNUtxbzJRNGNzTlg5U0hPbUVmem9ST2pRcWFoRWN2ZTFqQlhsdW9DWGRZdVlweDRfMXRmUmdHNmlpNFVoeGg2aUk4cU5NSlFYLWZMZnFoYmZZZnhCUVZSUHl3QmtBYklQNHgxRUFzYkM2RlNObWtoQ3hpTU5xRWd4YUlwWThDMmtKZEpfWklWLVdXNG5vRGR6cEtxSGN3bUI4RnNydW1sVllfRE5WdlVTRElpcGlxOVBiUDRIOTlUWE4xbzc0Nm9SYU5hMDdycTFob0NnTVNTeS04NVNhZ0NveGxteUUtRC1vZjlTc01ZOE9sOXQwcmR6cG9iQnVoeUpfbzVkZnZqS3cifX0.eyJodG0iOiJHRVQiLCJodHUiOiJodHRwczovL3Jlc291cmNlLmV4YW1wbGUuY29tL3Jlc291cmNlIiwiYXRoIjoiZlVIeU8ycjJaM0RaNTNFc05yV0JiMHhXWG9hTnk1OUlpS0NBcWtzbVFFbyIsImlhdCI6MTc0NjgwNzEzOCwianRpIjoiM2MyZWU5YmItMDNhYy00MGNmLWI4MTItMDBiZmJhMzQxY2VlIn0.oS6NwjURR6wZemh1ZBNiBjycGeXwnkguLtgiKdCjQSEhFQpEJm04bBa0tdfZgWT17Z2mBgddnNQSkROzUGfssg8rBBldZXOAiduF-whtEGZA-pXXWJilXrwH3Glb6hIOMZOVmIH8fmYCDmqn-sE_DmDIsv57Il2-jdZbgeDcrxADO-6E5gsuNf1jvy7qqHq7INrKX6jRuydti_Re35lecvaAWfTyD7s7tQ_-3x_xLxxPwf_eA6z8OWbc58O2PYoUeO2JKLiOIg6UVZOZzxLEWV42WIKjha_kkoykvsf98W2y8pWOEr65u0VPsn5esw2X3I1eFL_A-XkxstZHRaGXJg

以下は、DPoP Proof JWT ヘッダーと ath クレームを含むクレームの表現を示しています。

{
  "typ": "dpop+jwt",
  "alg": "RS256",
  "jwk": {
    "kty": "RSA",
    "e": "AQAB",
    "n": "3FlqJr5TRskIQIgdE3Dd7D9lboWdcTUT8a-fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRvc5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4_1tfRgG6ii4Uhxh6iI8qNMJQX-fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2kJdJ_ZIV-WW4noDdzpKqHcwmB8FsrumlVY_DNVvUSDIipiq9PbP4H99TXN1o746oRaNa07rq1hoCgMSSy-85SagCoxlmyE-D-of9SsMY8Ol9t0rdzpobBuhyJ_o5dfvjKw"
  }
}
{
  "htm": "GET",
  "htu": "https://resource.example.com/resource",
  "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo",
  "iat": 1746807138,
  "jti": "3c2ee9bb-03ac-40cf-b812-00bfba341cee"
}

次のコードは、DPoP Proof JWT を生成する方法の例を示しています。

  • Java

RSAKey rsaKey = ...
JWKSource<SecurityContext> jwkSource = (jwkSelector, securityContext) -> jwkSelector
		.select(new JWKSet(rsaKey));
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource);

String accessToken = ...

JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
		.type("dpop+jwt")
		.jwk(rsaKey.toPublicJWK().toJSONObject())
		.build();
JwtClaimsSet claims = JwtClaimsSet.builder()
		.issuedAt(Instant.now())
		.claim("htm", "GET")
		.claim("htu", "https://resource.example.com/resource")
		.claim("ath", sha256(accessToken))
		.id(UUID.randomUUID().toString())
		.build();

Jwt dPoPProof = jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));