Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ dotnet_diagnostic.ca1819.severity = none
dotnet_diagnostic.CA1848.severity = silent

# CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA2007.severity = none

# IDE0058: Expression value is never used
dotnet_diagnostic.IDE0058.severity = none
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/master-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ jobs:
steps:
- name: Checkout source
uses: actions/checkout@v3
- name: Setup .NET 7.0.x
- name: Setup .NET 8.0.x
uses: actions/setup-dotnet@v3
with:
dotnet-version: '7.0.x'
dotnet-version: '8.0.x'
- name: Restore solution
run: dotnet restore
- name: Build solution
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/master-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
echo "VERSION=${VERSION#v}" >> $GITHUB_ENV
- name: Checkout source
uses: actions/checkout@v3
- name: Setup .NET 7.0.x
- name: Setup .NET 8.0.x
uses: actions/setup-dotnet@v3
with:
dotnet-version: '7.0.x'
dotnet-version: '8.0.x'
- name: Restore solution
run: dotnet restore
- name: Build solution (pre-release)
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/master-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
steps:
- name: Checkout source
uses: actions/checkout@v3
- name: Setup .NET 7.0.x
- name: Setup .NET 8.0.x
uses: actions/setup-dotnet@v3
with:
dotnet-version: '7.0.x'
dotnet-version: '8.0.x'
- name: Restore solution
run: dotnet restore
- name: Build solution
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2023 Rune Gulbrandsen
Copyright (c) 2024 Rune Gulbrandsen

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,14 @@ Authentication using OAuth2.

##### Client credentials

Using OAuth2 client credentials, all settings except `Scope` is required.
Using OAuth2 client credentials, all settings except `DisableTokenCache` and `Scope` is required.

