Skip to content

Add RFC 9449 DPoP ath claim to protected-resource proofs#767

Open
jeswr wants to merge 1 commit into
solid-contrib:mainfrom
jeswr:fix/dpop-ath
Open

Add RFC 9449 DPoP ath claim to protected-resource proofs#767
jeswr wants to merge 1 commit into
solid-contrib:mainfrom
jeswr:fix/dpop-ath

Conversation

@jeswr

@jeswr jeswr commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Problem

When a DPoP proof accompanies an access token in protected-resource access, RFC 9449 §4.2/§7.1 requires the proof to carry an ath claim — the base64url-encoded SHA-256 of the access token — so the proof is cryptographically bound to that specific token, and the resource server MUST verify it. (The same requirement has been in draft-ietf-oauth-dpop since draft-03/04, the version Solid-OIDC references.)

Client.generateDpopToken currently sets only htm/htu/jti/iat, so the proofs it sends on resource requests have no ath. As a result the harness cannot authenticate against a resource server that enforces ath — the server (correctly) rejects the proof. I hit this validating a Solid server whose resource-server token+DPoP validation is built on panva's oauth4webapi, which enforces ath unconditionally for DPoP: the harness authenticated, discovered the pod, then got 401 on HEAD /<pod>/ with "JWT ath (access token hash) claim missing", aborting in PREPARE SERVER before any scenario ran.

Change

  • Thread the access token into generateDpopToken and set ath = base64url(SHA-256(token)) when a token is present.
  • The public signRequest (resource requests) binds the current access token; the token-endpoint request signs with an explicit null, so its proof never carries ath — including during a token refresh, where a stale token is still held in the field.
  • Tests: assert the resource-request proof from both getAuthHeaders and signRequest carries ath = base64url(SHA-256(access token)).

No behaviour change for servers that don't enforce ath; this only adds the spec-required claim.

🤖 Generated with Claude Code

When a DPoP proof accompanies an access token to a protected resource, RFC 9449
§4.2/§7.1 REQUIRES the proof to carry an `ath` claim (base64url SHA-256 of the
access token) so the proof is cryptographically bound to that specific token, and
the resource server MUST verify it. `Client.generateDpopToken` only set
`htm`/`htu`/`jti`/`iat`, so the harness could not authenticate against any resource
server that enforces `ath` (e.g. one built on panva's oauth4webapi) — the server
correctly rejects a proof with no `ath`.

Thread the access token into `generateDpopToken` and set `ath` when present. The
public `signRequest` (resource requests) binds the current access token; the
token-endpoint request signs with an explicit `null` so its proof never carries
`ath` — including during a refresh, where a stale token is still held in the field.
Add tests asserting the resource-request proof from both `getAuthHeaders` and
`signRequest` carries `ath` = base64url(SHA-256(access token)).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the HTTP client’s DPoP proof generation to comply with RFC 9449 by binding protected-resource proofs to the access token via the required ath claim, and adds unit tests to validate the behavior.

Changes:

  • Add ath (base64url(SHA-256(access token))) to DPoP proofs when an access token is presented to a protected resource.
  • Ensure token-endpoint DPoP proofs do not include ath by explicitly binding null during token requests/refresh.
  • Add tests that decode the DPoP JWT payload and assert ath is present and correctly computed for resource requests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/main/java/org/solid/testharness/http/Client.java Thread access token binding into DPoP proof generation and add ath computation helper.
src/main/java/org/solid/testharness/http/HttpConstants.java Introduce a constant for the DPoP ath claim key.
src/test/java/org/solid/testharness/http/ClientTest.java Add tests to assert protected-resource DPoP proofs include the correct ath claim.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +381 to +384
// it via `ath`.
return signRequest(builder, accessToken);
}

Comment on lines +505 to +510
final Map<String, String> headers = client.getAuthHeaders("GET", TEST_URL);
final var claims = dpopProofClaims(headers.get(HttpConstants.HEADER_DPOP));
final var expected = Base64.getUrlEncoder().withoutPadding().encodeToString(
MessageDigest.getInstance("SHA-256").digest(accessToken.getBytes(StandardCharsets.US_ASCII)));
assertEquals(expected, claims.get("ath"));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants