Fix stale ClientAssertion on DPoP nonce retry and missing assertion in OIDC code exchange#329
Fix stale ClientAssertion on DPoP nonce retry and missing assertion in OIDC code exchange#329Erwinvandervalk wants to merge 3 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request fixes two critical issues with client assertions in DPoP-enabled OAuth/OIDC flows:
-
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. -
Missing assertions in OIDC code exchange: The OIDC authorization code exchange flow previously required manual
PostConfigureworkarounds to inject client assertions. This PR automatically injects them whenIClientAssertionServiceis registered.
Changes:
- Client assertion regeneration added to three DPoP nonce retry paths: client credentials, refresh token, and authorization code exchange
IClientAssertionServiceinjection added to OIDC code exchange flow viaConfigureOpenIdConnectOptions- Fixed async/await bugs in OIDC event callbacks (inner callbacks were not awaited)
- Added
ClientAssertionFactoryproperty toProofTokenMessageHandlerfor 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.
...management/src/AccessTokenManagement.OpenIdConnect/Internal/ConfigureOpenIdConnectOptions.cs
Outdated
Show resolved
Hide resolved
ed4a2c7 to
cb31f00
Compare
josephdecock
left a comment
There was a problem hiding this comment.
Strongly recommend that we get a sample client running end to end for access token management.
identity-model-oidc-client/samples/NetCoreConsoleClient/src/NetCoreConsoleClient/Program.cs
Outdated
Show resolved
Hide resolved
access-token-management/test/AccessTokenManagement.Tests/Framework/JwtClientAssertionService.cs
Outdated
Show resolved
Hide resolved
...y-model-oidc-client/src/IdentityModel.OidcClient.Extensions/DPoP/ProofTokenMessageHandler.cs
Show resolved
Hide resolved
56cdb90 to
74f0c5e
Compare
633c0b1 to
f9ac011
Compare
…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
f9ac011 to
c89c216
Compare
246bdde to
749c2a2
Compare
|
|
||
| options.Events.OnSigningOut = async e => { await e.HttpContext.RevokeRefreshTokenAsync(); }; | ||
| }) | ||
| .AddOpenIdConnect("oidc", options => |
There was a problem hiding this comment.
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.
No description provided.