Skip to content

Fix stale ClientAssertion on DPoP nonce retry and missing assertion in OIDC code exchange#329

Closed
Erwinvandervalk wants to merge 3 commits intomainfrom
fix/dpop-stale-client-assertion-1392
Closed

Fix stale ClientAssertion on DPoP nonce retry and missing assertion in OIDC code exchange#329
Erwinvandervalk wants to merge 3 commits intomainfrom
fix/dpop-stale-client-assertion-1392

Conversation

@Erwinvandervalk
Copy link
Contributor

@Erwinvandervalk Erwinvandervalk commented Feb 27, 2026

No description provided.

Copilot AI review requested due to automatic review settings February 27, 2026 13:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes two critical issues with client assertions in DPoP-enabled OAuth/OIDC flows:

  1. Stale client assertions on DPoP nonce retry: When authorization servers require unique jti (JWT ID) claims in client assertions, retrying a failed request with the same assertion causes rejection. This PR regenerates client assertions on DPoP nonce retries across all token request paths.

  2. Missing assertions in OIDC code exchange: The OIDC authorization code exchange flow previously required manual PostConfigure workarounds to inject client assertions. This PR automatically injects them when IClientAssertionService is registered.

Changes:

  • Client assertion regeneration added to three DPoP nonce retry paths: client credentials, refresh token, and authorization code exchange
  • IClientAssertionService injection added to OIDC code exchange flow via ConfigureOpenIdConnectOptions
  • Fixed async/await bugs in OIDC event callbacks (inner callbacks were not awaited)
  • Added ClientAssertionFactory property to ProofTokenMessageHandler for OidcClient Extensions library
  • 8 new tests verify the fixes work correctly and maintain backward compatibility

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ClientCredentialsTokenClient.cs Regenerates client assertions on DPoP nonce retry for client credentials flow
OpenIdConnectUserTokenEndpoint.cs Regenerates client assertions on DPoP nonce retry for refresh token flow
ConfigureOpenIdConnectOptions.cs Injects IClientAssertionService, fixes async bugs, adds assertion to code exchange
AuthorizationServerDPoPHandler.cs Accepts optional assertion service, rebuilds form body with fresh assertion on retry
ProofTokenMessageHandler.cs Adds ClientAssertionFactory property, implements form body rebuilding on retry
OidcClientExtensions.cs Wires up assertion factory when GetClientAssertionAsync is configured
DPoPTests.cs Unit tests for ProofTokenMessageHandler assertion factory behavior
UserTokenManagementWithDPoPTests.cs Integration tests for refresh and code exchange assertion regeneration
UserTokenManagementTests.cs Updated test to remove PostConfigure workaround, adds automatic injection test
ClientTokenManagementTests.cs Tests client credentials assertion regeneration on DPoP nonce retry

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

@Erwinvandervalk Erwinvandervalk marked this pull request as draft February 27, 2026 13:37
@Erwinvandervalk Erwinvandervalk force-pushed the fix/dpop-stale-client-assertion-1392 branch from ed4a2c7 to cb31f00 Compare March 3, 2026 07:16
@Erwinvandervalk Erwinvandervalk marked this pull request as ready for review March 3, 2026 10:24
Copy link
Member

@josephdecock josephdecock left a comment

Choose a reason for hiding this comment

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

Strongly recommend that we get a sample client running end to end for access token management.

@Erwinvandervalk Erwinvandervalk force-pushed the fix/dpop-stale-client-assertion-1392 branch from 56cdb90 to 74f0c5e Compare March 12, 2026 14:50
@Erwinvandervalk Erwinvandervalk force-pushed the fix/dpop-stale-client-assertion-1392 branch 3 times, most recently from 633c0b1 to f9ac011 Compare March 13, 2026 09:05
…n OIDC code exchange

When DPoP nonce negotiation triggers a token request retry, regenerate
the client assertion (private_key_jwt) so servers enforcing unique jti
claims accept the retry. Also inject IClientAssertionService into the
OIDC code-exchange flow so assertions are sent automatically without
requiring manual PostConfigure workarounds.

Five fix sites across two libraries:

1. ClientCredentialsTokenClient — re-invoke GetClientAssertionAsync
   before DPoP nonce retry (access-token-management)
2. OpenIdConnectUserTokenEndpoint — same pattern for refresh path
3. ConfigureOpenIdConnectOptions — inject IClientAssertionService, fix
   async bugs in CreateCallback, add assertion to OnAuthorizationCodeReceived
4. AuthorizationServerDPoPHandler — accept optional IClientAssertionService,
   rebuild form body with fresh assertion on nonce retry
5. ProofTokenMessageHandler — add optional ClientAssertionFactory callback,
   rebuild form body on nonce retry (identity-model-oidc-client)

OidcClientExtensions.ConfigureDPoP() wires GetClientAssertionAsync to the
token-endpoint handler's ClientAssertionFactory automatically.

Backward compatible: NoOpClientAssertionService returns null, all new
code paths null-check and skip. Static parameters.Assertion path is
intentionally left unchanged.

Closes #1392, closes #1393

dotnet format

Regenerate client assertions on DPoP nonce retries across all flows

Refactor client assertion regeneration to use a factory pattern that
produces fresh JWTs (with unique jti) on each DPoP nonce retry attempt,
preventing servers from rejecting replayed assertions.

identity-model:
- Add ProtocolRequestOptions.ClientAssertionFactory on HttpRequestMessage.Options
- Add ClientAssertionFactory property to ProtocolRequest, wired into Clone<T>()
- Invoke factory in RequestTokenAsync and PushAuthorizationAsync
- Add ClientAssertions sample project

identity-model-oidc-client:
- Rewrite ProofTokenMessageHandler to read factory from request.Options
- Remove handler-level ClientAssertionFactory property and WireClientAssertionFactory
- Wire ClientAssertionFactory in AuthorizeClient, OidcClient, ResponseProcessor
- Update NetCoreConsoleClient sample for DPoP + assertions
- Switch to ProjectReference for cross-project visibility

access-token-management:
- Refactor AuthorizationServerDPoPHandler to resolve IClientAssertionService
  from DI (HttpContext.RequestServices) instead of constructor injection
- Add integration test framework: JwtClientAssertionService, dpop-assertion
  client, JWT bearer auth, AppHost assertion wiring
- Add DPoPWithClientAssertionsTests with full IdentityServer integration
  tests for code exchange, refresh token, and client credentials flows

cleanup

Add null check for TokenEndpointRequest in code exchange callback

Revert unrelated razor-slices changes to match main

cleanup

don't update razor slices

fixed the test clients

Address PR review: fix audience to use issuer identifier and revert razor-slices

not working sample

reset
@Erwinvandervalk Erwinvandervalk force-pushed the fix/dpop-stale-client-assertion-1392 branch from f9ac011 to c89c216 Compare March 13, 2026 09:27
@Erwinvandervalk Erwinvandervalk self-assigned this Mar 13, 2026
@Erwinvandervalk Erwinvandervalk added area/foss/atm Issues related to Access Token Management area/foss/im Issues related to Identity Model area/foss/im-oidc-client Issues related to Identity Model OIDC Client labels Mar 13, 2026
@Erwinvandervalk Erwinvandervalk force-pushed the fix/dpop-stale-client-assertion-1392 branch from 246bdde to 749c2a2 Compare March 13, 2026 13:44

options.Events.OnSigningOut = async e => { await e.HttpContext.RevokeRefreshTokenAsync(); };
})
.AddOpenIdConnect("oidc", options =>
Copy link
Member

Choose a reason for hiding this comment

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

I get invalid_client when I run this on my machine. I thought we were updating demo to fix that, but it looks like that's been merged already.

@Erwinvandervalk
Copy link
Contributor Author

Closing in favor of #343 #344 #345

@Erwinvandervalk Erwinvandervalk deleted the fix/dpop-stale-client-assertion-1392 branch March 19, 2026 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/foss/atm Issues related to Access Token Management area/foss/im Issues related to Identity Model area/foss/im-oidc-client Issues related to Identity Model OIDC Client

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client Assertions are reused when negotiating DPoP Nonce with the Token-endpoint

3 participants