```
"<section name>": {
"AuthenticationProvider": "OAuth2",
"OAuth2": {
"AuthorizationEndpoint": "<OAuth2 token endpoint>",
"DisableTokenCache": false,
"GrantType": "ClientCredentials",
"Scope": "<Optional scopes separated by space>",
"ClientCredentials": {
Expand Down
2 changes: 1 addition & 1 deletion src/HttpClientAuthentication/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using System.Runtime.CompilerServices;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

namespace KISS.HttpClientAuthentication.Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

namespace KISS.HttpClientAuthentication.Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

namespace KISS.HttpClientAuthentication.Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using KISS.HttpClientAuthentication.Constants;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using KISS.HttpClientAuthentication.Constants;
Expand All @@ -24,6 +24,11 @@ public sealed class OAuth2Configuration
/// </summary>
public string? AuthorizationScheme { get; set; }

/// <summary>
/// Gets or sets if the access token should be cached or not.
/// </summary>
public bool DisableTokenCache { get; set; }

/// <summary>
/// Gets or sets the type of grant flow to be used.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

namespace KISS.HttpClientAuthentication.Constants
Expand Down
2 changes: 1 addition & 1 deletion src/HttpClientAuthentication/Constants/OAuth2GrantType.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

namespace KISS.HttpClientAuthentication.Constants
Expand Down
2 changes: 1 addition & 1 deletion src/HttpClientAuthentication/Constants/OAuth2Keyword.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

namespace KISS.HttpClientAuthentication.Constants
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using KISS.HttpClientAuthentication.Configuration;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

namespace KISS.HttpClientAuthentication.Handlers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using System.Net.Http.Headers;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

namespace KISS.HttpClientAuthentication.Handlers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using System.Net.Http.Headers;
Expand All @@ -8,20 +8,13 @@

namespace KISS.HttpClientAuthentication.Handlers
{
internal sealed class OAuth2AuthenticationHandler : BaseAuthenticationHandler<OAuth2Configuration>
internal sealed class OAuth2AuthenticationHandler(IOAuth2Provider provider) : BaseAuthenticationHandler<OAuth2Configuration>
{
private readonly IOAuth2Provider _provider;

public OAuth2AuthenticationHandler(IOAuth2Provider provider)
{
_provider = provider;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
AccessTokenResponse? token = Configuration.GrantType switch
{
OAuth2GrantType.ClientCredentials => await _provider.GetClientCredentialsAccessTokenAsync(Configuration, cancellationToken).ConfigureAwait(false),
OAuth2GrantType.ClientCredentials => await provider.GetClientCredentialsAccessTokenAsync(Configuration, cancellationToken).ConfigureAwait(false),
OAuth2GrantType.None => throw new InvalidOperationException($"{nameof(Configuration.GrantType)} must be specified."),
_ => throw new InvalidOperationException($"The {nameof(Configuration.GrantType)} {Configuration.GrantType} is not supported."),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using System.Text.Json.Serialization;
Expand Down
2 changes: 1 addition & 1 deletion src/HttpClientAuthentication/Helpers/ErrorResponse.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using System.Text.Json.Serialization;
Expand Down
2 changes: 1 addition & 1 deletion src/HttpClientAuthentication/Helpers/IOAuth2Provider.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using KISS.HttpClientAuthentication.Configuration;
Expand Down
51 changes: 24 additions & 27 deletions src/HttpClientAuthentication/Helpers/OAuth2Provider.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using System.Net;
Expand All @@ -15,18 +15,10 @@ namespace KISS.HttpClientAuthentication.Helpers
/// <summary>
/// Implementation of <see cref="IOAuth2Provider"/>.
/// </summary>
internal sealed class OAuth2Provider : IOAuth2Provider
internal sealed class OAuth2Provider(IHttpClientFactory clientFactory, ILogger<OAuth2Provider> logger, IMemoryCache memoryCache)
: IOAuth2Provider
{
private readonly HttpClient _client;
private readonly ILogger<OAuth2Provider> _logger;
private readonly IMemoryCache _memoryCache;

public OAuth2Provider(IHttpClientFactory clientFactory, ILogger<OAuth2Provider> logger, IMemoryCache memoryCache)
{
_client = clientFactory.CreateClient(nameof(HttpClientAuthentication));
_logger = logger;
_memoryCache = memoryCache;
}
private readonly HttpClient _client = clientFactory.CreateClient(nameof(HttpClientAuthentication));

/// <inheritdoc />
public async ValueTask<AccessTokenResponse?> GetClientCredentialsAccessTokenAsync(OAuth2Configuration configuration, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -56,15 +48,15 @@ public OAuth2Provider(IHttpClientFactory clientFactory, ILogger<OAuth2Provider>

string cacheKey = $"{configuration.GrantType}#{configuration.AuthorizationEndpoint}#{configuration.ClientCredentials!.ClientId}";

if (_memoryCache.TryGetValue(cacheKey, out AccessTokenResponse? token))
if (memoryCache.TryGetValue(cacheKey, out AccessTokenResponse? token))
{
_logger.LogInformation("Token for {AuthorizationEndpoint} with client id {ClientId} found in cache, using this.",
configuration.AuthorizationEndpoint, configuration.ClientCredentials.ClientId);
logger.LogInformation("Token for {AuthorizationEndpoint} with client id {ClientId} found in cache, using this.",
configuration.AuthorizationEndpoint, configuration.ClientCredentials.ClientId);
return token;
}

_logger.LogDebug("Could not find existing token in cache, requesting token from endpoint {AuthorizationEndpoint} with client id {ClientId}.",
configuration.AuthorizationEndpoint, configuration.ClientCredentials.ClientId);
logger.LogDebug("Could not find existing token in cache, requesting token from endpoint {AuthorizationEndpoint} with client id {ClientId}.",
configuration.AuthorizationEndpoint, configuration.ClientCredentials.ClientId);

using FormUrlEncodedContent requestContent = GetClientCredentialsContent(configuration.ClientCredentials!, configuration.Scope);

Expand All @@ -79,18 +71,23 @@ public OAuth2Provider(IHttpClientFactory clientFactory, ILogger<OAuth2Provider>
return null;
}

if (token.ExpiresIn > 0)
if (configuration.DisableTokenCache)
{
logger.LogInformation("Token retrieved from {AuthorizationEndpoint} with client id {ClientId}, but the token cache is disabled.",
configuration.AuthorizationEndpoint, configuration.ClientCredentials!.ClientId);
}
else if (token.ExpiresIn > 0)
{
double cacheExpiresIn = (int)token.ExpiresIn * 0.95;
_memoryCache.Set(cacheKey, token, TimeSpan.FromSeconds(cacheExpiresIn));
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 {AuthorizationEndpoint} with client id {ClientId} and cached for {CacheExpiresIn} seconds.",
configuration.AuthorizationEndpoint, 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 {AuthorizationEndpoint} with client id {ClientId}, but not cached since it is missing expires_in information.",
configuration.AuthorizationEndpoint, configuration.ClientCredentials!.ClientId);
}

return token;
Expand Down Expand Up @@ -131,8 +128,8 @@ private static FormUrlEncodedContent GetClientCredentialsContent(ClientCredentia
if (result.StatusCode != HttpStatusCode.BadRequest ||
!TryParseAndLogOAuth2Error(body, configuration.AuthorizationEndpoint, 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 {AuthorizationEndpoint}, the returned status code was {StatusCode}. Response body: {Body}.",
configuration.AuthorizationEndpoint, result.StatusCode, body);
}

return null;
Expand All @@ -142,7 +139,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 {AuthorizationEndpoint} is not a valid OAuth2 result.", configuration.AuthorizationEndpoint);

return null;
}
Expand Down Expand Up @@ -222,7 +219,7 @@ private bool TryParseAndLogOAuth2Error(string errorContent, Uri authorizationEnd
logMessage.Append('.');

#pragma warning disable CA2254 // Template should be a static expression
_logger.LogError(logMessage.ToString());
logger.LogError(logMessage.ToString());
#pragma warning restore CA2254 // Template should be a static expression

return true;
Expand Down
27 changes: 9 additions & 18 deletions src/HttpClientAuthentication/HttpClientAuthentication.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<AssemblyName>KISS.HttpClientAuthentication</AssemblyName>
<RootNamespace>KISS.HttpClientAuthentication</RootNamespace>

Expand All @@ -9,9 +9,9 @@
<ImplicitUsings>enable</ImplicitUsings>

<!-- Ensure bump this when working on new version-->
<VersionPrefix>1.0.0</VersionPrefix>
<VersionPrefix>2.0.0</VersionPrefix>
<Authors>Rune Gulbrandsen</Authors>
<Copyright>Copyright (c) 2023 Rune Gulbrandsen. All rights reserved.</Copyright>
<Copyright>Copyright (c) 2024 Rune Gulbrandsen. All rights reserved.</Copyright>
<Summary>
Extension methods to apply authentication handling to HttpClient based on configuration from
.NET configuration providers.
Expand All @@ -20,18 +20,9 @@
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>

<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
Expand All @@ -41,12 +32,12 @@
<PackageReference Include="System.Text.Json" Version="6.0.8" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="System.Text.Json" Version="7.0.3" />
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.3" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Rune Gulbrandsen.
// Copyright © 2024 Rune Gulbrandsen.
// All rights reserved. Licensed under the MIT License; see LICENSE.txt.

using KISS.HttpClientAuthentication.Configuration;
Expand Down Expand Up @@ -50,6 +50,10 @@ public static IHttpClientBuilder AddAuthenticatedHttpMessageHandler(this IHttpCl
/// </exception>
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));
Expand All @@ -59,6 +63,7 @@ public static IHttpClientBuilder AddAuthenticatedHttpMessageHandler(this IHttpCl
{
throw new ArgumentNullException(nameof(configSection));
}
#endif

builder.Services.AddMemoryCache();

Expand Down
Loading