From 082334fcba07e1ba3a99a753292aa8636a8b3035 Mon Sep 17 00:00:00 2001 From: Rune Gulbrandsen Date: Mon, 13 Jan 2025 15:34:39 +0100 Subject: [PATCH 1/5] Update to .NET 9 Updated to support .NET 9 and removed .NET standard 2.0 and .NET 6 support. --- README.md | 5 ---- .../Helpers/OAuth2Provider.cs | 4 --- .../HttpClientAuthentication.csproj | 29 +++++++++---------- .../HttpClientAuthenticationExtensions.cs | 12 -------- .../HttpClientAuthentication.Test.csproj | 16 +++++----- 5 files changed, 22 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 3ada81f..5018686 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,6 @@ The package currently supports the following authentication methods: - OAuth2 - Client credentials -> Please note: The .NET Standard 2.0 and .NET 6 targeted packages references the LTS version 6 of the dependent -NuGet packages. The reason for this is that while it is possible to run .NET 7 NuGet packages on .NET 6, it is known that some -of them may introduce unexpected behavior. It is recommended (especially from the ASP.NET Core team) limit usage of .NET 7 -based NuGet packages on .NET 6 runtime. - ## USAGE Add the NuGet package `KISS.HttpClientAuthentication` to your project and whenever a diff --git a/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs b/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs index 63dc494..bad044f 100644 --- a/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs +++ b/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs @@ -117,11 +117,7 @@ private static FormUrlEncodedContent GetClientCredentialsContent(ClientCredentia private async Task ParseResponseAsync(OAuth2Configuration configuration, HttpResponseMessage result, CancellationToken cancellationToken) { -#if NET6_0_OR_GREATER string body = await result.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); -#else - string body = await result.Content.ReadAsStringAsync().ConfigureAwait(false); -#endif if (!result.IsSuccessStatusCode) { diff --git a/src/HttpClientAuthentication/HttpClientAuthentication.csproj b/src/HttpClientAuthentication/HttpClientAuthentication.csproj index c43842c..d68f88e 100644 --- a/src/HttpClientAuthentication/HttpClientAuthentication.csproj +++ b/src/HttpClientAuthentication/HttpClientAuthentication.csproj @@ -1,6 +1,6 @@  - netstandard2.0;net6.0;net8.0 + net8.0;net9.0 KISS.HttpClientAuthentication KISS.HttpClientAuthentication @@ -9,9 +9,9 @@ enable - 2.0.2 + 3.0.0 Rune Gulbrandsen - Copyright (c) 2024 Rune Gulbrandsen. All rights reserved. + Copyright (c) 2025 Rune Gulbrandsen. All rights reserved. Extension methods to apply authentication handling to HttpClient based on configuration from .NET configuration providers. @@ -23,20 +23,19 @@ true snupkg - - - - - - + + + + + + - - - - - - + + + + + diff --git a/src/HttpClientAuthentication/HttpClientAuthenticationExtensions.cs b/src/HttpClientAuthentication/HttpClientAuthenticationExtensions.cs index d4f3771..2fe4ef4 100644 --- a/src/HttpClientAuthentication/HttpClientAuthenticationExtensions.cs +++ b/src/HttpClientAuthentication/HttpClientAuthenticationExtensions.cs @@ -50,20 +50,8 @@ public static IHttpClientBuilder AddAuthenticatedHttpMessageHandler(this IHttpCl /// public static IHttpClientBuilder AddAuthenticatedHttpMessageHandler(this IHttpClientBuilder builder, string configSection) { -#if NET8_0_OR_GREATER ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(configSection); -#else - if (builder is null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (configSection is null) - { - throw new ArgumentNullException(nameof(configSection)); - } -#endif builder.Services.AddMemoryCache(); diff --git a/test/HttpClientAuthentication.Test/HttpClientAuthentication.Test.csproj b/test/HttpClientAuthentication.Test/HttpClientAuthentication.Test.csproj index ba829d0..4a8f680 100644 --- a/test/HttpClientAuthentication.Test/HttpClientAuthentication.Test.csproj +++ b/test/HttpClientAuthentication.Test/HttpClientAuthentication.Test.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 KISS.HttpClientAuthentication.Test KISS.HttpClientAuthentication.Test @@ -14,21 +14,21 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all From f8f499646f52690d5d256afb36e115d434742755 Mon Sep 17 00:00:00 2001 From: Rune Gulbrandsen Date: Mon, 13 Jan 2025 15:42:26 +0100 Subject: [PATCH 2/5] Replace AuthorizationEndpoint with TokenEndpoint Replaced AuthorizationEndpoint configuration property with TokenEndpoint property which is more correct name in OAuth2. The AuthorizationEndpoint configuration property is still around, but obsoleted and will be removed later. --- README.md | 4 +- .../Configuration/OAuth2Configuration.cs | 14 ++++++- .../Helpers/OAuth2Provider.cs | 38 +++++++++---------- ...tClientCredentialsAccessTokenAsyncTests.cs | 30 +++++++-------- .../ParseResponseAsyncTests.cs | 12 +++--- .../TryParseAndLogOAuth2ErrorTests.cs | 12 +++--- 6 files changed, 62 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 5018686..e5211f8 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,10 @@ Using OAuth2 client credentials, all settings except `DisableTokenCache` and `Sc "
": { "AuthenticationProvider": "OAuth2", "OAuth2": { - "AuthorizationEndpoint": "", "DisableTokenCache": false, "GrantType": "ClientCredentials", "Scope": "", + "TokenEndpoint": "", "ClientCredentials": { "ClientId": "", "ClientSecret": "" @@ -88,6 +88,8 @@ Using OAuth2 client credentials, all settings except `DisableTokenCache` and `Sc } ``` +> **NOTE**: The previous `AuthorizationEndpoint` is replaced by `TokenEndpoint`. It still exists, +but is obsoleted and will be removed in a later version. ### Examples diff --git a/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs b/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs index 6486ffb..b069fe7 100644 --- a/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs +++ b/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs @@ -14,7 +14,11 @@ public sealed class OAuth2Configuration /// Gets or sets the authorization endpoint used by some /// configuration. ///
- public Uri AuthorizationEndpoint { get; set; } = default!; + /// + /// Obsolete: Use instead. + /// + [Obsolete("Use TokenEndpoint instead.")] + public Uri AuthorizationEndpoint { get => TokenEndpoint; set => TokenEndpoint = value; } /// /// Gets or sets the authorization scheme to use if is @@ -47,5 +51,13 @@ public sealed class OAuth2Configuration /// Scopes must be separated with a space. /// public string? Scope { get; set; } + + /// + /// Gets or sets the token endpoint. + /// + /// + /// Replaces . + /// + public Uri TokenEndpoint { get; set; } = default!; } } diff --git a/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs b/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs index bad044f..c930474 100644 --- a/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs +++ b/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs @@ -46,23 +46,23 @@ internal sealed class OAuth2Provider(IHttpClientFactory clientFactory, ILogger 0) { double cacheExpiresIn = (int)token.ExpiresIn * 0.95; memoryCache.Set(cacheKey, token, TimeSpan.FromSeconds(cacheExpiresIn)); - logger.LogInformation("Token retrieved from {AuthorizationEndpoint} with client id {ClientId} and cached for {CacheExpiresIn} seconds.", - configuration.AuthorizationEndpoint, configuration.ClientCredentials!.ClientId, cacheExpiresIn); + logger.LogInformation("Token retrieved from {TokenEndpoint} with client id {ClientId} and cached for {CacheExpiresIn} seconds.", + configuration.TokenEndpoint, configuration.ClientCredentials!.ClientId, cacheExpiresIn); } else { - logger.LogInformation("Token retrieved from {AuthorizationEndpoint} with client id {ClientId}, but not cached since it is missing expires_in information.", - configuration.AuthorizationEndpoint, configuration.ClientCredentials!.ClientId); + logger.LogInformation("Token retrieved from {TokenEndpoint} with client id {ClientId}, but not cached since it is missing expires_in information.", + configuration.TokenEndpoint, configuration.ClientCredentials!.ClientId); } return token; @@ -122,10 +122,10 @@ private static FormUrlEncodedContent GetClientCredentialsContent(ClientCredentia if (!result.IsSuccessStatusCode) { if (result.StatusCode != HttpStatusCode.BadRequest || - !TryParseAndLogOAuth2Error(body, configuration.AuthorizationEndpoint, configuration.ClientCredentials!.ClientId)) + !TryParseAndLogOAuth2Error(body, configuration.TokenEndpoint, configuration.ClientCredentials!.ClientId)) { - logger.LogError("Could not authenticate against {AuthorizationEndpoint}, the returned status code was {StatusCode}. Response body: {Body}.", - configuration.AuthorizationEndpoint, result.StatusCode, body); + logger.LogError("Could not authenticate against {TokenEndpoint}, the returned status code was {StatusCode}. Response body: {Body}.", + configuration.TokenEndpoint, result.StatusCode, body); } return null; @@ -135,7 +135,7 @@ private static FormUrlEncodedContent GetClientCredentialsContent(ClientCredentia if (token?.AccessToken is null) { - logger.LogError("The result from {AuthorizationEndpoint} is not a valid OAuth2 result.", configuration.AuthorizationEndpoint); + logger.LogError("The result from {TokenEndpoint} is not a valid OAuth2 result.", configuration.TokenEndpoint); return null; } @@ -160,7 +160,7 @@ private async Task PostWithBasicAuthenticationAsync(OAuth2C { Content = requestContent, Method = HttpMethod.Post, - RequestUri = configuration.AuthorizationEndpoint, + RequestUri = configuration.TokenEndpoint, Headers = { Authorization = new AuthenticationHeaderValue("Basic", encodedAuthorization) @@ -170,7 +170,7 @@ private async Task PostWithBasicAuthenticationAsync(OAuth2C return await _client.SendAsync(request, cancellationToken).ConfigureAwait(false); } - private bool TryParseAndLogOAuth2Error(string errorContent, Uri authorizationEndpoint, string? clientId) + private bool TryParseAndLogOAuth2Error(string errorContent, Uri tokenEndpoint, string? clientId) { ErrorResponse? response = null; @@ -189,7 +189,7 @@ private bool TryParseAndLogOAuth2Error(string errorContent, Uri authorizationEnd StringBuilder logMessage = new($"Could not authenticate against "); - logMessage.Append(authorizationEndpoint); + logMessage.Append(tokenEndpoint); if (!string.IsNullOrWhiteSpace(clientId)) { diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs index a4428d5..72f2dd2 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs @@ -54,7 +54,7 @@ public async Task TestClientIdIsNullEmptyOrWhitespacesThrowsArgumentException(st OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = clientId!, @@ -83,7 +83,7 @@ public async Task TestClientSecretIsNullEmptyOrWhitespacesThrowsArgumentExceptio OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -120,7 +120,7 @@ public async Task TestCacheHitIsReturnedAsToken() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -138,7 +138,7 @@ public async Task TestCacheHitIsReturnedAsToken() Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogInformation("Token for {AuthorizationEndpoint} with client id {ClientId} found in cache, using this.", + loggerMock.VerifyExt(l => l.LogInformation("Token for {TokenEndpoint} with client id {ClientId} found in cache, using this.", "https://somehost/", "client_id"), Times.Once); } @@ -165,7 +165,7 @@ public async Task TestGetAndCacheAccessTokenResponse() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -191,10 +191,10 @@ public async Task TestGetAndCacheAccessTokenResponse() Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogDebug("Could not find existing token in cache, requesting token from endpoint {AuthorizationEndpoint} with client id {ClientId}.", + loggerMock.VerifyExt(l => l.LogDebug("Could not find existing token in cache, requesting token from endpoint {TokenEndpoint} with client id {ClientId}.", "https://somehost/", "client_id"), Times.Once); - loggerMock.VerifyExt(l => l.LogInformation("Token retrieved from {AuthorizationEndpoint} with client id {ClientId} and cached for {CacheExpiresIn} seconds.", + loggerMock.VerifyExt(l => l.LogInformation("Token retrieved from {TokenEndpoint} with client id {ClientId} and cached for {CacheExpiresIn} seconds.", "https://somehost/", "client_id", 3420), Times.Once); } @@ -221,7 +221,7 @@ public async Task TestNoCachingOfAccessTokenResponseWithMissingExpiresIn() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -238,7 +238,7 @@ public async Task TestNoCachingOfAccessTokenResponseWithMissingExpiresIn() Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogInformation("Token retrieved from {AuthorizationEndpoint} with client id {ClientId}, but not cached since it is missing expires_in information.", + loggerMock.VerifyExt(l => l.LogInformation("Token retrieved from {TokenEndpoint} with client id {ClientId}, but not cached since it is missing expires_in information.", "https://somehost/", "client_id"), Times.Once); } @@ -265,7 +265,7 @@ public async Task TestNoCachingOfAccessTokenResponseWhenCacheIsDiabled() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -283,7 +283,7 @@ public async Task TestNoCachingOfAccessTokenResponseWhenCacheIsDiabled() Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogInformation("Token retrieved from {AuthorizationEndpoint} with client id {ClientId}, but the token cache is disabled.", + loggerMock.VerifyExt(l => l.LogInformation("Token retrieved from {TokenEndpoint} with client id {ClientId}, but the token cache is disabled.", "https://somehost/", "client_id"), Times.Once); } @@ -308,7 +308,7 @@ public async Task TestUseFormBasedAuthentication() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -350,7 +350,7 @@ public async Task TestUseBasicAuthentication() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { UseBasicAuthorizationHeader = true, @@ -385,7 +385,7 @@ public async Task TestRequestContainsScopeWhenSpecified() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -424,7 +424,7 @@ public async Task TestRequestHasNoScopeWhenNullEmptyOrWhitespace(string? scope) OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs index 892780b..4ec47e7 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs @@ -41,7 +41,7 @@ public async Task TestSetsTokenTypeToBearerWhenNotSpecified() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -81,7 +81,7 @@ public async Task TestUsesConfiguredAuthorizationSchemeAsTokenType() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), AuthorizationScheme = "Authorization_Scheme", ClientCredentials = new() { @@ -110,7 +110,7 @@ public async Task TestFailedRequestReturnsNullAndLogsError() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -126,7 +126,7 @@ public async Task TestFailedRequestReturnsNullAndLogsError() Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogError("Could not authenticate against {AuthorizationEndpoint}, the returned status code was {StatusCode}. Response body: {Body}.", + loggerMock.VerifyExt(l => l.LogError("Could not authenticate against {TokenEndpoint}, the returned status code was {StatusCode}. Response body: {Body}.", "https://somehost/", HttpStatusCode.NotFound, "ERROR_BODY"), Times.Once); } @@ -146,7 +146,7 @@ public async Task TestInvalidResponseReturnsNullAndLogsError(string response) OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -162,7 +162,7 @@ public async Task TestInvalidResponseReturnsNullAndLogsError(string response) Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogError("The result from {AuthorizationEndpoint} is not a valid OAuth2 result.", "https://somehost/"), + loggerMock.VerifyExt(l => l.LogError("The result from {TokenEndpoint} is not a valid OAuth2 result.", "https://somehost/"), Times.Once); } } diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs index 63df3fe..9be9eba 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs @@ -37,7 +37,7 @@ public async Task TestErrorIsFullyParsedAndLogged() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -76,7 +76,7 @@ public async Task TestDescriptionIsSkippedWhenNotSpecified() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -115,7 +115,7 @@ public async Task TestUriIsSkippedWhenNotSpecified() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -154,7 +154,7 @@ public async Task TestDescriptionAndUriIsSkippedWhenNotSpecified() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -191,7 +191,7 @@ public async Task TestParsingIsSkippedOnInvalidErrorContent(string content) OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - AuthorizationEndpoint = new("https://somehost/"), + TokenEndpoint = new("https://somehost/"), ClientCredentials = new() { ClientId = "client_id", @@ -207,7 +207,7 @@ public async Task TestParsingIsSkippedOnInvalidErrorContent(string content) Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogError("Could not authenticate against {AuthorizationEndpoint}, the returned status code was {StatusCode}. Response body: {Body}.", + loggerMock.VerifyExt(l => l.LogError("Could not authenticate against {TokenEndpoint}, the returned status code was {StatusCode}. Response body: {Body}.", "https://somehost/", HttpStatusCode.BadRequest, content), Times.Once); } } From 3ee78f98edbb83c3c1df9f07e4d150f37ba7ae8a Mon Sep 17 00:00:00 2001 From: Rune Gulbrandsen Date: Mon, 13 Jan 2025 16:20:34 +0100 Subject: [PATCH 3/5] Update copyright year --- src/HttpClientAuthentication/AssemblyInfo.cs | 2 +- .../Configuration/ApiKeyConfiguration.cs | 2 +- .../Configuration/BasicConfiguration.cs | 2 +- .../Configuration/ClientCredentialsConfiguration.cs | 2 +- .../Configuration/HttpClientAuthenticationConfiguration.cs | 2 +- .../Configuration/OAuth2Configuration.cs | 2 +- .../Constants/AuthenticationProvider.cs | 2 +- src/HttpClientAuthentication/Constants/OAuth2GrantType.cs | 2 +- src/HttpClientAuthentication/Constants/OAuth2Keyword.cs | 2 +- .../Handlers/ApiKeyAuthenticationHandler.cs | 2 +- .../Handlers/BaseAuthenticationHandler.cs | 2 +- .../Handlers/BasicAuthenticationHandler.cs | 2 +- .../Handlers/NoAuthenticationHandler.cs | 2 +- .../Handlers/OAuth2AuthenticationHandler.cs | 2 +- src/HttpClientAuthentication/Helpers/AccessTokenResponse.cs | 2 +- src/HttpClientAuthentication/Helpers/ErrorResponse.cs | 2 +- src/HttpClientAuthentication/Helpers/IOAuth2Provider.cs | 2 +- src/HttpClientAuthentication/Helpers/OAuth2Provider.cs | 2 +- .../HttpClientAuthenticationExtensions.cs | 2 +- test/HttpClientAuthentication.Test/AssemblyInfo.cs | 2 +- .../Handlers/ApiKeyAuthenticationHandlerTests.cs | 2 +- .../Handlers/BasicAuthenticationHandlerTests.cs | 2 +- test/HttpClientAuthentication.Test/Handlers/HandlerTestBase.cs | 2 +- .../Handlers/OAuth2AuthenticationHandlerTests.cs | 2 +- .../GetClientCredentialsAccessTokenAsyncTests.cs | 2 +- .../Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs | 2 +- .../Helpers/OAuth2ProviderTests/TestBase.cs | 2 +- .../OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs | 2 +- .../HttpClientAuthenticationExtensionsTests.cs | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/HttpClientAuthentication/AssemblyInfo.cs b/src/HttpClientAuthentication/AssemblyInfo.cs index 9be9855..0141421 100644 --- a/src/HttpClientAuthentication/AssemblyInfo.cs +++ b/src/HttpClientAuthentication/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Runtime.CompilerServices; diff --git a/src/HttpClientAuthentication/Configuration/ApiKeyConfiguration.cs b/src/HttpClientAuthentication/Configuration/ApiKeyConfiguration.cs index 17133ac..3180c31 100644 --- a/src/HttpClientAuthentication/Configuration/ApiKeyConfiguration.cs +++ b/src/HttpClientAuthentication/Configuration/ApiKeyConfiguration.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. namespace KISS.HttpClientAuthentication.Configuration diff --git a/src/HttpClientAuthentication/Configuration/BasicConfiguration.cs b/src/HttpClientAuthentication/Configuration/BasicConfiguration.cs index df82c67..0bccaef 100644 --- a/src/HttpClientAuthentication/Configuration/BasicConfiguration.cs +++ b/src/HttpClientAuthentication/Configuration/BasicConfiguration.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. namespace KISS.HttpClientAuthentication.Configuration diff --git a/src/HttpClientAuthentication/Configuration/ClientCredentialsConfiguration.cs b/src/HttpClientAuthentication/Configuration/ClientCredentialsConfiguration.cs index d858720..2055148 100644 --- a/src/HttpClientAuthentication/Configuration/ClientCredentialsConfiguration.cs +++ b/src/HttpClientAuthentication/Configuration/ClientCredentialsConfiguration.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. namespace KISS.HttpClientAuthentication.Configuration diff --git a/src/HttpClientAuthentication/Configuration/HttpClientAuthenticationConfiguration.cs b/src/HttpClientAuthentication/Configuration/HttpClientAuthenticationConfiguration.cs index bf6d9d8..6983793 100644 --- a/src/HttpClientAuthentication/Configuration/HttpClientAuthenticationConfiguration.cs +++ b/src/HttpClientAuthentication/Configuration/HttpClientAuthenticationConfiguration.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using KISS.HttpClientAuthentication.Constants; diff --git a/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs b/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs index b069fe7..1111203 100644 --- a/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs +++ b/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using KISS.HttpClientAuthentication.Constants; diff --git a/src/HttpClientAuthentication/Constants/AuthenticationProvider.cs b/src/HttpClientAuthentication/Constants/AuthenticationProvider.cs index eec6f79..6ed74dc 100644 --- a/src/HttpClientAuthentication/Constants/AuthenticationProvider.cs +++ b/src/HttpClientAuthentication/Constants/AuthenticationProvider.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. namespace KISS.HttpClientAuthentication.Constants diff --git a/src/HttpClientAuthentication/Constants/OAuth2GrantType.cs b/src/HttpClientAuthentication/Constants/OAuth2GrantType.cs index e4e091c..ab5e8f9 100644 --- a/src/HttpClientAuthentication/Constants/OAuth2GrantType.cs +++ b/src/HttpClientAuthentication/Constants/OAuth2GrantType.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. namespace KISS.HttpClientAuthentication.Constants diff --git a/src/HttpClientAuthentication/Constants/OAuth2Keyword.cs b/src/HttpClientAuthentication/Constants/OAuth2Keyword.cs index 1f6120f..63e2218 100644 --- a/src/HttpClientAuthentication/Constants/OAuth2Keyword.cs +++ b/src/HttpClientAuthentication/Constants/OAuth2Keyword.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. namespace KISS.HttpClientAuthentication.Constants diff --git a/src/HttpClientAuthentication/Handlers/ApiKeyAuthenticationHandler.cs b/src/HttpClientAuthentication/Handlers/ApiKeyAuthenticationHandler.cs index fc9f93a..0b19585 100644 --- a/src/HttpClientAuthentication/Handlers/ApiKeyAuthenticationHandler.cs +++ b/src/HttpClientAuthentication/Handlers/ApiKeyAuthenticationHandler.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using KISS.HttpClientAuthentication.Configuration; diff --git a/src/HttpClientAuthentication/Handlers/BaseAuthenticationHandler.cs b/src/HttpClientAuthentication/Handlers/BaseAuthenticationHandler.cs index 41099f6..c3c2747 100644 --- a/src/HttpClientAuthentication/Handlers/BaseAuthenticationHandler.cs +++ b/src/HttpClientAuthentication/Handlers/BaseAuthenticationHandler.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. namespace KISS.HttpClientAuthentication.Handlers diff --git a/src/HttpClientAuthentication/Handlers/BasicAuthenticationHandler.cs b/src/HttpClientAuthentication/Handlers/BasicAuthenticationHandler.cs index c6cfe3a..deb22ef 100644 --- a/src/HttpClientAuthentication/Handlers/BasicAuthenticationHandler.cs +++ b/src/HttpClientAuthentication/Handlers/BasicAuthenticationHandler.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Net.Http.Headers; diff --git a/src/HttpClientAuthentication/Handlers/NoAuthenticationHandler.cs b/src/HttpClientAuthentication/Handlers/NoAuthenticationHandler.cs index dfe8eb7..e9f586a 100644 --- a/src/HttpClientAuthentication/Handlers/NoAuthenticationHandler.cs +++ b/src/HttpClientAuthentication/Handlers/NoAuthenticationHandler.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. namespace KISS.HttpClientAuthentication.Handlers diff --git a/src/HttpClientAuthentication/Handlers/OAuth2AuthenticationHandler.cs b/src/HttpClientAuthentication/Handlers/OAuth2AuthenticationHandler.cs index f918acf..1b9f624 100644 --- a/src/HttpClientAuthentication/Handlers/OAuth2AuthenticationHandler.cs +++ b/src/HttpClientAuthentication/Handlers/OAuth2AuthenticationHandler.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Net.Http.Headers; diff --git a/src/HttpClientAuthentication/Helpers/AccessTokenResponse.cs b/src/HttpClientAuthentication/Helpers/AccessTokenResponse.cs index fe7dba7..81bc364 100644 --- a/src/HttpClientAuthentication/Helpers/AccessTokenResponse.cs +++ b/src/HttpClientAuthentication/Helpers/AccessTokenResponse.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Text.Json.Serialization; diff --git a/src/HttpClientAuthentication/Helpers/ErrorResponse.cs b/src/HttpClientAuthentication/Helpers/ErrorResponse.cs index 9065c72..78dbc0e 100644 --- a/src/HttpClientAuthentication/Helpers/ErrorResponse.cs +++ b/src/HttpClientAuthentication/Helpers/ErrorResponse.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Text.Json.Serialization; diff --git a/src/HttpClientAuthentication/Helpers/IOAuth2Provider.cs b/src/HttpClientAuthentication/Helpers/IOAuth2Provider.cs index fa479c9..875ef5a 100644 --- a/src/HttpClientAuthentication/Helpers/IOAuth2Provider.cs +++ b/src/HttpClientAuthentication/Helpers/IOAuth2Provider.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using KISS.HttpClientAuthentication.Configuration; diff --git a/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs b/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs index c930474..862b1c4 100644 --- a/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs +++ b/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Net; diff --git a/src/HttpClientAuthentication/HttpClientAuthenticationExtensions.cs b/src/HttpClientAuthentication/HttpClientAuthenticationExtensions.cs index 2fe4ef4..b2e67db 100644 --- a/src/HttpClientAuthentication/HttpClientAuthenticationExtensions.cs +++ b/src/HttpClientAuthentication/HttpClientAuthenticationExtensions.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using KISS.HttpClientAuthentication.Configuration; diff --git a/test/HttpClientAuthentication.Test/AssemblyInfo.cs b/test/HttpClientAuthentication.Test/AssemblyInfo.cs index 402c6a0..a7b8d57 100644 --- a/test/HttpClientAuthentication.Test/AssemblyInfo.cs +++ b/test/HttpClientAuthentication.Test/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. [assembly: CLSCompliant(false)] diff --git a/test/HttpClientAuthentication.Test/Handlers/ApiKeyAuthenticationHandlerTests.cs b/test/HttpClientAuthentication.Test/Handlers/ApiKeyAuthenticationHandlerTests.cs index 1a835a2..6e4a3f7 100644 --- a/test/HttpClientAuthentication.Test/Handlers/ApiKeyAuthenticationHandlerTests.cs +++ b/test/HttpClientAuthentication.Test/Handlers/ApiKeyAuthenticationHandlerTests.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using FluentAssertions; diff --git a/test/HttpClientAuthentication.Test/Handlers/BasicAuthenticationHandlerTests.cs b/test/HttpClientAuthentication.Test/Handlers/BasicAuthenticationHandlerTests.cs index 53f1130..2bb4750 100644 --- a/test/HttpClientAuthentication.Test/Handlers/BasicAuthenticationHandlerTests.cs +++ b/test/HttpClientAuthentication.Test/Handlers/BasicAuthenticationHandlerTests.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Text; diff --git a/test/HttpClientAuthentication.Test/Handlers/HandlerTestBase.cs b/test/HttpClientAuthentication.Test/Handlers/HandlerTestBase.cs index 61fa5dd..8f94aa3 100644 --- a/test/HttpClientAuthentication.Test/Handlers/HandlerTestBase.cs +++ b/test/HttpClientAuthentication.Test/Handlers/HandlerTestBase.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Net; diff --git a/test/HttpClientAuthentication.Test/Handlers/OAuth2AuthenticationHandlerTests.cs b/test/HttpClientAuthentication.Test/Handlers/OAuth2AuthenticationHandlerTests.cs index 9b7a7ba..f309770 100644 --- a/test/HttpClientAuthentication.Test/Handlers/OAuth2AuthenticationHandlerTests.cs +++ b/test/HttpClientAuthentication.Test/Handlers/OAuth2AuthenticationHandlerTests.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using FluentAssertions; diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs index 72f2dd2..ffe5a5f 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Net; diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs index 4ec47e7..e1565df 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Net; diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TestBase.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TestBase.cs index 3304015..fcba625 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TestBase.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TestBase.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using KISS.HttpClientAuthentication.Helpers; diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs index 9be9eba..122cf02 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using System.Net; diff --git a/test/HttpClientAuthentication.Test/HttpClientAuthenticationExtensionsTests.cs b/test/HttpClientAuthentication.Test/HttpClientAuthenticationExtensionsTests.cs index 53967ec..5942a36 100644 --- a/test/HttpClientAuthentication.Test/HttpClientAuthenticationExtensionsTests.cs +++ b/test/HttpClientAuthentication.Test/HttpClientAuthenticationExtensionsTests.cs @@ -1,4 +1,4 @@ -// Copyright © 2024 Rune Gulbrandsen. +// Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. using FluentAssertions; From 997082551473482f1dd84245a768d29a18e07920 Mon Sep 17 00:00:00 2001 From: Rune Gulbrandsen Date: Tue, 14 Jan 2025 14:16:14 +0100 Subject: [PATCH 4/5] Add support for token endpoint parameters Added support for supplying custom parameters in body, headers and body for the token endpoint request. This is a breaking change since the configuration has changed. --- .gitignore | 3 + README.md | 18 +- .../Configuration/OAuth2Configuration.cs | 12 +- .../Configuration/OAuth2Endpoint.cs | 40 ++++ .../Helpers/OAuth2Provider.cs | 193 +++++++++++------ ...tClientCredentialsAccessTokenAsyncTests.cs | 199 +++++++++++++----- .../ParseResponseAsyncTests.cs | 8 +- .../TryParseAndLogOAuth2ErrorTests.cs | 10 +- 8 files changed, 339 insertions(+), 144 deletions(-) create mode 100644 src/HttpClientAuthentication/Configuration/OAuth2Endpoint.cs diff --git a/.gitignore b/.gitignore index d0357ba..c8ac887 100644 --- a/.gitignore +++ b/.gitignore @@ -344,5 +344,8 @@ healthchecksdb /test/coverage.opencover.xml /test/result.json +# POC related +/poc + # Custom exclusions *.AssemblyAttributes diff --git a/README.md b/README.md index e5211f8..417de8c 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,8 @@ Authentication using OAuth2. ##### Client credentials -Using OAuth2 client credentials, all settings except `DisableTokenCache` and `Scope` is required. +Using OAuth2 client credentials, all settings except `DisableTokenCache`, `Scope` and +`TokenEndpoint`'s `Additional*Parameters` is required. ``` "
": { @@ -79,7 +80,15 @@ Using OAuth2 client credentials, all settings except `DisableTokenCache` and `Sc "DisableTokenCache": false, "GrantType": "ClientCredentials", "Scope": "", - "TokenEndpoint": "", + "TokenEndpoint": { + "Url": "", + "AdditionalHeaderParameters": { + }, + "AdditionalBodyParameters": { + }, + "AdditionalQueryParameters": { + } + }, "ClientCredentials": { "ClientId": "", "ClientSecret": "" @@ -88,8 +97,9 @@ Using OAuth2 client credentials, all settings except `DisableTokenCache` and `Sc } ``` -> **NOTE**: The previous `AuthorizationEndpoint` is replaced by `TokenEndpoint`. It still exists, -but is obsoleted and will be removed in a later version. +The `Additional*Parameters` configuration is dynamic, any configuration in these will +be added to their respective parts of the request accordingly. Please note that the +`AdditionalQueryParameters` will be url encoded. ### Examples diff --git a/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs b/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs index 1111203..fb95b59 100644 --- a/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs +++ b/src/HttpClientAuthentication/Configuration/OAuth2Configuration.cs @@ -10,16 +10,6 @@ namespace KISS.HttpClientAuthentication.Configuration ///
public sealed class OAuth2Configuration { - /// - /// Gets or sets the authorization endpoint used by some - /// configuration. - /// - /// - /// Obsolete: Use instead. - /// - [Obsolete("Use TokenEndpoint instead.")] - public Uri AuthorizationEndpoint { get => TokenEndpoint; set => TokenEndpoint = value; } - /// /// Gets or sets the authorization scheme to use if is /// Authorization or the selected uses Authorization as default header. @@ -58,6 +48,6 @@ public sealed class OAuth2Configuration /// /// Replaces . /// - public Uri TokenEndpoint { get; set; } = default!; + public OAuth2Endpoint TokenEndpoint { get; set; } = default!; } } diff --git a/src/HttpClientAuthentication/Configuration/OAuth2Endpoint.cs b/src/HttpClientAuthentication/Configuration/OAuth2Endpoint.cs new file mode 100644 index 0000000..500d48f --- /dev/null +++ b/src/HttpClientAuthentication/Configuration/OAuth2Endpoint.cs @@ -0,0 +1,40 @@ +// Copyright © 2025 Rune Gulbrandsen. +// All rights reserved. Licensed under the MIT License; see LICENSE.txt. + +namespace KISS.HttpClientAuthentication.Configuration +{ + /// + /// Endpoint configuration for OAuth2 endpoints + /// + public sealed partial class OAuth2Endpoint + { + /// + /// Gets or sets the Url to the OAuth2 endpoint. + /// + public Uri Url { get; set; } = default!; + + /// + /// Gets a dictionary that can contain additional headers that will be + /// supplied when requesting . + /// + public Dictionary AdditionalHeaderParameters { get; } = []; + + + /// + /// Gets a collection of additional form body parameters that will be + /// supplied when requesting . + /// + public Dictionary AdditionalBodyParameters { get; } = []; + + /// + /// Gets a collection of additional query string parameters that will + /// be supplied when requesting . + /// + public Dictionary AdditionalQueryParameters { get; } = []; + + public override string ToString() + { + return Url.ToString(); + } + } +} diff --git a/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs b/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs index 862b1c4..978e9af 100644 --- a/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs +++ b/src/HttpClientAuthentication/Helpers/OAuth2Provider.cs @@ -1,8 +1,8 @@ // Copyright © 2025 Rune Gulbrandsen. // All rights reserved. Licensed under the MIT License; see LICENSE.txt. +using System.Globalization; using System.Net; -using System.Net.Http.Headers; using System.Text; using System.Text.Json; using KISS.HttpClientAuthentication.Configuration; @@ -23,48 +23,30 @@ internal sealed class OAuth2Provider(IHttpClientFactory clientFactory, ILogger public async ValueTask GetClientCredentialsAccessTokenAsync(OAuth2Configuration configuration, CancellationToken cancellationToken = default) { - if (configuration.GrantType is not OAuth2GrantType.ClientCredentials) - { - throw new ArgumentException($"{nameof(configuration.GrantType)} must be {OAuth2GrantType.ClientCredentials}.", nameof(configuration)); - } - - if (configuration.ClientCredentials is null) - { - throw new ArgumentException($"No valid {nameof(configuration.ClientCredentials)} found.", nameof(configuration)); - } - - if (string.IsNullOrWhiteSpace(configuration.ClientCredentials.ClientId)) - { - throw new ArgumentException($"{nameof(configuration.ClientCredentials)}.{nameof(configuration.ClientCredentials.ClientId)} must be specified.", - nameof(configuration)); - } - - if (string.IsNullOrWhiteSpace(configuration.ClientCredentials.ClientSecret)) - { - throw new ArgumentException($"{nameof(configuration.ClientCredentials)}.{nameof(configuration.ClientCredentials.ClientSecret)} must be specified.", - nameof(configuration)); - } - + ValidateClientCredentialParameters(configuration); string cacheKey = $"{configuration.GrantType}#{configuration.TokenEndpoint}#{configuration.ClientCredentials!.ClientId}"; - if (memoryCache.TryGetValue(cacheKey, out AccessTokenResponse? token)) + AccessTokenResponse? token; + + if (!configuration.DisableTokenCache) { - logger.LogInformation("Token for {TokenEndpoint} with client id {ClientId} found in cache, using this.", - configuration.TokenEndpoint, configuration.ClientCredentials.ClientId); - return token; - } + if (memoryCache.TryGetValue(cacheKey, out token)) + { + logger.LogDebug("Token for {TokenEndpoint} with client id {ClientId} found in cache, using this.", + configuration.TokenEndpoint, configuration.ClientCredentials.ClientId); + return token; + } - logger.LogDebug("Could not find existing token in cache, requesting token from endpoint {TokenEndpoint} with client id {ClientId}.", + logger.LogDebug("Could not find existing token in cache, requesting token from endpoint {TokenEndpoint} with client id {ClientId}.", configuration.TokenEndpoint, configuration.ClientCredentials.ClientId); + } - using FormUrlEncodedContent requestContent = GetClientCredentialsContent(configuration.ClientCredentials!, configuration.Scope); + using HttpRequestMessage request = GetTokenRequest(configuration); - using HttpResponseMessage result = configuration.ClientCredentials!.UseBasicAuthorizationHeader - ? await PostWithBasicAuthenticationAsync(configuration, requestContent, cancellationToken).ConfigureAwait(false) - : await _client.PostAsync(configuration.TokenEndpoint, requestContent, cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await _client.SendAsync(request, cancellationToken); - token = await ParseResponseAsync(configuration, result, cancellationToken).ConfigureAwait(false); + token = await ParseResponseAsync(configuration, response, cancellationToken).ConfigureAwait(false); if (token is null) { @@ -73,7 +55,7 @@ internal sealed class OAuth2Provider(IHttpClientFactory clientFactory, ILogger 0) @@ -81,51 +63,107 @@ internal sealed class OAuth2Provider(IHttpClientFactory clientFactory, ILogger parameter in configuration.TokenEndpoint.AdditionalHeaderParameters) + { + request.Headers.Add(parameter.Key, parameter.Value); + } + } + + private static FormUrlEncodedContent GetClientCredentialsContent(OAuth2Configuration configuration) { Dictionary requestBody = new() { { OAuth2Keyword.GrantType, OAuth2Keyword.ClientCredentials } }; - if (!configuration.UseBasicAuthorizationHeader) + if (!configuration.ClientCredentials!.UseBasicAuthorizationHeader) + { + requestBody.Add(OAuth2Keyword.ClientId, configuration.ClientCredentials.ClientId); + requestBody.Add(OAuth2Keyword.ClientSecret, configuration.ClientCredentials.ClientSecret); + } + + if (!string.IsNullOrWhiteSpace(configuration.Scope)) { - requestBody.Add(OAuth2Keyword.ClientId, configuration.ClientId); - requestBody.Add(OAuth2Keyword.ClientSecret, configuration.ClientSecret); + requestBody.Add(OAuth2Keyword.Scope, configuration.Scope.Trim()); } - if (!string.IsNullOrWhiteSpace(scope)) + foreach (KeyValuePair parameter in configuration.TokenEndpoint.AdditionalBodyParameters) { - requestBody.Add(OAuth2Keyword.Scope, scope!.Trim()); + requestBody.Add(parameter.Key, parameter.Value); } - return new FormUrlEncodedContent(requestBody!); + return new FormUrlEncodedContent(requestBody); } - private async Task ParseResponseAsync(OAuth2Configuration configuration, HttpResponseMessage result, + private static Uri GetCompleteTokenUrl(OAuth2Endpoint tokenEndpoint) + { + if (tokenEndpoint.AdditionalQueryParameters.Count == 0) + { + return tokenEndpoint.Url; + } + + UriBuilder uriBuilder = new(tokenEndpoint.Url); + + StringBuilder stringBuilder = new(); + + foreach (KeyValuePair parameter in tokenEndpoint.AdditionalQueryParameters) + { + stringBuilder.Append(CultureInfo.InvariantCulture, $"{parameter.Key}={WebUtility.UrlEncode(parameter.Value)}"); + } + + stringBuilder.Replace("&", "?", 0, 1); + + uriBuilder.Query = stringBuilder.ToString(); + + return uriBuilder.Uri; + } + + private static HttpRequestMessage GetTokenRequest(OAuth2Configuration configuration) + { + HttpRequestMessage request = new(HttpMethod.Get, GetCompleteTokenUrl(configuration.TokenEndpoint)) + { + Method = HttpMethod.Post, + Content = GetClientCredentialsContent(configuration) + }; + + AddTokenRequestHeaders(request, configuration); + return request; + } + + private async Task ParseResponseAsync(OAuth2Configuration configuration, HttpResponseMessage response, CancellationToken cancellationToken) { - string body = await result.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + string body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - if (!result.IsSuccessStatusCode) + if (!response.IsSuccessStatusCode) { - if (result.StatusCode != HttpStatusCode.BadRequest || - !TryParseAndLogOAuth2Error(body, configuration.TokenEndpoint, configuration.ClientCredentials!.ClientId)) + if (response.StatusCode != HttpStatusCode.BadRequest || + !TryParseAndLogOAuth2Error(body, configuration.TokenEndpoint.Url, configuration.ClientCredentials!.ClientId)) { logger.LogError("Could not authenticate against {TokenEndpoint}, the returned status code was {StatusCode}. Response body: {Body}.", - configuration.TokenEndpoint, result.StatusCode, body); + configuration.TokenEndpoint, response.StatusCode, body); } return null; @@ -150,26 +188,6 @@ private static FormUrlEncodedContent GetClientCredentialsContent(ClientCredentia return token; } - private async Task PostWithBasicAuthenticationAsync(OAuth2Configuration configuration, FormUrlEncodedContent requestContent, - CancellationToken cancellationToken) - { - string encodedAuthorization = Convert.ToBase64String( - Encoding.ASCII.GetBytes($"{configuration.ClientCredentials!.ClientId}:{configuration.ClientCredentials.ClientSecret}")); - - using HttpRequestMessage request = new() - { - Content = requestContent, - Method = HttpMethod.Post, - RequestUri = configuration.TokenEndpoint, - Headers = - { - Authorization = new AuthenticationHeaderValue("Basic", encodedAuthorization) - } - }; - - return await _client.SendAsync(request, cancellationToken).ConfigureAwait(false); - } - private bool TryParseAndLogOAuth2Error(string errorContent, Uri tokenEndpoint, string? clientId) { ErrorResponse? response = null; @@ -220,5 +238,40 @@ private bool TryParseAndLogOAuth2Error(string errorContent, Uri tokenEndpoint, s return true; } + + private static void ValidateClientCredentialParameters(OAuth2Configuration configuration) + { + if (configuration.GrantType is not OAuth2GrantType.ClientCredentials) + { + throw new ArgumentException($"{nameof(configuration.GrantType)} must be {OAuth2GrantType.ClientCredentials}.", nameof(configuration)); + } + + if (configuration.ClientCredentials is null) + { + throw new ArgumentException($"{nameof(configuration.ClientCredentials)} is null.", nameof(configuration)); + } + + if (string.IsNullOrWhiteSpace(configuration.ClientCredentials.ClientId)) + { + throw new ArgumentException($"{nameof(configuration.ClientCredentials)}.{nameof(configuration.ClientCredentials.ClientId)} must be specified.", + nameof(configuration)); + } + + if (string.IsNullOrWhiteSpace(configuration.ClientCredentials.ClientSecret)) + { + throw new ArgumentException($"{nameof(configuration.ClientCredentials)}.{nameof(configuration.ClientCredentials.ClientSecret)} must be specified.", + nameof(configuration)); + } + + if (configuration.TokenEndpoint is null) + { + throw new ArgumentException($"{nameof(configuration.TokenEndpoint)} is null.", nameof(configuration)); + } + + if (configuration.TokenEndpoint.Url is null) + { + throw new ArgumentException($"{nameof(configuration.TokenEndpoint)}.{nameof(configuration.TokenEndpoint.Url)} must be specified.", nameof(configuration)); + } + } } } diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs index ffe5a5f..af682b0 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/GetClientCredentialsAccessTokenAsyncTests.cs @@ -5,7 +5,6 @@ using System.Net.Http.Json; using System.Text; using FluentAssertions; -using FluentAssertions.Execution; using KISS.HttpClientAuthentication.Configuration; using KISS.HttpClientAuthentication.Constants; using KISS.HttpClientAuthentication.Helpers; @@ -38,7 +37,7 @@ public async Task TestMissingClientCredentialsConfigurationSectionThrowsArgument Func act = () => provider.GetClientCredentialsAccessTokenAsync(new() { GrantType = OAuth2GrantType.ClientCredentials }, default!) .AsTask(); - await act.Should().ThrowAsync().WithParameterName("configuration").WithMessage("No valid ClientCredentials found.*"); + await act.Should().ThrowAsync().WithParameterName("configuration").WithMessage("ClientCredentials is null.*"); } [Theory] @@ -54,7 +53,7 @@ public async Task TestClientIdIsNullEmptyOrWhitespacesThrowsArgumentException(st OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = clientId!, @@ -83,7 +82,7 @@ public async Task TestClientSecretIsNullEmptyOrWhitespacesThrowsArgumentExceptio OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -99,6 +98,43 @@ await act.Should().ThrowAsync() .WithMessage("ClientCredentials.ClientSecret must be specified.*"); } + [Fact] + public async Task TestMissingTokenEndpointConfigurationSectionThrowsArgumentException() + { + OAuth2Provider provider = BuildServices().GetRequiredService(); + + Func act = () => provider.GetClientCredentialsAccessTokenAsync(new() + { + GrantType = OAuth2GrantType.ClientCredentials, + ClientCredentials = new() + { + ClientId = "client_id", + ClientSecret = "client_secret" + } + }, default!).AsTask(); + + await act.Should().ThrowAsync().WithParameterName("configuration").WithMessage("TokenEndpoint is null.*"); + } + + [Fact] + public async Task TestMissingTokenEndpointUrlThrowsArgumentException() + { + OAuth2Provider provider = BuildServices().GetRequiredService(); + + Func act = () => provider.GetClientCredentialsAccessTokenAsync(new() + { + GrantType = OAuth2GrantType.ClientCredentials, + ClientCredentials = new() + { + ClientId = "client_id", + ClientSecret = "client_secret" + }, + TokenEndpoint = new() + }, default!).AsTask(); + + await act.Should().ThrowAsync().WithParameterName("configuration").WithMessage("TokenEndpoint.Url must be specified.*"); + } + [Fact] public async Task TestCacheHitIsReturnedAsToken() { @@ -120,7 +156,7 @@ public async Task TestCacheHitIsReturnedAsToken() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -138,7 +174,7 @@ public async Task TestCacheHitIsReturnedAsToken() Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogInformation("Token for {TokenEndpoint} with client id {ClientId} found in cache, using this.", + loggerMock.VerifyExt(l => l.LogDebug("Token for {TokenEndpoint} with client id {ClientId} found in cache, using this.", "https://somehost/", "client_id"), Times.Once); } @@ -165,7 +201,7 @@ public async Task TestGetAndCacheAccessTokenResponse() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -194,7 +230,7 @@ public async Task TestGetAndCacheAccessTokenResponse() loggerMock.VerifyExt(l => l.LogDebug("Could not find existing token in cache, requesting token from endpoint {TokenEndpoint} with client id {ClientId}.", "https://somehost/", "client_id"), Times.Once); - loggerMock.VerifyExt(l => l.LogInformation("Token retrieved from {TokenEndpoint} with client id {ClientId} and cached for {CacheExpiresIn} seconds.", + loggerMock.VerifyExt(l => l.LogDebug("Token retrieved from {TokenEndpoint} with client id {ClientId} and cached for {CacheExpiresIn} seconds.", "https://somehost/", "client_id", 3420), Times.Once); } @@ -221,7 +257,7 @@ public async Task TestNoCachingOfAccessTokenResponseWithMissingExpiresIn() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -238,7 +274,7 @@ public async Task TestNoCachingOfAccessTokenResponseWithMissingExpiresIn() Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogInformation("Token retrieved from {TokenEndpoint} with client id {ClientId}, but not cached since it is missing expires_in information.", + loggerMock.VerifyExt(l => l.LogDebug("Token retrieved from {TokenEndpoint} with client id {ClientId}, but not cached since it is missing expires_in information.", "https://somehost/", "client_id"), Times.Once); } @@ -265,7 +301,7 @@ public async Task TestNoCachingOfAccessTokenResponseWhenCacheIsDiabled() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -283,7 +319,7 @@ public async Task TestNoCachingOfAccessTokenResponseWhenCacheIsDiabled() Mock> loggerMock = services.GetRequiredService>>(); - loggerMock.VerifyExt(l => l.LogInformation("Token retrieved from {TokenEndpoint} with client id {ClientId}, but the token cache is disabled.", + loggerMock.VerifyExt(l => l.LogDebug("Token retrieved from {TokenEndpoint} with client id {ClientId}, but the token cache is disabled.", "https://somehost/", "client_id"), Times.Once); } @@ -294,21 +330,24 @@ public async Task TestUseFormBasedAuthentication() Mock httpClientMock = services.GetRequiredService>(); + HttpRequestMessage? actualRequest = null; + httpClientMock.Setup(httpClient => httpClient.SendAsync(It.IsAny(), It.IsAny())) - .Callback(async (HttpRequestMessage request, CancellationToken _) => + .Callback((HttpRequestMessage request, CancellationToken _) => actualRequest = request) + .ReturnsAsync(() => { - request.Should().NotBeNull("Missing SendAsync request."); - request.Headers.Authorization.Should().BeNull(); + actualRequest!.Headers.Authorization.Should().BeNull(); - string content = await request.Content!.ReadAsStringAsync(default); - content.Should().Be($"grant_type=client_credentials&client_id=client_id&client_secret=client_secret{Environment.NewLine}"); - }) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound)); + string content = actualRequest.Content!.ReadAsStringAsync(default).GetAwaiter().GetResult(); + content.Should().Be($"grant_type=client_credentials&client_id=client_id&client_secret=client_secret"); + + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -328,29 +367,26 @@ public async Task TestUseBasicAuthentication() Mock httpClientMock = services.GetRequiredService>(); - HttpRequestMessage? actualRequest = null!; + HttpRequestMessage? actualRequest = null; httpClientMock.Setup(httpClient => httpClient.SendAsync(It.IsAny(), It.IsAny())) - .Callback(async (HttpRequestMessage request, CancellationToken _) => + .Callback((HttpRequestMessage request, CancellationToken _) => actualRequest = request) + .ReturnsAsync(() => { - actualRequest.Should().NotBeNull("Missing SendAsync request."); - actualRequest.Headers.Authorization.Should().NotBeNull(); + actualRequest!.Headers.Authorization.Should().NotBeNull(); - using (new AssertionScope()) - { - actualRequest.Headers.Authorization!.Scheme.Should().Be("Basic"); - actualRequest.Headers.Authorization!.Parameter.Should().Be(Convert.ToBase64String(Encoding.ASCII.GetBytes($"client_id:client_secret"))); - } + actualRequest.Headers.Authorization!.Scheme.Should().Be("Basic"); + actualRequest.Headers.Authorization!.Parameter.Should().Be(Convert.ToBase64String(Encoding.ASCII.GetBytes($"client_id:client_secret"))); - string content = await request.Content!.ReadAsStringAsync(default); - content.Should().Be($"grant_type=client_credentials{Environment.NewLine}"); - }) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound)); + string content = actualRequest.Content!.ReadAsStringAsync(default).GetAwaiter().GetResult(); + content.Should().Be($"grant_type=client_credentials"); + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { UseBasicAuthorizationHeader = true, @@ -371,21 +407,24 @@ public async Task TestRequestContainsScopeWhenSpecified() Mock httpClientMock = services.GetRequiredService>(); + HttpRequestMessage? actualRequest = null; + httpClientMock.Setup(httpClient => httpClient.SendAsync(It.IsAny(), It.IsAny())) - .Callback(async (HttpRequestMessage request, CancellationToken _) => + .Callback((HttpRequestMessage request, CancellationToken _) => actualRequest = request) + .ReturnsAsync(() => { - request.Should().NotBeNull("Missing SendAsync request."); - request.Headers.Authorization.Should().BeNull(); + actualRequest!.Headers.Authorization.Should().BeNull(); + + string content = actualRequest.Content!.ReadAsStringAsync(default).GetAwaiter().GetResult(); + content.Should().Contain($"scope=test_scope"); - string content = await request.Content!.ReadAsStringAsync(default); - content.Should().Be($"*scope=test_scope*"); - }) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound)); + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -410,21 +449,24 @@ public async Task TestRequestHasNoScopeWhenNullEmptyOrWhitespace(string? scope) Mock httpClientMock = services.GetRequiredService>(); + HttpRequestMessage? actualRequest = null; + httpClientMock.Setup(httpClient => httpClient.SendAsync(It.IsAny(), It.IsAny())) - .Callback(async (HttpRequestMessage request, CancellationToken _) => + .Callback((HttpRequestMessage request, CancellationToken _) => actualRequest = request) + .ReturnsAsync(() => { - request.Should().NotBeNull("Missing SendAsync request."); - request.Headers.Authorization.Should().BeNull(); + actualRequest!.Headers.Authorization.Should().BeNull(); - string content = await request.Content!.ReadAsStringAsync(default); - content.Should().NotBe($"*scope=*"); - }) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound)); + string content = actualRequest.Content!.ReadAsStringAsync(default).GetAwaiter().GetResult(); + content.Should().NotContain($"scope="); + + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -437,5 +479,62 @@ public async Task TestRequestHasNoScopeWhenNullEmptyOrWhitespace(string? scope) await provider.GetClientCredentialsAccessTokenAsync(configuration, default); } + + + [Fact] + public async Task TestRequestContainsAdditionalParametersSpecified() + { + IServiceProvider services = BuildServices(); + + Mock httpClientMock = services.GetRequiredService>(); + + HttpRequestMessage? actualRequest = null; + + httpClientMock.Setup(httpClient => httpClient.SendAsync(It.IsAny(), It.IsAny())) + .Callback((HttpRequestMessage request, CancellationToken _) => actualRequest = request) + .ReturnsAsync(() => + { + actualRequest!.RequestUri!.Query.Should().Be("?query=query_value_with_%3F"); + + actualRequest.Headers.Should().Contain(kvp => + kvp.Key == "header" && kvp.Value.All(v => "header_value".Equals(v))); + + string content = actualRequest.Content!.ReadAsStringAsync(default).GetAwaiter().GetResult(); + + content.Should().Contain("body=body_value"); + + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + + OAuth2Configuration configuration = new() + { + GrantType = OAuth2GrantType.ClientCredentials, + TokenEndpoint = new() + { + Url = new("https://somehost/"), + AdditionalBodyParameters = + { + { "body", "body_value" } + }, + AdditionalHeaderParameters = + { + { "header", "header_value" } + }, + AdditionalQueryParameters = + { + { "query", "query_value_with_?" } + } + }, + ClientCredentials = new() + { + ClientId = "client_id", + ClientSecret = "client_secret" + } + }; + + OAuth2Provider provider = services.GetRequiredService(); + + await provider.GetClientCredentialsAccessTokenAsync(configuration, default); + } } } diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs index e1565df..00bdf10 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/ParseResponseAsyncTests.cs @@ -41,7 +41,7 @@ public async Task TestSetsTokenTypeToBearerWhenNotSpecified() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -81,7 +81,7 @@ public async Task TestUsesConfiguredAuthorizationSchemeAsTokenType() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, AuthorizationScheme = "Authorization_Scheme", ClientCredentials = new() { @@ -110,7 +110,7 @@ public async Task TestFailedRequestReturnsNullAndLogsError() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -146,7 +146,7 @@ public async Task TestInvalidResponseReturnsNullAndLogsError(string response) OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", diff --git a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs index 122cf02..d25b7df 100644 --- a/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs +++ b/test/HttpClientAuthentication.Test/Helpers/OAuth2ProviderTests/TryParseAndLogOAuth2ErrorTests.cs @@ -37,7 +37,7 @@ public async Task TestErrorIsFullyParsedAndLogged() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -76,7 +76,7 @@ public async Task TestDescriptionIsSkippedWhenNotSpecified() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -115,7 +115,7 @@ public async Task TestUriIsSkippedWhenNotSpecified() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -154,7 +154,7 @@ public async Task TestDescriptionAndUriIsSkippedWhenNotSpecified() OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", @@ -191,7 +191,7 @@ public async Task TestParsingIsSkippedOnInvalidErrorContent(string content) OAuth2Configuration configuration = new() { GrantType = OAuth2GrantType.ClientCredentials, - TokenEndpoint = new("https://somehost/"), + TokenEndpoint = new() { Url = new("https://somehost/") }, ClientCredentials = new() { ClientId = "client_id", From f23ea7585ac88b38d8f20ab14dbbdd0f47c9165f Mon Sep 17 00:00:00 2001 From: Rune Gulbrandsen Date: Tue, 14 Jan 2025 14:33:07 +0100 Subject: [PATCH 5/5] Update build scripts to use .NET 9 SDK --- .github/workflows/master-pr.yml | 4 ++-- .github/workflows/master-publish.yml | 4 ++-- .github/workflows/master-push.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/master-pr.yml b/.github/workflows/master-pr.yml index 7a42034..1e4aa96 100644 --- a/.github/workflows/master-pr.yml +++ b/.github/workflows/master-pr.yml @@ -22,10 +22,10 @@ jobs: steps: - name: Checkout source uses: actions/checkout@v4 - - name: Setup .NET 8.0.x + - name: Setup .NET 9.0.x uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Restore solution run: dotnet restore - name: Build solution diff --git a/.github/workflows/master-publish.yml b/.github/workflows/master-publish.yml index a292d32..b53d9c1 100644 --- a/.github/workflows/master-publish.yml +++ b/.github/workflows/master-publish.yml @@ -16,10 +16,10 @@ jobs: echo "VERSION=${VERSION#v}" >> $GITHUB_ENV - name: Checkout source uses: actions/checkout@v4 - - name: Setup .NET 8.0.x + - name: Setup .NET 9.0.x uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Restore solution run: dotnet restore - name: Build solution (pre-release) diff --git a/.github/workflows/master-push.yml b/.github/workflows/master-push.yml index 980d9a2..1bccccf 100644 --- a/.github/workflows/master-push.yml +++ b/.github/workflows/master-push.yml @@ -19,10 +19,10 @@ jobs: steps: - name: Checkout source uses: actions/checkout@v4 - - name: Setup .NET 8.0.x + - name: Setup .NET 9.0.x uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Restore solution run: dotnet restore - name: Build solution