👻

Okta の DPoP をためしてみた

2024/01/03に公開

Digital Identity 技術勉強会 #iddance Advent Calendar 2023 シリーズ 2 の記事です。

https://qiita.com/advent-calendar/2023/iddance

はじめに

2023 年に Okta が DPoP[1] のサポートを発表していました。今回はこれを試してみて手順などを備忘録として残します。

https://www.okta.com/blog/2023/06/a-leap-forward-in-token-security-okta-adds-support-for-dpop/

なお、本投稿はあくまで備忘録であり、正式な手順については以下の Okta のドキュメントから確認するようにお願いします。

https://developer.okta.com/docs/guides/dpop/main/

動作確認

Okta 設定

まずは Okta に OpenID Connect (OIDC) の Native Application を作成します。

このとき、Require Demonstrating Proof of Possession (DPoP) header in token requestsという設定を有効にします。

また、動作確認のために redirect URIsGrant type の設定を確認しておきます。

ちなみに、Authorization Server Metadata (https://<Okta domain>/oauth2/default/.well-known/openid-configuration) から "dpop_signing_alg_values_supported":["RS256","RS384","RS512","ES256","ES384","ES512"] を確認することができました。

JSON Web Key (JWK) および JSON Web Token (JWT) の作成

リクエストに含める DPoP proof JWT を生成するための準備を行います。

まずは署名および検証に使用する JWK を用意します。今回はあくまで動作確認の用途なので JSON Web Key generator[2] というサービスを使用して以下のように作成します。

次に DPoP proof JWT を作成する準備を行います。こちらもあくまで動作確認の用途なので JWT.IO[3] というサービスを使用します。

DPoP proof JWT のシンタックスの詳細については、以下の通り RFC 9449[1:1] からも確認することができます。

4.2. DPoP Proof JWT Syntax
A DPoP proof is a JWT [RFC7519] that is signed (using JSON Web Signature (JWS) [RFC7515]) with a private key chosen by the client (see below). The JOSE Header of a DPoP JWT MUST contain at least the following parameters:
(引用元: https://datatracker.ietf.org/doc/html/rfc9449#section-4.2)

先ほど作成した JWK の Public Key を Header に指定します。

また、X.509 PEM Format の Private Key および Public Key を設定して Signature Verified というメッセージが表示されることを確認します。

リクエスト

実際にトークンを取得してみます。

まずは Token Endpoint にリクエストするために Authorization Code を取得しておきます。例えば、以下のような形式の URL からフローを開始して Authorization Code を取得することができます。なお、PKCE に関するパラメーターについは RFC 7636[4] にサンプルとして記載されている値を使用しています。実際には code_verifier として推測不可能な値を使用するようにしてください。

https://<Okta domain>/oauth2/default/v1/authorize?response_type=code&client_id=<Client ID>&redirect_uri=https://localhost:8443/cb&scope=openid offline_access&state=abc&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=s256

次にここまで用意したパラメーターなどを使用して Token Endpoint に対してリクエストを行います。
なお、DPoP proof JWT の iat クレームが古くなっている場合、{"error":"invalid_dpop_proof","error_description":"The DPoP proof JWT is issued more than five minutes in the past."} というエラーが返されるため、適宜現在の Unix time を確認して DPoP proof JWT を更新します。

$ date +%s
1704201397

ここで、以下のように Authorization server requires nonce in DPoP proof というエラーが返され、dpop-nonce というレスポンスヘッダーに nonce が指定されていることが確認できます。これは、RFC 9449 - 8. Authorization Server-Provided Nonce[1:2] の動作っぽいです。

$ curl -i --request POST \
       --url 'https://<Okta domain>/oauth2/default/v1/token' \
       --header 'Accept: application/json' \
       --header 'DPoP: eyJ0eXAiOi.....J3_HkZhijxzWGqL9fPjGRng' \
       --header 'Content-Type: application/x-www-form-urlencoded' \
       --data 'grant_type=authorization_code' \
       --data 'redirect_uri=https://localhost:8443/cb' \
       --data 'code=<Authorization Code>' \
       --data 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' \
       --data 'client_id=<Client ID>'
HTTP/1.1 400 Bad Request
Date: Tue, 02 Jan 2024 14:28:36 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Server: nginx
...
dpop-nonce: yZrt0Q0sVAWm6DwghmxVQglPhxNBlt6A
x-content-type-options: nosniff
Strict-Transport-Security: max-age=315360000; includeSubDomains

{"error":"use_dpop_nonce","error_description":"Authorization server requires nonce in DPoP proof."}

確認した dpop-nonce を使用して再度 DPoP proof JWT を生成します。

再度 Token Endpoint に対してリクエストを行うことで各種トークンが取得できます。

$ curl -i --request POST \
     --url 'https://<Okta domain>/oauth2/default/v1/token' \
     --header 'Accept: application/json' \
     --header 'DPoP: eyJ0eXA.....ixVrkQkHHqQA' \
     --header 'Content-Type: application/x-www-form-urlencoded' \
     --data 'grant_type=authorization_code' \
     --data 'redirect_uri=https://localhost:8443/cb' \
     --data 'code=<Authorization Code>' \
     --data 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' \
     --data 'client_id=<Client ID>'
HTTP/1.1 200 OK
Date: Tue, 02 Jan 2024 14:32:25 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Server: nginx
...
dpop-nonce: yZrt0Q0sVAWm6DwghmxVQglPhxNBlt6A
x-content-type-options: nosniff
Strict-Transport-Security: max-age=315360000; includeSubDomains
X-Robots-Tag: noindex,nofollow

{"token_type":"DPoP","expires_in":3600,"access_token":"eyJraWQi.....QGvYLo1kbg","scope":"offline_access openid","refresh_token":"pGQ.....YBw","id_token":"eyJraWQiOiJ4....pujLlvsN8zm5KZxjblZA"}

発行された Access Token をデコードしたところ cnf クレームおよび jkt クレームが存在しており、値が Zj5f3MI-u0bUAWfSq857CcwIJFp0j9h7PQ_zcg0fOig であることが確認できました。jkt クレームについては RFC 9449 - 6.1. JWK Thumbprint Confirmation Method[1:3] などに記載されています。

また、jkt クレームが事前に生成した Public Key の SHA-256 thumbprint と一致し、Access Token と Public Key の紐付けが確認できました。

$ cat rsa.jwk
{
    "kty": "RSA",
    "e": "AQAB",
    "use": "sig",
    "kid": "A4RhBfBByv6bgVlURGJqozj5gPDhl-USWH-FB1bXBCA",
    "alg": "RS256",
    "n": "iJfCBYV2RooFd1lgg4en1OD3FFc3l6_yTXkCDC7eB3t1SG-qM1Ne1JQScBHZQECB6yhRVrjj_vcTtKKGqAlsUlGn8UhOlXEIrfIiUQqaAkXXSpIAU_QTqcD9qFAC_pzeBeFswP8-7RQETBsUaxII6oFXS-D1qVEoYuFKaOD7m-VEABQGrEsIet1Y907oOdlXM-6BwgzaVXdQrWtwXpPSJ40swTPYwGxjnb813MeA3liV8WA5MaHtr4Fqsj3cBmIMmPfkj1bggbK73xWTJvx_Yrxa4B9SnfWBgNCt6hUqgBYQ2A3zDGyEUSBbNzrJWCVNR711dqeDLthg2cuPBlgPsQ"
}

$ jose jwk thp -i rsa.jwk -a S256
Zj5f3MI-u0bUAWfSq857CcwIJFp0j9h7PQ_zcg0fOig

Resource Server によるトークンおよび DPoP HTTP Header の検証手順については以下とおり Okta のドキュメントから確認できます。
Resource Server は下記のすべての検証手順をおこなったうえでリソースアクセスを許可します。

The following is a high-level overview of the validation steps that the resource server must perform.

Note: The resource server must not grant access to the resource unless all checks are successful.

  1. Read the value in the DPoP header and decode the DPoP JWT.
  2. Get the jwk (public key) from the header portion of the DPoP JWT.
  3. Verify the signature of the DPoP JWT using the public key and algorithm in the JWT header.
  4. Verify that the htu and htm claims are in the DPoP JWT payload and match with the current API request HTTP method and URL.
  5. Calculate the jkt (SHA-256 thumbprint of the public key).
  6. Extract the DPoP-bound access token from the Authorization header, verify it with Okta, and extract the claims. You can also use the /introspect endpoint to extract the access token claims.
  7. Validate the token binding by comparing jkt from the access token with the calculated jkt from the DPoP header.

(引用元: https://developer.okta.com/docs/guides/dpop/main/#validate-token-and-dpop-header)

Refresh Token を使用する場合についても試してみます。
RFC 9449 に記載されているとおり、Authorization Server が DPoP をサポートしており Public Client に対して Refresh Token を発行した場合、Refresh Token と公開鍵の紐付けを確認する必要があります。

When an authorization server supporting DPoP issues a refresh token to a public client that presents a valid DPoP proof at the token endpoint, the refresh token MUST be bound to the respective public key.
(引用元: https://datatracker.ietf.org/doc/html/rfc9449#section-5)

まずは DPoP HTTP Header を指定することで Refresh Token が使用できることが確認します。

$ curl --request POST \
     --url 'https://<Okta domain>/oauth2/default/v1/token' \
     --header 'Accept: application/json' \
     --header 'DPoP: eyJ0e6fq.....FlJcxQ' \
     --header 'Content-Type: application/x-www-form-urlencoded' \
     --data 'grant_type=refresh_token' \
     --data 'redirect_uri=https://localhost:8443/cb' \
     --data 'client_id=<Client ID>' \
     --data 'scope=offline_access openid' \
     --data 'refresh_token=pGQ.....YBw'
{"token_type":"DPoP","expires_in":3600,"access_token":"eyJraWQiOiJ4ZG.....F0XV9NP50X-hB-Zeg","scope":"offline_access openid","refresh_token":"pGQ.....YBw","id_token":"eyJraWQiOi.....vadmg5dgr3a"}

次に、再度 JSON Web Key generator[2:1] でキーペアを作成し、そのキーペアを使用して生成した DPoP proof JWT を DPoP HTTP Header に指定してみます。

$ curl --request POST \
       --url 'https://<Okta domain>/oauth2/default/v1/token' \
       --header 'Accept: application/json' \
       --header 'DPoP: eyJ0eX.....AiORIUEuOz6quBApKRfSPA' \
       --header 'Content-Type: application/x-www-form-urlencoded' \
       --data 'grant_type=refresh_token' \
       --data 'redirect_uri=https://localhost:8443/cb' \
       --data 'client_id=<Client ID>' \
       --data 'scope=offline_access openid' \
       --data 'refresh_token=pGQ.....YBw''
{"error":"invalid_dpop_proof","error_description":"The DPoP proof JWT signature is invalid."}

上記のとおり、署名の検証に失敗してトークンが更新されないことが確認できました。

おわりに

本投稿では Okta の DPoP 実装を試してみました。
以下の通り DPoP は Public Client で sender-constrained token を扱う場合に使用できる方法として OAuth 2.0 Security Best Current Practice[5] などでも紹介されています。
一方で、Authorization Server、Client、Resource Server といった各コンポーネントでの対応が必要であり、これらのいずれかの対応状況が採用を難しくする要因の一つという印象がありました。このため、このように DPoP をサポートする Authorization Server が増えていくことで DPoP を採用する Client も増えていくキッカケにもなるのではないかと思っています。

OAuth 2.0 Demonstrating Proof of Possession (DPoP) ([RFC9449]): DPoP outlines an application-level sender-constraining for access and refresh tokens that can be used in cases where neither mTLS nor OAuth Token Binding (see below) are available. It uses proof-of-possession based on a public/private key pair and application-level signing. DPoP can be used with public clients and, in case of confidential clients, can be combined with any client authentication method.

(引用元: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.10.1)

それはそうと、Public Client でどのようにして Private Key を安全に保存するのかという点についてはどのような整理になっているのでしょうか。例えば RFC 9449[1:4] には以下のような記載がありますが、具体的にはどういった保存領域が使用されるのか様々な事例が出てくることを個人的に期待をしていたりします。

If the private key is non-extractable (as is possible with [W3C.WebCryptoAPI]), DPoP renders exfiltrated tokens alone unusable.
(引用元: https://datatracker.ietf.org/doc/html/rfc9449#section-2)

If the private key used for DPoP is stored in such a way that it cannot be exported, e.g., in a hardware or software security module, the adversary cannot exfiltrate the key and use it to create arbitrary DPoP proofs.
(引用元: https://datatracker.ietf.org/doc/html/rfc9449#name-untrusted-code-in-the-clien)

ではまた!

脚注
  1. OAuth 2.0 Demonstrating Proof of Possession (DPoP) https://datatracker.ietf.org/doc/html/rfc9449 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. mkjwk simple JSON Web Key generator https://mkjwk.org/ ↩︎ ↩︎

  3. JWT.IO generator https://jwt.io/ ↩︎

  4. Proof Key for Code Exchange by OAuth Public Clients https://datatracker.ietf.org/doc/html/rfc7636#appendix-B ↩︎

  5. OAuth 2.0 Security Best Current Practice https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics ↩︎

GitHubで編集を提案

Discussion