Skip to content

Commit ba4cc80

Browse files
fixup
1 parent dac33a9 commit ba4cc80

File tree

3 files changed

+87
-4
lines changed

3 files changed

+87
-4
lines changed

access-token-management/src/AccessTokenManagement.OpenIdConnect/Internal/AuthorizationServerDPoPHandler.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ internal class AuthorizationServerDPoPHandler(
3737
IDPoPProofService dPoPProofService,
3838
IDPoPNonceStore dPoPNonceStore,
3939
IHttpContextAccessor httpContextAccessor,
40+
ClientCredentialsClientName clientName,
4041
ILoggerFactory loggerFactory) : DelegatingHandler
4142
{
4243
// We depend on the logger factory, rather than the logger itself, since
@@ -90,7 +91,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
9091
_logger.DPoPErrorDuringTokenRefreshWillRetryWithServerNonce(LogLevel.Debug, response.GetDPoPError());
9192
response.Dispose();
9293
await SetDPoPProofTokenForCodeExchangeAsync(request, dPoPNonce, codeExchangeJwk).ConfigureAwait(false);
93-
await RefreshClientAssertionAsync(request).ConfigureAwait(false);
94+
await RefreshClientAssertionAsync(request, ct).ConfigureAwait(false);
9495
return await base.SendAsync(request, ct).ConfigureAwait(false);
9596
}
9697
}
@@ -141,7 +142,7 @@ internal async Task SetDPoPProofTokenForCodeExchangeAsync(HttpRequestMessage req
141142
}
142143
}
143144

144-
private async Task RefreshClientAssertionAsync(HttpRequestMessage request)
145+
private async Task RefreshClientAssertionAsync(HttpRequestMessage request, CT ct)
145146
{
146147
if (request.Content == null)
147148
{
@@ -163,7 +164,7 @@ private async Task RefreshClientAssertionAsync(HttpRequestMessage request)
163164
return;
164165
}
165166

166-
var assertion = await assertionService.GetClientAssertionAsync().ConfigureAwait(false);
167+
var assertion = await assertionService.GetClientAssertionAsync(clientName, ct: ct).ConfigureAwait(false);
167168
if (assertion == null)
168169
{
169170
return;

access-token-management/src/AccessTokenManagement.OpenIdConnect/Internal/ConfigureOpenIdConnectOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public void Configure(string? name, OpenIdConnectOptions options)
6161
options.Events.OnTokenValidated = CreateCallback(options.Events.OnTokenValidated);
6262
options.Events.OnPushAuthorization = CreateCallback(options.Events.OnPushAuthorization);
6363

64-
options.BackchannelHttpHandler = new AuthorizationServerDPoPHandler(dPoPProofService, dPoPNonceStore, httpContextAccessor, loggerFactory)
64+
options.BackchannelHttpHandler = new AuthorizationServerDPoPHandler(dPoPProofService, dPoPNonceStore, httpContextAccessor, ClientName, loggerFactory)
6565
{
6666
InnerHandler = options.BackchannelHttpHandler ?? new HttpClientHandler()
6767
};

access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementWithDPoPTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,69 @@ public async Task dpop_nonce_retry_during_code_exchange_should_use_fresh_client_
303303
"Client assertion must be regenerated on DPoP nonce retry during code exchange");
304304
}
305305

306+
[Fact]
307+
public async Task dpop_nonce_retry_during_code_exchange_should_pass_client_name_to_assertion_service()
308+
{
309+
var mockHttp = new MockHttpMessageHandler(BackendDefinitionBehavior.Always);
310+
AppHost.IdentityServerHttpHandler = mockHttp;
311+
AppHost.ClientSecret = null;
312+
313+
// Register an assertion service that captures the clientName on each call
314+
var capturedClientNames = new List<ClientCredentialsClientName?>();
315+
AppHost.OnConfigureServices += services =>
316+
{
317+
services.AddSingleton<IClientAssertionService>(
318+
new ClientNameCapturingAssertionService(capturedClientNames));
319+
};
320+
321+
// First code-exchange request — DPoP nonce error
322+
var nonceResponse = new
323+
{
324+
error = "use_dpop_nonce",
325+
error_description = "Invalid 'nonce' value.",
326+
};
327+
var nonce = "server-code-nonce";
328+
mockHttp.Expect("/connect/token")
329+
.WithFormData("grant_type", "authorization_code")
330+
.Respond(HttpStatusCode.BadRequest, headers: new Dictionary<string, string>
331+
{
332+
{ OidcConstants.HttpHeaders.DPoPNonce, nonce }
333+
},
334+
"application/json", JsonSerializer.Serialize(nonceResponse));
335+
336+
// Second code-exchange request (nonce retry) — succeeds
337+
var tokenResponse = new
338+
{
339+
id_token = IdentityServerHost.CreateIdToken("1", "dpop"),
340+
access_token = "initial_access_token",
341+
token_type = "DPoP",
342+
expires_in = 3600,
343+
refresh_token = "initial_refresh_token",
344+
};
345+
mockHttp.Expect("/connect/token")
346+
.WithFormData("grant_type", "authorization_code")
347+
.Respond("application/json", JsonSerializer.Serialize(tokenResponse));
348+
349+
await InitializeAsync();
350+
await AppHost.LoginAsync("alice");
351+
352+
mockHttp.VerifyNoOutstandingExpectation();
353+
354+
// The assertion service should have been called at least twice:
355+
// once for the initial code exchange (via ConfigureOpenIdConnectOptions),
356+
// and once for the retry (via AuthorizationServerDPoPHandler.RefreshClientAssertionAsync).
357+
capturedClientNames.Count.ShouldBeGreaterThanOrEqualTo(2,
358+
"Expected at least 2 assertion calls (initial code exchange + nonce retry)");
359+
360+
// ALL calls should have received the scheme-derived client name, not null
361+
var expectedPrefix = OpenIdConnect.OpenIdConnectTokenManagementDefaults.ClientCredentialsClientNamePrefix;
362+
foreach (var name in capturedClientNames)
363+
{
364+
name.ShouldNotBeNull("clientName must not be null — the OIDC scheme name should be forwarded");
365+
name.Value.ToString().ShouldStartWith(expectedPrefix);
366+
}
367+
}
368+
306369
// A client assertion service that returns a new assertion value on each call.
307370
// Matches any client name (for integration tests where scheme-derived names vary).
308371
private class CountingClientAssertionService(Func<string> valueFactory) : IClientAssertionService
@@ -316,4 +379,23 @@ private class CountingClientAssertionService(Func<string> valueFactory) : IClien
316379
Value = valueFactory()
317380
});
318381
}
382+
383+
// A client assertion service that captures the clientName parameter on each call.
384+
private class ClientNameCapturingAssertionService(List<ClientCredentialsClientName?> capturedNames) : IClientAssertionService
385+
{
386+
private int _callCount;
387+
388+
public Task<ClientAssertion?> GetClientAssertionAsync(
389+
ClientCredentialsClientName? clientName = null,
390+
TokenRequestParameters? parameters = null,
391+
CancellationToken ct = default)
392+
{
393+
capturedNames.Add(clientName);
394+
return Task.FromResult<ClientAssertion?>(new ClientAssertion
395+
{
396+
Type = OidcConstants.ClientAssertionTypes.JwtBearer,
397+
Value = $"assertion_{Interlocked.Increment(ref _callCount)}"
398+
});
399+
}
400+
}
319401
}

0 commit comments

Comments
 (0)