diff --git a/src/Duende.AccessTokenManagement/AccessTokenHandler.cs b/src/Duende.AccessTokenManagement/AccessTokenHandler.cs index 7fb1bad..c544006 100644 --- a/src/Duende.AccessTokenManagement/AccessTokenHandler.cs +++ b/src/Duende.AccessTokenManagement/AccessTokenHandler.cs @@ -102,6 +102,18 @@ protected virtual async Task SetTokenAsync(HttpRequestMessage request, bool forc } } + // since AccessTokenType above in the token endpoint response (the token_type value) could be case insensitive, but + // when we send it as an Authoriization header in the API request it must be case sensitive, we + // are checking for that here and forcing it to the exact casing required. + if (scheme.Equals(AuthenticationSchemes.AuthorizationHeaderBearer, System.StringComparison.OrdinalIgnoreCase)) + { + scheme = AuthenticationSchemes.AuthorizationHeaderBearer; + } + else if (scheme.Equals(AuthenticationSchemes.AuthorizationHeaderDPoP, System.StringComparison.OrdinalIgnoreCase)) + { + scheme = AuthenticationSchemes.AuthorizationHeaderDPoP; + } + // checking for null AccessTokenType and falling back to "Bearer" since this might be coming // from an old cache/store prior to adding the AccessTokenType property. request.SetToken(scheme, token.AccessToken); diff --git a/src/Duende.AccessTokenManagement/DPoPExtensions.cs b/src/Duende.AccessTokenManagement/DPoPExtensions.cs index 63828ce..6090b2a 100644 --- a/src/Duende.AccessTokenManagement/DPoPExtensions.cs +++ b/src/Duende.AccessTokenManagement/DPoPExtensions.cs @@ -79,6 +79,5 @@ public static bool IsDPoPError(this HttpResponseMessage response) public static string GetDPoPUrl(this HttpRequestMessage request) { return request.RequestUri!.Scheme + "://" + request.RequestUri!.Authority + request.RequestUri!.LocalPath; - return request.RequestUri!.Scheme + "://" + request.RequestUri!.Authority + request.RequestUri!.AbsolutePath; } } \ No newline at end of file diff --git a/test/Tests/AccessTokenHandlerTests.cs b/test/Tests/AccessTokenHandlerTests.cs new file mode 100644 index 0000000..bcabae6 --- /dev/null +++ b/test/Tests/AccessTokenHandlerTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RichardSzalay.MockHttp; + +namespace Duende.AccessTokenManagement.Tests; + +public class AccessTokenHandlerTests +{ + TestDPoPProofService _testDPoPProofService = new TestDPoPProofService(); + TestHttpMessageHandler _testHttpMessageHandler = new TestHttpMessageHandler(); + + AccessTokenHandlerSubject _subject; + + public AccessTokenHandlerTests() + { + _subject = new AccessTokenHandlerSubject(_testDPoPProofService, new TestDPoPNonceStore(), new TestLoggerProvider().CreateLogger("AccessTokenHandlerSubject")); + _subject.InnerHandler = _testHttpMessageHandler; + } + + [Fact] + public async Task lower_case_token_type_should_be_converted_to_case_sensitive() + { + var client = new HttpClient(_subject); + + { + _subject.AccessToken.AccessTokenType = "bearer"; + + var response = await client.GetAsync("https://test/api"); + + _testHttpMessageHandler.Request!.Headers.Authorization!.Scheme.ShouldBe("Bearer"); + } + + { + _subject.AccessToken.AccessTokenType = "dpop"; + + var response = await client.GetAsync("https://test/api"); + + _testHttpMessageHandler.Request!.Headers.Authorization!.Scheme.ShouldBe("DPoP"); + } + } + + public class TestHttpMessageHandler : HttpMessageHandler + { + public HttpRequestMessage? Request { get; set; } + public HttpResponseMessage Response { get; set; } = new HttpResponseMessage(System.Net.HttpStatusCode.NoContent); + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Request = request; + return Task.FromResult(Response); + } + } + + public class AccessTokenHandlerSubject : AccessTokenHandler + { + public ClientCredentialsToken AccessToken { get; set; } = new ClientCredentialsToken + { + AccessToken = "at", + AccessTokenType = "bearer", + }; + + public AccessTokenHandlerSubject(IDPoPProofService dPoPProofService, IDPoPNonceStore dPoPNonceStore, ILogger logger) : base(dPoPProofService, dPoPNonceStore, logger) + { + } + + protected override Task GetAccessTokenAsync(bool forceRenewal, CancellationToken cancellationToken) + { + return Task.FromResult(AccessToken); + } + } +} \ No newline at end of file diff --git a/test/Tests/ClientTokenManagementApiTests.cs b/test/Tests/ClientTokenManagementApiTests.cs index 5b3588d..ceaf268 100644 --- a/test/Tests/ClientTokenManagementApiTests.cs +++ b/test/Tests/ClientTokenManagementApiTests.cs @@ -1,3 +1,5 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. using Duende.IdentityServer.Configuration; using IdentityModel; diff --git a/test/Tests/ClientTokenManagementTests.cs b/test/Tests/ClientTokenManagementTests.cs index b44c59f..1d6240d 100644 --- a/test/Tests/ClientTokenManagementTests.cs +++ b/test/Tests/ClientTokenManagementTests.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + using System.Net; using System.Text.Json; using IdentityModel; diff --git a/test/Tests/Framework/TestClientAssertionService.cs b/test/Tests/Framework/TestClientAssertionService.cs index b0d2087..c03a7d3 100644 --- a/test/Tests/Framework/TestClientAssertionService.cs +++ b/test/Tests/Framework/TestClientAssertionService.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + using IdentityModel.Client; namespace Duende.AccessTokenManagement.Tests; diff --git a/test/Tests/Framework/TestDPoPNonceStore.cs b/test/Tests/Framework/TestDPoPNonceStore.cs new file mode 100644 index 0000000..066ecec --- /dev/null +++ b/test/Tests/Framework/TestDPoPNonceStore.cs @@ -0,0 +1,18 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +namespace Duende.AccessTokenManagement.Tests; + +public class TestDPoPNonceStore : IDPoPNonceStore +{ + public Task GetNonceAsync(DPoPNonceContext context, CancellationToken cancellationToken = default) + { + return Task.FromResult(null); + } + + public Task StoreNonceAsync(DPoPNonceContext context, string nonce, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/Tests/UserTokenManagementTests.cs b/test/Tests/UserTokenManagementTests.cs index 53b443e..e6cf89f 100644 --- a/test/Tests/UserTokenManagementTests.cs +++ b/test/Tests/UserTokenManagementTests.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + using System.Net.Http.Json; using System.Text.Json; using Duende.AccessTokenManagement.OpenIdConnect; diff --git a/test/Tests/Usings.cs b/test/Tests/Usings.cs index bd8299f..42ff96b 100644 --- a/test/Tests/Usings.cs +++ b/test/Tests/Usings.cs @@ -1,2 +1,5 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + global using Xunit; global using Shouldly; \ No newline at end of file