From 4e164422a7b66d1bcf364cae205f0966ced1a8e9 Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Mon, 17 Mar 2025 11:31:58 -0300 Subject: [PATCH 1/2] Created LogDinifitions --- .../Log/AuthorizationInterceptorLogDifinitions.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/AuthorizationInterceptor/Log/AuthorizationInterceptorLogDifinitions.cs diff --git a/src/AuthorizationInterceptor/Log/AuthorizationInterceptorLogDifinitions.cs b/src/AuthorizationInterceptor/Log/AuthorizationInterceptorLogDifinitions.cs new file mode 100644 index 0000000..5fc24b2 --- /dev/null +++ b/src/AuthorizationInterceptor/Log/AuthorizationInterceptorLogDifinitions.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Logging; + +namespace AuthorizationInterceptor.Log; + +public static partial class AuthorizationInterceptorLogDifinitions +{ + [LoggerMessage(Level = LogLevel.Warning, Message = "No interceptor was configured for HttpClient `{httpClientName}`. A Runtime interceptor was used instead. It is recommended to use at least the MemoryCache interceptor.")] + public static partial void LogNoInterceptorUsed(this ILogger logger, string httpClientName); +} + From 402adf90d7a6f66a941c73a1640612d5f9578a06 Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Mon, 17 Mar 2025 20:05:06 -0300 Subject: [PATCH 2/2] - Added Hybrid Cache support - Removed .NET 5 support - Fixed bug independence of Interceptors between multiple HttpClients in the same solution. - Updated Samples --- AuthorizationInterceptor.sln | 14 ++ samples/AuthorizationInterceptor.Sample.sln | 28 ++- samples/SourceApi/Program.cs | 70 ++++++- samples/SourceApi/SourceApi.csproj | 8 +- .../SourceApi/appsettings.Development.json | 9 + samples/TargetApi/Program.cs | 15 +- .../TargetApi/appsettings.Development.json | 11 +- samples/TargetApi/appsettings.json | 2 +- ...Interceptor.Extensions.Abstractions.csproj | 4 - .../Handlers/IAuthenticationHandler.cs | 29 ++- .../Headers/AuthorizationHeaders.cs | 130 ++++++------ .../Headers/OAuthHeaders.cs | 59 +++--- .../Interceptors/IAuthorizationInterceptor.cs | 49 +++-- .../Json/AuthorizationHeadersJsonConverter.cs | 190 +++++++++--------- .../AuthorizationHeadersJsonSerializer.cs | 49 +++-- .../IAuthorizationInterceptorOptions.cs | 22 +- ...thorizationInterceptorOptionsExtensions.cs | 25 ++- ...istributedCacheAuthorizationInterceptor.cs | 43 ++-- ...nInterceptor.Extensions.HybridCache.csproj | 17 ++ ...thorizationInterceptorOptionsExtensions.cs | 21 ++ .../HybridCacheAuthorizationInterceptor.cs | 37 ++++ .../README.md | 32 +++ ...thorizationInterceptorOptionsExtensions.cs | 30 ++- .../Interceptors/MemoryCacheInterceptor.cs | 49 ++--- .../Extensions/HttpClientBuilderExtensions.cs | 81 ++++---- .../AuthorizationInterceptorHandler.cs | 119 ++++++----- .../AuthorizationInterceptorLogDifinitions.cs | 8 +- .../AuthorizationInterceptorOptions.cs | 40 ++-- .../AuthorizationInterceptorStrategy.cs | 167 ++++++++------- .../IAuthorizationInterceptorStrategy.cs | 13 +- src/Directory.Build.props | 8 +- ...butedCacheAuthorizationInterceptorTests.cs | 2 +- ...ceptor.Extensions.HybridCache.Tests.csproj | 5 + ...zationInterceptorBuilderExtensionsTests.cs | 22 ++ ...ybridCacheAuthorizationInterceptorTests.cs | 103 ++++++++++ .../AuthorizationInterceptorHandlerTests.cs | 13 +- .../AuthorizationInterceptorStrategyTests.cs | 8 +- .../Utils/MockAuthorizationInterceptor.cs | 19 +- ...izationInterceptorAuthenticationHandler.cs | 11 +- 39 files changed, 940 insertions(+), 622 deletions(-) create mode 100644 src/AuthorizationInterceptor.Extensions.HybridCache/AuthorizationInterceptor.Extensions.HybridCache.csproj create mode 100644 src/AuthorizationInterceptor.Extensions.HybridCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs create mode 100644 src/AuthorizationInterceptor.Extensions.HybridCache/Interceptors/HybridCacheAuthorizationInterceptor.cs create mode 100644 src/AuthorizationInterceptor.Extensions.HybridCache/README.md create mode 100644 tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/AuthorizationInterceptor.Extensions.HybridCache.Tests.csproj create mode 100644 tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/Extensions/AuthorizationInterceptorBuilderExtensionsTests.cs create mode 100644 tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/Interceptors/HybridCacheAuthorizationInterceptorTests.cs diff --git a/AuthorizationInterceptor.sln b/AuthorizationInterceptor.sln index 53c431b..481ba1d 100644 --- a/AuthorizationInterceptor.sln +++ b/AuthorizationInterceptor.sln @@ -23,6 +23,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{200E9ECE-6ABD-4AB7-AA41-31FC95E618E7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationInterceptor.Extensions.HybridCache", "src\AuthorizationInterceptor.Extensions.HybridCache\AuthorizationInterceptor.Extensions.HybridCache.csproj", "{4F7333E9-31FC-4D90-9D8F-2BBB075E2412}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationInterceptor.Extensions.HybridCache.Tests", "tests\AuthorizationInterceptor.Extensions.HybridCache.Tests\AuthorizationInterceptor.Extensions.HybridCache.Tests.csproj", "{95360C34-CA3E-47F7-825E-69E00AF42C71}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,6 +65,14 @@ Global {72A318F6-B2F7-B014-0657-071FE035DE96}.Debug|Any CPU.Build.0 = Debug|Any CPU {72A318F6-B2F7-B014-0657-071FE035DE96}.Release|Any CPU.ActiveCfg = Release|Any CPU {72A318F6-B2F7-B014-0657-071FE035DE96}.Release|Any CPU.Build.0 = Release|Any CPU + {4F7333E9-31FC-4D90-9D8F-2BBB075E2412}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F7333E9-31FC-4D90-9D8F-2BBB075E2412}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F7333E9-31FC-4D90-9D8F-2BBB075E2412}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F7333E9-31FC-4D90-9D8F-2BBB075E2412}.Release|Any CPU.Build.0 = Release|Any CPU + {95360C34-CA3E-47F7-825E-69E00AF42C71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95360C34-CA3E-47F7-825E-69E00AF42C71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95360C34-CA3E-47F7-825E-69E00AF42C71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95360C34-CA3E-47F7-825E-69E00AF42C71}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -74,6 +86,8 @@ Global {458A0C0A-6D65-DCF5-6B21-30D8E1374EE6} = {200E9ECE-6ABD-4AB7-AA41-31FC95E618E7} {F368BB6C-9EDD-4727-58BD-68799CF990B3} = {200E9ECE-6ABD-4AB7-AA41-31FC95E618E7} {72A318F6-B2F7-B014-0657-071FE035DE96} = {200E9ECE-6ABD-4AB7-AA41-31FC95E618E7} + {4F7333E9-31FC-4D90-9D8F-2BBB075E2412} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {95360C34-CA3E-47F7-825E-69E00AF42C71} = {200E9ECE-6ABD-4AB7-AA41-31FC95E618E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {16C3D677-0CE4-4111-9BD9-EC1C11956A8C} diff --git a/samples/AuthorizationInterceptor.Sample.sln b/samples/AuthorizationInterceptor.Sample.sln index 9ff1dde..096c1aa 100644 --- a/samples/AuthorizationInterceptor.Sample.sln +++ b/samples/AuthorizationInterceptor.Sample.sln @@ -7,7 +7,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TargetApi", "TargetApi\Targ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceApi", "SourceApi\SourceApi.csproj", "{835CBED5-B68E-4DF4-9B29-9BAD55824F76}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthorizationInterceptor", "..\src\AuthorizationInterceptor.csproj", "{6A402E57-96D7-43E7-9B87-7514CBDF29B0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationInterceptor", "..\src\AuthorizationInterceptor\AuthorizationInterceptor.csproj", "{426A39AB-B325-3774-1961-2E0C961E10AC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationInterceptor.Extensions.MemoryCache", "..\src\AuthorizationInterceptor.Extensions.MemoryCache\AuthorizationInterceptor.Extensions.MemoryCache.csproj", "{8622B530-4867-3E69-414B-D0A1DB23EE5E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationInterceptor.Extensions.DistributedCache", "..\src\AuthorizationInterceptor.Extensions.DistributedCache\AuthorizationInterceptor.Extensions.DistributedCache.csproj", "{114EA1B9-F717-2EA4-F5D1-7C9E97E37CAC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationInterceptor.Extensions.HybridCache", "..\src\AuthorizationInterceptor.Extensions.HybridCache\AuthorizationInterceptor.Extensions.HybridCache.csproj", "{B6B934AB-B383-0E92-B44A-22EC3C0F3B20}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -23,10 +29,22 @@ Global {835CBED5-B68E-4DF4-9B29-9BAD55824F76}.Debug|Any CPU.Build.0 = Debug|Any CPU {835CBED5-B68E-4DF4-9B29-9BAD55824F76}.Release|Any CPU.ActiveCfg = Release|Any CPU {835CBED5-B68E-4DF4-9B29-9BAD55824F76}.Release|Any CPU.Build.0 = Release|Any CPU - {6A402E57-96D7-43E7-9B87-7514CBDF29B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6A402E57-96D7-43E7-9B87-7514CBDF29B0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A402E57-96D7-43E7-9B87-7514CBDF29B0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6A402E57-96D7-43E7-9B87-7514CBDF29B0}.Release|Any CPU.Build.0 = Release|Any CPU + {426A39AB-B325-3774-1961-2E0C961E10AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {426A39AB-B325-3774-1961-2E0C961E10AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {426A39AB-B325-3774-1961-2E0C961E10AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {426A39AB-B325-3774-1961-2E0C961E10AC}.Release|Any CPU.Build.0 = Release|Any CPU + {8622B530-4867-3E69-414B-D0A1DB23EE5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8622B530-4867-3E69-414B-D0A1DB23EE5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8622B530-4867-3E69-414B-D0A1DB23EE5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8622B530-4867-3E69-414B-D0A1DB23EE5E}.Release|Any CPU.Build.0 = Release|Any CPU + {114EA1B9-F717-2EA4-F5D1-7C9E97E37CAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {114EA1B9-F717-2EA4-F5D1-7C9E97E37CAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {114EA1B9-F717-2EA4-F5D1-7C9E97E37CAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {114EA1B9-F717-2EA4-F5D1-7C9E97E37CAC}.Release|Any CPU.Build.0 = Release|Any CPU + {B6B934AB-B383-0E92-B44A-22EC3C0F3B20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6B934AB-B383-0E92-B44A-22EC3C0F3B20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6B934AB-B383-0E92-B44A-22EC3C0F3B20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6B934AB-B383-0E92-B44A-22EC3C0F3B20}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/SourceApi/Program.cs b/samples/SourceApi/Program.cs index a2fbce7..9dd6be4 100644 --- a/samples/SourceApi/Program.cs +++ b/samples/SourceApi/Program.cs @@ -2,6 +2,9 @@ using AuthorizationInterceptor.Extensions.Abstractions.Handlers; using AuthorizationInterceptor.Extensions.Abstractions.Headers; using AuthorizationInterceptor.Extensions.Abstractions.Interceptors; +using AuthorizationInterceptor.Extensions.DistributedCache.Extensions; +using AuthorizationInterceptor.Extensions.HybridCache.Extensions; +using AuthorizationInterceptor.Extensions.MemoryCache.Extensions; using System.Text.Json; using System.Text.Json.Serialization; @@ -10,12 +13,49 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// Add the cache options +builder.Services.AddMemoryCache(); +builder.Services.AddStackExchangeRedisCache(opt => +{ + opt.InstanceName = "SourceApi"; + opt.Configuration = "localhost:6379"; +}); +builder.Services.AddHybridCache(); + builder.Services.AddHttpClient("TargetApiAuth") .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5121")); -builder.Services.AddHttpClient("TargetApi") +builder.Services.AddHttpClient("TargetApiWithNoInterceptor") + .AddAuthorizationInterceptorHandler() + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5121")); + +builder.Services.AddHttpClient("TargetApiWithMemoryCache") + .AddAuthorizationInterceptorHandler(opt => + { + opt.UseMemoryCacheInterceptor(); + }) + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5121")); + +builder.Services.AddHttpClient("TargetApiWithDistributedCache") .AddAuthorizationInterceptorHandler(opt => { + opt.UseDistributedCacheInterceptor(); + }) + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5121")); + +builder.Services.AddHttpClient("TargetApiWithHybridCache") + .AddAuthorizationInterceptorHandler(opt => + { + opt.UseHybridCacheInterceptor(); + }) + .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5121")); + +builder.Services.AddHttpClient("TargetApiWithCustomInterceptors") + .AddAuthorizationInterceptorHandler(opt => + { + opt.UseMemoryCacheInterceptor(); + opt.UseDistributedCacheInterceptor(); + opt.UseCustomInterceptor(); opt.UseCustomInterceptor(); opt.UseCustomInterceptor(); @@ -30,9 +70,33 @@ app.UseSwaggerUI(); } -app.MapGet("/data", async (IHttpClientFactory factory) => +app.MapGet("/test/TargetApiWithNoInterceptor", async (IHttpClientFactory factory) => +{ + var client = factory.CreateClient("TargetApiWithNoInterceptor"); + return await client.GetAsync("/data"); +}); + +app.MapGet("/test/TargetApiWithMemoryCache", async (IHttpClientFactory factory) => +{ + var client = factory.CreateClient("TargetApiWithMemoryCache"); + return await client.GetAsync("/data"); +}); + +app.MapGet("/test/TargetApiWithDistributedCache", async (IHttpClientFactory factory) => +{ + var client = factory.CreateClient("TargetApiWithDistributedCache"); + return await client.GetAsync("/data"); +}); + +app.MapGet("/test/TargetApiWithHybridCache", async (IHttpClientFactory factory) => +{ + var client = factory.CreateClient("TargetApiWithHybridCache"); + return await client.GetAsync("/data"); +}); + +app.MapGet("/test/TargetApiWithCustomInterceptors", async (IHttpClientFactory factory) => { - var client = factory.CreateClient("TargetApi"); + var client = factory.CreateClient("TargetApiWithCustomInterceptors"); return await client.GetAsync("/data"); }); diff --git a/samples/SourceApi/SourceApi.csproj b/samples/SourceApi/SourceApi.csproj index 2ef3adb..3fcfefb 100644 --- a/samples/SourceApi/SourceApi.csproj +++ b/samples/SourceApi/SourceApi.csproj @@ -8,11 +8,17 @@ + + + - + + + + diff --git a/samples/SourceApi/appsettings.Development.json b/samples/SourceApi/appsettings.Development.json index 0c208ae..b10a3ef 100644 --- a/samples/SourceApi/appsettings.Development.json +++ b/samples/SourceApi/appsettings.Development.json @@ -3,6 +3,15 @@ "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "SingleLine": true, + "IncludeScopes": false, + "TimestampFormat": "dd/MM/yyyy-HH:mm:ss UTC ", + "UseUtcTimestamp": true + } } } } diff --git a/samples/TargetApi/Program.cs b/samples/TargetApi/Program.cs index b4c9fb8..70b8786 100644 --- a/samples/TargetApi/Program.cs +++ b/samples/TargetApi/Program.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); @@ -15,8 +16,10 @@ app.UseSwaggerUI(); } -app.MapPost("/auth", (UserContainer users) => +app.MapPost("/auth", (UserContainer users, ILoggerFactory loggerFactory) => { + var logger = loggerFactory.CreateLogger("TargetApi"); + logger.LogDebug("Received request on /auth endpoint"); var user = new User { AccessToken = Guid.NewGuid().ToString(), @@ -33,8 +36,11 @@ .WithName("auth") .WithOpenApi(); -app.MapPost("/refresh", (UserContainer users, [FromQuery] string refresh) => +app.MapPost("/refresh", (UserContainer users, [FromQuery] string refresh, ILoggerFactory loggerFactory) => { + var logger = loggerFactory.CreateLogger("TargetApi"); + logger.LogDebug("Received request on /refresh endpoint"); + if (string.IsNullOrEmpty(refresh)) return Results.Unauthorized(); @@ -58,8 +64,11 @@ .WithName("refresh") .WithOpenApi(); -app.MapGet("/data", (HttpRequest request, UserContainer users, [FromHeader(Name = "Authorization")] string? token = null) => +app.MapGet("/data", (HttpRequest request, UserContainer users, ILoggerFactory loggerFactory, [FromHeader(Name = "Authorization")] string? token = null) => { + var logger = loggerFactory.CreateLogger("TargetApi"); + logger.LogDebug("Received request on /data endpoint"); + if (string.IsNullOrWhiteSpace(token)) return Results.Unauthorized(); diff --git a/samples/TargetApi/appsettings.Development.json b/samples/TargetApi/appsettings.Development.json index 0c208ae..86df04e 100644 --- a/samples/TargetApi/appsettings.Development.json +++ b/samples/TargetApi/appsettings.Development.json @@ -1,8 +1,17 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.AspNetCore": "Warning" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "SingleLine": true, + "IncludeScopes": false, + "TimestampFormat": "dd/MM/yyyy-HH:mm:ss UTC ", + "UseUtcTimestamp": true + } } } } diff --git a/samples/TargetApi/appsettings.json b/samples/TargetApi/appsettings.json index 10f68b8..23039ed 100644 --- a/samples/TargetApi/appsettings.json +++ b/samples/TargetApi/appsettings.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.AspNetCore": "Warning" } }, diff --git a/src/AuthorizationInterceptor.Extensions.Abstractions/AuthorizationInterceptor.Extensions.Abstractions.csproj b/src/AuthorizationInterceptor.Extensions.Abstractions/AuthorizationInterceptor.Extensions.Abstractions.csproj index 4f78098..db3a013 100644 --- a/src/AuthorizationInterceptor.Extensions.Abstractions/AuthorizationInterceptor.Extensions.Abstractions.csproj +++ b/src/AuthorizationInterceptor.Extensions.Abstractions/AuthorizationInterceptor.Extensions.Abstractions.csproj @@ -16,10 +16,6 @@ - - - - <_Parameter1>AuthorizationInterceptor.Extensions.Abstractions.Tests diff --git a/src/AuthorizationInterceptor.Extensions.Abstractions/Handlers/IAuthenticationHandler.cs b/src/AuthorizationInterceptor.Extensions.Abstractions/Handlers/IAuthenticationHandler.cs index e96271f..19f4ddc 100644 --- a/src/AuthorizationInterceptor.Extensions.Abstractions/Handlers/IAuthenticationHandler.cs +++ b/src/AuthorizationInterceptor.Extensions.Abstractions/Handlers/IAuthenticationHandler.cs @@ -1,22 +1,19 @@ using AuthorizationInterceptor.Extensions.Abstractions.Headers; -using System.Threading; -using System.Threading.Tasks; -namespace AuthorizationInterceptor.Extensions.Abstractions.Handlers +namespace AuthorizationInterceptor.Extensions.Abstractions.Handlers; + +/// +/// Defines an abstraction interface to handle the origin of the authorization headers. +/// +public interface IAuthenticationHandler { /// - /// Defines an abstraction interface to handle the origin of the authorization headers. + /// Implementation of the authentication method. /// - public interface IAuthenticationHandler - { - /// - /// Implementation of the authentication method. - /// - /// If a previous authentication was made, the expiredHeaders will be passed; otherwise, it will be null. - /// The expiredHeaders are mostly used to refresh tokens or to re-authenticate with previous header information. - /// - /// A token to monitor for cancellation requests. - /// Returns a set of with authorized headers. - ValueTask AuthenticateAsync(AuthorizationHeaders? expiredHeaders, CancellationToken cancellationToken); - } + /// If a previous authentication was made, the expiredHeaders will be passed; otherwise, it will be null. + /// The expiredHeaders are mostly used to refresh tokens or to re-authenticate with previous header information. + /// + /// A token to monitor for cancellation requests. + /// Returns a set of with authorized headers. + ValueTask AuthenticateAsync(AuthorizationHeaders? expiredHeaders, CancellationToken cancellationToken); } diff --git a/src/AuthorizationInterceptor.Extensions.Abstractions/Headers/AuthorizationHeaders.cs b/src/AuthorizationInterceptor.Extensions.Abstractions/Headers/AuthorizationHeaders.cs index 0651536..66910d5 100644 --- a/src/AuthorizationInterceptor.Extensions.Abstractions/Headers/AuthorizationHeaders.cs +++ b/src/AuthorizationInterceptor.Extensions.Abstractions/Headers/AuthorizationHeaders.cs @@ -1,83 +1,79 @@ -using System; -using System.Collections.Generic; +namespace AuthorizationInterceptor.Extensions.Abstractions.Headers; -namespace AuthorizationInterceptor.Extensions.Abstractions.Headers +/// +/// Represents the headers returned by an authorization interceptor process or authentication method and including expiration information. +/// +public class AuthorizationHeaders : Dictionary { /// - /// Represents the headers returned by an authorization interceptor process or authentication method and including expiration information. + /// Get auth headers in an OAuth settings /// - public class AuthorizationHeaders : Dictionary - { - /// - /// Get auth headers in an OAuth settings - /// - public OAuthHeaders? OAuthHeaders { get; private set; } + public OAuthHeaders? OAuthHeaders { get; private set; } - /// - /// Gets the optional expiration timespan for the authorization data. - /// - /// - /// The duration for which the authorization data is considered valid, or null if it does not expire. - /// - public TimeSpan? ExpiresIn { get; private set; } + /// + /// Gets the optional expiration timespan for the authorization data. + /// + /// + /// The duration for which the authorization data is considered valid, or null if it does not expire. + /// + public TimeSpan? ExpiresIn { get; private set; } - /// - /// Gets the time at which the authorization data was generated/authenticated. - /// - /// - /// The representing the point in time the authorization data was generated/authenticated. - /// - public DateTimeOffset AuthenticatedAt { get; private set; } + /// + /// Gets the time at which the authorization data was generated/authenticated. + /// + /// + /// The representing the point in time the authorization data was generated/authenticated. + /// + public DateTimeOffset AuthenticatedAt { get; private set; } - /// - /// Calculate the real expiration time - /// - /// Returns the real expiration time relative to now - public TimeSpan? GetRealExpiration() - => ExpiresIn - (DateTimeOffset.UtcNow - AuthenticatedAt); + /// + /// Calculate the real expiration time + /// + /// Returns the real expiration time relative to now + public TimeSpan? GetRealExpiration() + => ExpiresIn - (DateTimeOffset.UtcNow - AuthenticatedAt); - /// - /// Initializes a new instance of the class with an optional expiration timespan. - /// - /// The optional expiration timespan for the authorization data. - public AuthorizationHeaders(TimeSpan? expiresIn = null) - { - ExpiresIn = expiresIn; - AuthenticatedAt = DateTimeOffset.UtcNow; - } + /// + /// Initializes a new instance of the class with an optional expiration timespan. + /// + /// The optional expiration timespan for the authorization data. + public AuthorizationHeaders(TimeSpan? expiresIn = null) + { + ExpiresIn = expiresIn; + AuthenticatedAt = DateTimeOffset.UtcNow; + } - /// - /// Determines if the headers are still valid based on their expiration time. - /// - /// - /// true if the headers are valid or if there is no expiration time set; otherwise, false. - /// - public bool IsHeadersValid() - { - if (ExpiresIn is null || OAuthHeaders?.ExpiresIn is null) - return true; + /// + /// Determines if the headers are still valid based on their expiration time. + /// + /// + /// true if the headers are valid or if there is no expiration time set; otherwise, false. + /// + public bool IsHeadersValid() + { + if (ExpiresIn is null || OAuthHeaders?.ExpiresIn is null) + return true; - var realExpiration = TimeSpan.FromSeconds(OAuthHeaders.ExpiresIn.Value); - return (realExpiration - (DateTimeOffset.UtcNow - AuthenticatedAt)) > TimeSpan.Zero; - } + var realExpiration = TimeSpan.FromSeconds(OAuthHeaders.ExpiresIn.Value); + return (realExpiration - (DateTimeOffset.UtcNow - AuthenticatedAt)) > TimeSpan.Zero; + } - public static implicit operator AuthorizationHeaders(OAuthHeaders headers) - { - headers.Validate(); - return new AuthorizationHeaders(headers); - } + public static implicit operator AuthorizationHeaders(OAuthHeaders headers) + { + headers.Validate(); + return new AuthorizationHeaders(headers); + } - private AuthorizationHeaders(OAuthHeaders oAuthHeaders) - { - AuthenticatedAt = DateTimeOffset.UtcNow; - OAuthHeaders = oAuthHeaders; - ExpiresIn = oAuthHeaders.GetRealTimeExpiration(); + private AuthorizationHeaders(OAuthHeaders oAuthHeaders) + { + AuthenticatedAt = DateTimeOffset.UtcNow; + OAuthHeaders = oAuthHeaders; + ExpiresIn = oAuthHeaders.GetRealTimeExpiration(); - Add("Authorization", $"{oAuthHeaders.TokenType} {oAuthHeaders.AccessToken}"); - } + Add("Authorization", $"{oAuthHeaders.TokenType} {oAuthHeaders.AccessToken}"); + } - private AuthorizationHeaders() - { - } + private AuthorizationHeaders() + { } } diff --git a/src/AuthorizationInterceptor.Extensions.Abstractions/Headers/OAuthHeaders.cs b/src/AuthorizationInterceptor.Extensions.Abstractions/Headers/OAuthHeaders.cs index fd707ad..dc352c9 100644 --- a/src/AuthorizationInterceptor.Extensions.Abstractions/Headers/OAuthHeaders.cs +++ b/src/AuthorizationInterceptor.Extensions.Abstractions/Headers/OAuthHeaders.cs @@ -1,40 +1,37 @@ -using System; +namespace AuthorizationInterceptor.Extensions.Abstractions.Headers; -namespace AuthorizationInterceptor.Extensions.Abstractions.Headers +/// +/// Represents OAuth authentication headers containing details about an access token and its properties. +/// +/// The access token issued by the OAuth authorization server. This token is used in HTTP requests to access protected resources. +/// The type of token issued. Typically, this is "bearer", indicating that the given access token should be used as a bearer token. +/// The lifetime in seconds of the access token. After this period, a new authentication will be required. Can be null if the expiration is not applicable. If you set , it will be considered in cache expiration, enabling the retrieval of expired headers to facilitate the refresh token process. +/// An optional refresh token that can be used to obtain new access tokens using the same authorization grant as described in the OAuth 2.0 specification. Can be null if not provided by the authorization server. +/// The lifetime in seconds of the refresh token. After this period, you can no longer reuse the refresh token, and a new authentication will be required. Can be null if the is also null. +public record OAuthHeaders(string AccessToken, string TokenType, double? ExpiresIn = null, string? RefreshToken = null, double? ExpiresInRefreshToken = null) { - /// - /// Represents OAuth authentication headers containing details about an access token and its properties. - /// - /// The access token issued by the OAuth authorization server. This token is used in HTTP requests to access protected resources. - /// The type of token issued. Typically, this is "bearer", indicating that the given access token should be used as a bearer token. - /// The lifetime in seconds of the access token. After this period, a new authentication will be required. Can be null if the expiration is not applicable. If you set , it will be considered in cache expiration, enabling the retrieval of expired headers to facilitate the refresh token process. - /// An optional refresh token that can be used to obtain new access tokens using the same authorization grant as described in the OAuth 2.0 specification. Can be null if not provided by the authorization server. - /// The lifetime in seconds of the refresh token. After this period, you can no longer reuse the refresh token, and a new authentication will be required. Can be null if the is also null. - public record OAuthHeaders(string AccessToken, string TokenType, double? ExpiresIn = null, string? RefreshToken = null, double? ExpiresInRefreshToken = null) + public void Validate() { - public void Validate() - { - ValidateValue(nameof(AccessToken), AccessToken); - ValidateValue(nameof(TokenType), TokenType); + ValidateValue(nameof(AccessToken), AccessToken); + ValidateValue(nameof(TokenType), TokenType); - if (ExpiresIn is <= 0) - throw new ArgumentException("ExpiresIn must be greater tha 0."); + if (ExpiresIn is <= 0) + throw new ArgumentException("ExpiresIn must be greater tha 0."); - if (!string.IsNullOrWhiteSpace(RefreshToken) && ExpiresInRefreshToken is null or <= 0) - throw new ArgumentException("ExpiresInRefreshToken must be greater than 0 if RefreshToken is provided."); - } + if (!string.IsNullOrWhiteSpace(RefreshToken) && ExpiresInRefreshToken is null or <= 0) + throw new ArgumentException("ExpiresInRefreshToken must be greater than 0 if RefreshToken is provided."); + } - public TimeSpan? GetRealTimeExpiration() - => ExpiresInRefreshToken.HasValue - ? TimeSpan.FromSeconds(ExpiresInRefreshToken.Value) - : ExpiresIn.HasValue - ? TimeSpan.FromSeconds(ExpiresIn.Value) - : null; + public TimeSpan? GetRealTimeExpiration() + => ExpiresInRefreshToken.HasValue + ? TimeSpan.FromSeconds(ExpiresInRefreshToken.Value) + : ExpiresIn.HasValue + ? TimeSpan.FromSeconds(ExpiresIn.Value) + : null; - private static void ValidateValue(string name, string value) - { - if (string.IsNullOrEmpty(value)) - throw new ArgumentException($"Property '{name}' is required."); - } + private static void ValidateValue(string name, string value) + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentException($"Property '{name}' is required."); } } diff --git a/src/AuthorizationInterceptor.Extensions.Abstractions/Interceptors/IAuthorizationInterceptor.cs b/src/AuthorizationInterceptor.Extensions.Abstractions/Interceptors/IAuthorizationInterceptor.cs index bee07c6..c855a60 100644 --- a/src/AuthorizationInterceptor.Extensions.Abstractions/Interceptors/IAuthorizationInterceptor.cs +++ b/src/AuthorizationInterceptor.Extensions.Abstractions/Interceptors/IAuthorizationInterceptor.cs @@ -1,32 +1,29 @@ -using System.Threading; -using System.Threading.Tasks; -using AuthorizationInterceptor.Extensions.Abstractions.Headers; +using AuthorizationInterceptor.Extensions.Abstractions.Headers; -namespace AuthorizationInterceptor.Extensions.Abstractions.Interceptors +namespace AuthorizationInterceptor.Extensions.Abstractions.Interceptors; + +/// +/// Defines the interface for interceptor components responsible for managing authorization headers. +/// +public interface IAuthorizationInterceptor { + /// - /// Defines the interface for interceptor components responsible for managing authorization headers. + /// Retrieves the current set of authorization headers in current interceptor. /// - public interface IAuthorizationInterceptor - { - - /// - /// Retrieves the current set of authorization headers in current interceptor. - /// - /// Name of the integration or HttpClient - /// The cancellation token. - /// - /// containing the authorization headers. - /// - ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken); + /// Name of the integration or HttpClient + /// The cancellation token. + /// + /// containing the authorization headers. + /// + ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken); - /// - /// Update the current set of authorization headers in current interceptor. - /// - /// Name of the integration or HttpClient - /// The old expired headers - /// The new valid headers - /// The cancellation token. - ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, AuthorizationHeaders? newHeaders, CancellationToken cancellationToken); - } + /// + /// Update the current set of authorization headers in current interceptor. + /// + /// Name of the integration or HttpClient + /// The old expired headers + /// The new valid headers + /// The cancellation token. + ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, AuthorizationHeaders? newHeaders, CancellationToken cancellationToken); } diff --git a/src/AuthorizationInterceptor.Extensions.Abstractions/Json/AuthorizationHeadersJsonConverter.cs b/src/AuthorizationInterceptor.Extensions.Abstractions/Json/AuthorizationHeadersJsonConverter.cs index 916fc6d..bd59ec7 100644 --- a/src/AuthorizationInterceptor.Extensions.Abstractions/Json/AuthorizationHeadersJsonConverter.cs +++ b/src/AuthorizationInterceptor.Extensions.Abstractions/Json/AuthorizationHeadersJsonConverter.cs @@ -1,131 +1,129 @@ using AuthorizationInterceptor.Extensions.Abstractions.Headers; -using System; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; -namespace AuthorizationInterceptor.Extensions.Abstractions.Json +namespace AuthorizationInterceptor.Extensions.Abstractions.Json; + +internal class AuthorizationHeadersJsonConverter : JsonConverter { - internal class AuthorizationHeadersJsonConverter : JsonConverter + public override AuthorizationHeaders Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override AuthorizationHeaders Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + var authorizationHeaders = (AuthorizationHeaders)Activator.CreateInstance(typeof(AuthorizationHeaders), true)!; + + while (reader.Read()) { - var authorizationHeaders = (AuthorizationHeaders)Activator.CreateInstance(typeof(AuthorizationHeaders), true)!; + if (reader.TokenType == JsonTokenType.EndObject) + return authorizationHeaders; - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) - return authorizationHeaders; + var propertyName = reader.GetString(); + reader.Read(); - var propertyName = reader.GetString(); - reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + continue; - if (reader.TokenType == JsonTokenType.Null) + switch (propertyName) + { + case "Headers": + SetHeaders(ref reader, authorizationHeaders); continue; - - switch (propertyName) - { - case "Headers": - SetHeaders(ref reader, authorizationHeaders); + case "ExpiresIn": + var expiresIn = reader.GetString(); + if (string.IsNullOrEmpty(expiresIn)) continue; - case "ExpiresIn": - var expiresIn = reader.GetString(); - if (string.IsNullOrEmpty(expiresIn)) - continue; - SetProperty(authorizationHeaders, "ExpiresIn", TimeSpan.Parse(expiresIn)); + SetProperty(authorizationHeaders, "ExpiresIn", TimeSpan.Parse(expiresIn)); + continue; + case "AuthenticatedAt": + var authenticatedAt = reader.GetString(); + if (string.IsNullOrEmpty(authenticatedAt)) continue; - case "AuthenticatedAt": - var authenticatedAt = reader.GetString(); - if (string.IsNullOrEmpty(authenticatedAt)) - continue; - SetProperty(authorizationHeaders, "AuthenticatedAt", DateTimeOffset.Parse(authenticatedAt)); - continue; - case "OAuthHeaders": - SetOAuthHeaders(ref reader, options, authorizationHeaders); - continue; - } + SetProperty(authorizationHeaders, "AuthenticatedAt", DateTimeOffset.Parse(authenticatedAt)); + continue; + case "OAuthHeaders": + SetOAuthHeaders(ref reader, options, authorizationHeaders); + continue; } - - throw new JsonException("Expected EndObject token."); } - public override void Write(Utf8JsonWriter writer, AuthorizationHeaders value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - - writer.WriteStartObject("Headers"); - foreach (var item in value) - writer.WriteString(item.Key, item.Value); - - writer.WriteEndObject(); + throw new JsonException("Expected EndObject token."); + } - writer.WriteString("ExpiresIn", value.ExpiresIn?.ToString()); - writer.WriteString("AuthenticatedAt", value.AuthenticatedAt); + public override void Write(Utf8JsonWriter writer, AuthorizationHeaders value, JsonSerializerOptions options) + { + writer.WriteStartObject(); - if (value.OAuthHeaders == null) - { - writer.WriteNull("OAuthHeaders"); - writer.WriteEndObject(); - return; - } + writer.WriteStartObject("Headers"); + foreach (var item in value) + writer.WriteString(item.Key, item.Value); - writer.WriteStartObject("OAuthHeaders"); - writer.WriteString("AccessToken", value.OAuthHeaders?.AccessToken); - writer.WriteString("TokenType", value.OAuthHeaders?.TokenType); - writer.WriteString("ExpiresIn", value.OAuthHeaders?.ExpiresIn?.ToString()); - writer.WriteString("RefreshToken", value.OAuthHeaders?.RefreshToken); - writer.WriteString("ExpiresInRefreshToken", value.OAuthHeaders?.ExpiresInRefreshToken?.ToString()); - writer.WriteEndObject(); + writer.WriteEndObject(); - writer.WriteEndObject(); - } + writer.WriteString("ExpiresIn", value.ExpiresIn?.ToString()); + writer.WriteString("AuthenticatedAt", value.AuthenticatedAt); - private static void SetOAuthHeaders(ref Utf8JsonReader reader, JsonSerializerOptions options, AuthorizationHeaders authorizationHeaders) + if (value.OAuthHeaders == null) { - var oAuthHeadersJson = JsonSerializer.Deserialize(ref reader, options); - - var accessToken = RequireStringProperty(oAuthHeadersJson, "AccessToken"); - if (string.IsNullOrEmpty(accessToken)) - return; - - var tokenType = RequireStringProperty(oAuthHeadersJson, "TokenType"); - if (string.IsNullOrEmpty(tokenType)) - return; - - var expiresInValue = RequireStringProperty(oAuthHeadersJson, "ExpiresIn"); - double? expiresIn = string.IsNullOrEmpty(expiresInValue) ? null : double.Parse(expiresInValue); - var refreshToken = RequireStringProperty(oAuthHeadersJson, "RefreshToken"); - var expiresInRefreshTokenString = RequireStringProperty(oAuthHeadersJson, "ExpiresInRefreshToken"); - double? expiresInRefreshToken = string.IsNullOrEmpty(expiresInRefreshTokenString) ? null : double.Parse(expiresInRefreshTokenString); - SetProperty(authorizationHeaders, "OAuthHeaders", new OAuthHeaders(accessToken, tokenType, expiresIn, refreshToken, expiresInRefreshToken)); + writer.WriteNull("OAuthHeaders"); + writer.WriteEndObject(); + return; } - private static void SetHeaders(ref Utf8JsonReader reader, AuthorizationHeaders authorizationHeaders) - { - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) - { - if (reader.TokenType != JsonTokenType.PropertyName) - continue; + writer.WriteStartObject("OAuthHeaders"); + writer.WriteString("AccessToken", value.OAuthHeaders?.AccessToken); + writer.WriteString("TokenType", value.OAuthHeaders?.TokenType); + writer.WriteString("ExpiresIn", value.OAuthHeaders?.ExpiresIn?.ToString()); + writer.WriteString("RefreshToken", value.OAuthHeaders?.RefreshToken); + writer.WriteString("ExpiresInRefreshToken", value.OAuthHeaders?.ExpiresInRefreshToken?.ToString()); + writer.WriteEndObject(); - var key = reader.GetString(); - reader.Read(); - var value = reader.GetString(); - if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) - continue; + writer.WriteEndObject(); + } - authorizationHeaders.Add(key, value); - } - } + private static void SetOAuthHeaders(ref Utf8JsonReader reader, JsonSerializerOptions options, AuthorizationHeaders authorizationHeaders) + { + var oAuthHeadersJson = JsonSerializer.Deserialize(ref reader, options); + + var accessToken = RequireStringProperty(oAuthHeadersJson, "AccessToken"); + if (string.IsNullOrEmpty(accessToken)) + return; + + var tokenType = RequireStringProperty(oAuthHeadersJson, "TokenType"); + if (string.IsNullOrEmpty(tokenType)) + return; + + var expiresInValue = RequireStringProperty(oAuthHeadersJson, "ExpiresIn"); + double? expiresIn = string.IsNullOrEmpty(expiresInValue) ? null : double.Parse(expiresInValue); + var refreshToken = RequireStringProperty(oAuthHeadersJson, "RefreshToken"); + var expiresInRefreshTokenString = RequireStringProperty(oAuthHeadersJson, "ExpiresInRefreshToken"); + double? expiresInRefreshToken = string.IsNullOrEmpty(expiresInRefreshTokenString) ? null : double.Parse(expiresInRefreshTokenString); + SetProperty(authorizationHeaders, "OAuthHeaders", new OAuthHeaders(accessToken, tokenType, expiresIn, refreshToken, expiresInRefreshToken)); + } - private static void SetProperty(AuthorizationHeaders obj, string propertyName, TValue value) + private static void SetHeaders(ref Utf8JsonReader reader, AuthorizationHeaders authorizationHeaders) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { - var propertyInfo = obj.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - propertyInfo?.SetValue(obj, value, null); + if (reader.TokenType != JsonTokenType.PropertyName) + continue; + + var key = reader.GetString(); + reader.Read(); + var value = reader.GetString(); + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) + continue; + + authorizationHeaders.Add(key, value); } + } - private static string? RequireStringProperty(JsonElement oAuthHeadersJson, string propertyName) - => !oAuthHeadersJson.TryGetProperty(propertyName, out var property) ? null : property.GetString(); + private static void SetProperty(AuthorizationHeaders obj, string propertyName, TValue value) + { + var propertyInfo = obj.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + propertyInfo?.SetValue(obj, value, null); } + + private static string? RequireStringProperty(JsonElement oAuthHeadersJson, string propertyName) + => !oAuthHeadersJson.TryGetProperty(propertyName, out var property) ? null : property.GetString(); } diff --git a/src/AuthorizationInterceptor.Extensions.Abstractions/Json/AuthorizationHeadersJsonSerializer.cs b/src/AuthorizationInterceptor.Extensions.Abstractions/Json/AuthorizationHeadersJsonSerializer.cs index 56381a8..4a592a1 100644 --- a/src/AuthorizationInterceptor.Extensions.Abstractions/Json/AuthorizationHeadersJsonSerializer.cs +++ b/src/AuthorizationInterceptor.Extensions.Abstractions/Json/AuthorizationHeadersJsonSerializer.cs @@ -1,35 +1,34 @@ using AuthorizationInterceptor.Extensions.Abstractions.Headers; using System.Text.Json; -namespace AuthorizationInterceptor.Extensions.Abstractions.Json +namespace AuthorizationInterceptor.Extensions.Abstractions.Json; + +/// +/// A Json Serializer for the class. Used to print or save in a format where data loss does not occur. +/// +public class AuthorizationHeadersJsonSerializer { /// - /// A Json Serializer for the class. Used to print or save in a format where data loss does not occur. + /// Custom converter used to print or save in a format where data loss does not occur. /// - public class AuthorizationHeadersJsonSerializer + public static JsonSerializerOptions DefaultOptions { get; } = new() { - /// - /// Custom converter used to print or save in a format where data loss does not occur. - /// - public static JsonSerializerOptions DefaultOptions { get; } = new() - { - Converters = { new AuthorizationHeadersJsonConverter() } - }; + Converters = { new AuthorizationHeadersJsonConverter() } + }; - /// - /// Serialize an object with a custom converter - /// - /// The authorization headers - /// Returns the headers as json - public static string Serialize(AuthorizationHeaders headers) - => JsonSerializer.Serialize(headers, DefaultOptions); + /// + /// Serialize an object with a custom converter + /// + /// The authorization headers + /// Returns the headers as json + public static string Serialize(AuthorizationHeaders headers) + => JsonSerializer.Serialize(headers, DefaultOptions); - /// - /// Deserialize to a new instance of with a custom converter - /// - /// - /// Returns an instance from json - public static AuthorizationHeaders? Deserialize(string json) - => JsonSerializer.Deserialize(json, DefaultOptions); - } + /// + /// Deserialize to a new instance of with a custom converter + /// + /// The json format of + /// Returns an instance from json + public static AuthorizationHeaders? Deserialize(string json) + => JsonSerializer.Deserialize(json, DefaultOptions); } diff --git a/src/AuthorizationInterceptor.Extensions.Abstractions/Options/IAuthorizationInterceptorOptions.cs b/src/AuthorizationInterceptor.Extensions.Abstractions/Options/IAuthorizationInterceptorOptions.cs index adecb93..ea16f2c 100644 --- a/src/AuthorizationInterceptor.Extensions.Abstractions/Options/IAuthorizationInterceptorOptions.cs +++ b/src/AuthorizationInterceptor.Extensions.Abstractions/Options/IAuthorizationInterceptorOptions.cs @@ -1,19 +1,17 @@ using AuthorizationInterceptor.Extensions.Abstractions.Interceptors; using Microsoft.Extensions.DependencyInjection; -using System; -namespace AuthorizationInterceptor.Extensions.Abstractions.Options +namespace AuthorizationInterceptor.Extensions.Abstractions.Options; + +/// +/// Options class that configures the authorization interceptors +/// +public interface IAuthorizationInterceptorOptions { /// - /// Options class that configures the authorization interceptors + /// Adds a custom interceptor to the interceptor sequence. Note that the interceptor addition sequence interferes with the headers query sequence. /// - public interface IAuthorizationInterceptorOptions - { - /// - /// Adds a custom interceptor to the interceptor sequence. Note that the interceptor addition sequence interferes with the headers query sequence. - /// - /// Implementation class of type - /// Access to if necessary - void UseCustomInterceptor(Func? func = null) where T : IAuthorizationInterceptor; - } + /// Implementation class of type + /// Access to if necessary + void UseCustomInterceptor(Func? func = null) where T : IAuthorizationInterceptor; } diff --git a/src/AuthorizationInterceptor.Extensions.DistributedCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs b/src/AuthorizationInterceptor.Extensions.DistributedCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs index 7696d05..894e276 100644 --- a/src/AuthorizationInterceptor.Extensions.DistributedCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs +++ b/src/AuthorizationInterceptor.Extensions.DistributedCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs @@ -1,22 +1,21 @@ using AuthorizationInterceptor.Extensions.Abstractions.Options; using AuthorizationInterceptor.Extensions.DistributedCache.Interceptors; -namespace AuthorizationInterceptor.Extensions.DistributedCache.Extensions +namespace AuthorizationInterceptor.Extensions.DistributedCache.Extensions; + +/// +/// Extension methods that Configures the authorization interceptor to use a distributed cache interceptor for +/// +public static class AuthorizationInterceptorOptionsExtensions { /// - /// Extension methods that Configures the authorization interceptor to use a distributed cache interceptor for + /// Configures the authorization interceptor to use a distributed cache interceptor. /// - public static class AuthorizationInterceptorOptionsExtensions + /// + /// + public static IAuthorizationInterceptorOptions UseDistributedCacheInterceptor(this IAuthorizationInterceptorOptions options) { - /// - /// Configures the authorization interceptor to use a distributed cache interceptor. - /// - /// - /// - public static IAuthorizationInterceptorOptions UseDistributedCacheInterceptor(this IAuthorizationInterceptorOptions options) - { - options.UseCustomInterceptor(); - return options; - } + options.UseCustomInterceptor(); + return options; } } diff --git a/src/AuthorizationInterceptor.Extensions.DistributedCache/Interceptors/DistributedCacheAuthorizationInterceptor.cs b/src/AuthorizationInterceptor.Extensions.DistributedCache/Interceptors/DistributedCacheAuthorizationInterceptor.cs index 1aa2025..ac7ee5d 100644 --- a/src/AuthorizationInterceptor.Extensions.DistributedCache/Interceptors/DistributedCacheAuthorizationInterceptor.cs +++ b/src/AuthorizationInterceptor.Extensions.DistributedCache/Interceptors/DistributedCacheAuthorizationInterceptor.cs @@ -2,37 +2,32 @@ using AuthorizationInterceptor.Extensions.Abstractions.Interceptors; using AuthorizationInterceptor.Extensions.Abstractions.Json; using Microsoft.Extensions.Caching.Distributed; -using System.Threading; -using System.Threading.Tasks; -namespace AuthorizationInterceptor.Extensions.DistributedCache.Interceptors +namespace AuthorizationInterceptor.Extensions.DistributedCache.Interceptors; + +internal sealed class DistributedCacheAuthorizationInterceptor(IDistributedCache cache) : IAuthorizationInterceptor { - internal class DistributedCacheAuthorizationInterceptor : IAuthorizationInterceptor + private const string CacheKey = "authorization_interceptor_distributed_cache_DistributedCacheAuthorizationInterceptor_{0}"; + + public async ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken) { - private readonly IDistributedCache _cache; - private const string CacheKey = "authorization_interceptor_distributed_cache_DistributedCacheAuthorizationInterceptor_{0}"; + var data = await cache.GetStringAsync(string.Format(CacheKey, name), cancellationToken); + return string.IsNullOrEmpty(data) ? null : AuthorizationHeadersJsonSerializer.Deserialize(data); + } - public DistributedCacheAuthorizationInterceptor(IDistributedCache cache) - => _cache = cache; + public async ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, AuthorizationHeaders? newHeaders, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - public async ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken) - { - var data = await _cache.GetStringAsync(string.Format(CacheKey, name), cancellationToken); - return string.IsNullOrEmpty(data) ? null : AuthorizationHeadersJsonSerializer.Deserialize(data); - } + if (newHeaders == null) + return; - public async ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, AuthorizationHeaders? newHeaders, CancellationToken cancellationToken) + var data = AuthorizationHeadersJsonSerializer.Serialize(newHeaders); + var options = new DistributedCacheEntryOptions { - if (newHeaders == null) - return; - - var data = AuthorizationHeadersJsonSerializer.Serialize(newHeaders); - var options = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = newHeaders.GetRealExpiration() - }; + AbsoluteExpirationRelativeToNow = newHeaders.GetRealExpiration() + }; - await _cache.SetStringAsync(string.Format(CacheKey, name), data, options, cancellationToken); - } + await cache.SetStringAsync(string.Format(CacheKey, name), data, options, cancellationToken); } } diff --git a/src/AuthorizationInterceptor.Extensions.HybridCache/AuthorizationInterceptor.Extensions.HybridCache.csproj b/src/AuthorizationInterceptor.Extensions.HybridCache/AuthorizationInterceptor.Extensions.HybridCache.csproj new file mode 100644 index 0000000..58fe057 --- /dev/null +++ b/src/AuthorizationInterceptor.Extensions.HybridCache/AuthorizationInterceptor.Extensions.HybridCache.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + <_Parameter1>AuthorizationInterceptor.Extensions.HybridCache.Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + diff --git a/src/AuthorizationInterceptor.Extensions.HybridCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs b/src/AuthorizationInterceptor.Extensions.HybridCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs new file mode 100644 index 0000000..c4f6361 --- /dev/null +++ b/src/AuthorizationInterceptor.Extensions.HybridCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs @@ -0,0 +1,21 @@ +using AuthorizationInterceptor.Extensions.Abstractions.Options; +using AuthorizationInterceptor.Extensions.HybridCache.Interceptors; + +namespace AuthorizationInterceptor.Extensions.HybridCache.Extensions; + +/// +/// Extension methods that Configures the authorization interceptor to use a hybrid cache interceptor for +/// +public static class AuthorizationInterceptorOptionsExtensions +{ + /// + /// Configures the authorization interceptor to use a hybrid cache interceptor. + /// + /// + /// + public static IAuthorizationInterceptorOptions UseHybridCacheInterceptor(this IAuthorizationInterceptorOptions options) + { + options.UseCustomInterceptor(); + return options; + } +} \ No newline at end of file diff --git a/src/AuthorizationInterceptor.Extensions.HybridCache/Interceptors/HybridCacheAuthorizationInterceptor.cs b/src/AuthorizationInterceptor.Extensions.HybridCache/Interceptors/HybridCacheAuthorizationInterceptor.cs new file mode 100644 index 0000000..0764e56 --- /dev/null +++ b/src/AuthorizationInterceptor.Extensions.HybridCache/Interceptors/HybridCacheAuthorizationInterceptor.cs @@ -0,0 +1,37 @@ +using AuthorizationInterceptor.Extensions.Abstractions.Headers; +using AuthorizationInterceptor.Extensions.Abstractions.Interceptors; +using AuthorizationInterceptor.Extensions.Abstractions.Json; +using Microsoft.Extensions.Caching.Hybrid; + +namespace AuthorizationInterceptor.Extensions.HybridCache.Interceptors; + +internal sealed class HybridCacheAuthorizationInterceptor(Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache) : IAuthorizationInterceptor +{ + private const string CacheKey = "authorization_interceptor_hybrid_cache_HybridCacheAuthorizationInterceptor_{0}"; + + public async ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken) + { + var data = await hybridCache.GetOrCreateAsync(string.Format(CacheKey, name), + _ => ValueTask.FromResult((string?)null), + new HybridCacheEntryOptions { Flags = HybridCacheEntryFlags.DisableLocalCacheWrite | HybridCacheEntryFlags.DisableDistributedCacheWrite }, + cancellationToken: cancellationToken); + return string.IsNullOrEmpty(data) ? null : AuthorizationHeadersJsonSerializer.Deserialize(data); + } + + public async ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, AuthorizationHeaders? newHeaders, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (newHeaders == null) + return; + + var data = AuthorizationHeadersJsonSerializer.Serialize(newHeaders); + var options = new HybridCacheEntryOptions + { + Expiration = newHeaders.GetRealExpiration(), + LocalCacheExpiration = newHeaders.GetRealExpiration(), + }; + + await hybridCache.SetAsync(string.Format(CacheKey, name), data, options, cancellationToken: cancellationToken); + } +} diff --git a/src/AuthorizationInterceptor.Extensions.HybridCache/README.md b/src/AuthorizationInterceptor.Extensions.HybridCache/README.md new file mode 100644 index 0000000..4e6cb86 --- /dev/null +++ b/src/AuthorizationInterceptor.Extensions.HybridCache/README.md @@ -0,0 +1,32 @@ +![AuthorizationInterceptor Icon](../../resources/icon.png) + +# AuthorizationInterceptor.Extensions.HybridCache +[![GithubActions](https://github.com/Adolfok3/authorizationinterceptor/actions/workflows/main.yml/badge.svg)](https://github.com/Adolfok3/AuthorizationInterceptor/actions) +[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE) +[![Codecov](https://codecov.io/github/Adolfok3/AuthorizationInterceptor/graph/badge.svg?token=PHBV20RCQK)](https://codecov.io/github/Adolfok3/AuthorizationInterceptor) +[![NuGet Version](https://img.shields.io/nuget/vpre/AuthorizationInterceptor.Extensions.Abstractions)](https://nuget.org/packages/AuthorizationInterceptor.Extensions.HybridCache) + +An interceptor for [AuthorizationInterceptor](https://github.com/Adolfok3/AuthorizationInterceptor) that uses a hybrid cache abstraction to handle authorization headers. For more information on how to configure and use Authorization Interceptor, please check the main page of [AuthorizationInterceptor](https://github.com/Adolfok3/AuthorizationInterceptor). + +### Installation +Run the following command in package manager console: +``` +PM> Install-Package AuthorizationInterceptor.Extensions.HybridCache +``` + +Or from the .NET CLI as: +``` +dotnet add package AuthorizationInterceptor.Extensions.HybridCache +``` + +### Setup +When adding Authorization Interceptor Handler, call the extension method `UseHybridCacheInterceptor`: +```csharp +services.AddHttpClient("TargetApi") + .AddAuthorizationInterceptorHandler(options => + { + options.UseHybridCacheInterceptor(); + }) +``` + +> Note: Hybrid cache is an abstraction that needs to be pre-configured using some libraries already available for .Net. Please check the official documentation of [HybridCache library in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0) \ No newline at end of file diff --git a/src/AuthorizationInterceptor.Extensions.MemoryCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs b/src/AuthorizationInterceptor.Extensions.MemoryCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs index 33dc32a..79c43db 100644 --- a/src/AuthorizationInterceptor.Extensions.MemoryCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs +++ b/src/AuthorizationInterceptor.Extensions.MemoryCache/Extensions/AuthorizationInterceptorOptionsExtensions.cs @@ -1,25 +1,23 @@ -using System; -using AuthorizationInterceptor.Extensions.Abstractions.Options; +using AuthorizationInterceptor.Extensions.Abstractions.Options; using AuthorizationInterceptor.Extensions.MemoryCache.Interceptors; using Microsoft.Extensions.DependencyInjection; -namespace AuthorizationInterceptor.Extensions.MemoryCache.Extensions +namespace AuthorizationInterceptor.Extensions.MemoryCache.Extensions; + +/// +/// Extension methods that Configures the authorization interceptor to use a memory cache interceptor for +/// +public static class AuthorizationInterceptorOptionsExtensions { /// - /// Extension methods that Configures the authorization interceptor to use a memory cache interceptor for + /// Configures the authorization interceptor to use a memory cache interceptor. /// - public static class AuthorizationInterceptorOptionsExtensions + /// + /// to configure additional dependencies if necessary + /// + public static IAuthorizationInterceptorOptions UseMemoryCacheInterceptor(this IAuthorizationInterceptorOptions options, Func? servicesFunc = null) { - /// - /// Configures the authorization interceptor to use a memory cache interceptor. - /// - /// - /// to configure additional dependencies if necessary - /// - public static IAuthorizationInterceptorOptions UseMemoryCacheInterceptor(this IAuthorizationInterceptorOptions options, Func? servicesFunc = null) - { - options.UseCustomInterceptor(servicesFunc); - return options; - } + options.UseCustomInterceptor(servicesFunc); + return options; } } diff --git a/src/AuthorizationInterceptor.Extensions.MemoryCache/Interceptors/MemoryCacheInterceptor.cs b/src/AuthorizationInterceptor.Extensions.MemoryCache/Interceptors/MemoryCacheInterceptor.cs index 14ea062..96e3aba 100644 --- a/src/AuthorizationInterceptor.Extensions.MemoryCache/Interceptors/MemoryCacheInterceptor.cs +++ b/src/AuthorizationInterceptor.Extensions.MemoryCache/Interceptors/MemoryCacheInterceptor.cs @@ -1,43 +1,34 @@ using AuthorizationInterceptor.Extensions.Abstractions.Headers; using AuthorizationInterceptor.Extensions.Abstractions.Interceptors; using Microsoft.Extensions.Caching.Memory; -using System.Threading; -using System.Threading.Tasks; -namespace AuthorizationInterceptor.Extensions.MemoryCache.Interceptors +namespace AuthorizationInterceptor.Extensions.MemoryCache.Interceptors; + +internal class MemoryCacheInterceptor(IMemoryCache memoryCache) : IAuthorizationInterceptor { - internal class MemoryCacheInterceptor : IAuthorizationInterceptor + private const string CacheKey = "authorization_interceptor_memory_cache_MemoryCacheInterceptor_{0}"; + + public ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken) { - private const string CacheKey = "authorization_interceptor_memory_cache_MemoryCacheInterceptor_{0}"; - private readonly IMemoryCache _memoryCache; + cancellationToken.ThrowIfCancellationRequested(); - public MemoryCacheInterceptor(IMemoryCache memoryCache) - { - _memoryCache = memoryCache; - } + var headers = memoryCache.Get(string.Format(CacheKey, name)); + return new(headers); + } - public ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + public ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, AuthorizationHeaders? newHeaders, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - var headers = _memoryCache.Get(string.Format(CacheKey, name)); - return new(headers); - } + if (newHeaders == null) + return ValueTask.CompletedTask; - public ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, AuthorizationHeaders? newHeaders, CancellationToken cancellationToken) + memoryCache.Set(string.Format(CacheKey, name), newHeaders, new MemoryCacheEntryOptions { - cancellationToken.ThrowIfCancellationRequested(); - - if (newHeaders == null) - return ValueTask.CompletedTask; + AbsoluteExpirationRelativeToNow = newHeaders.GetRealExpiration(), + Priority = CacheItemPriority.NeverRemove + }); - _memoryCache.Set(string.Format(CacheKey, name), newHeaders, new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = newHeaders.GetRealExpiration(), - Priority = CacheItemPriority.NeverRemove - }); - - return ValueTask.CompletedTask; - } + return ValueTask.CompletedTask; } } diff --git a/src/AuthorizationInterceptor/Extensions/HttpClientBuilderExtensions.cs b/src/AuthorizationInterceptor/Extensions/HttpClientBuilderExtensions.cs index 7354e30..b3026be 100644 --- a/src/AuthorizationInterceptor/Extensions/HttpClientBuilderExtensions.cs +++ b/src/AuthorizationInterceptor/Extensions/HttpClientBuilderExtensions.cs @@ -4,56 +4,59 @@ using AuthorizationInterceptor.Options; using AuthorizationInterceptor.Strategies; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; +using Microsoft.Extensions.Logging; -namespace AuthorizationInterceptor.Extensions +namespace AuthorizationInterceptor.Extensions; + +/// +/// Extension methods that add a new authorization interceptor handler configuration for +/// +public static class HttpClientBuilderExtensions { /// - /// Extension methods that add a new authorization interceptor handler configuration for + /// Init a new authorization interceptor handler configuration for IHttpClientBuilder /// - public static class HttpClientBuilderExtensions + /// Implementation of + /// + /// Configuration options of Authorization interceptor + /// Returns + public static IHttpClientBuilder AddAuthorizationInterceptorHandler(this IHttpClientBuilder builder, Action? options = null) + where T : class, IAuthenticationHandler { - /// - /// Init a new authorization interceptor handler configuration for IHttpClientBuilder - /// - /// Implementation of - /// - /// Configuration options of Authorization interceptor - /// Returns - public static IHttpClientBuilder AddAuthorizationInterceptorHandler(this IHttpClientBuilder builder, Action? options = null) - where T : class, IAuthenticationHandler - { - var optionsInstance = RequireOptions(options); - AddInterceptorsDependencies(builder, optionsInstance.Interceptors); - builder.Services.TryAddTransient(); - builder.AddHttpMessageHandler(provider => ActivatorUtilities.CreateInstance(provider, builder.Name, optionsInstance.UnauthenticatedPredicate, ActivatorUtilities.CreateInstance(provider, typeof(T)))); + var optionsInstance = RequireOptions(options); + builder.AddHttpMessageHandler(provider => new AuthorizationInterceptorHandler( + builder.Name, + optionsInstance.UnauthenticatedPredicate, + CreateAuthenticationHandler(provider), + CreateStrategy(provider, builder, optionsInstance.Interceptors), + provider.GetRequiredService() + )); - return builder; - } + return builder; + } - private static AuthorizationInterceptorOptions RequireOptions(Action? options) - { - var optionsInstance = new AuthorizationInterceptorOptions(); - options?.Invoke(optionsInstance); + private static AuthorizationInterceptorOptions RequireOptions(Action? options) + { + var optionsInstance = new AuthorizationInterceptorOptions(); + options?.Invoke(optionsInstance); - return optionsInstance; - } + return optionsInstance; + } + + private static T CreateAuthenticationHandler(IServiceProvider provider) where T : class, IAuthenticationHandler + => ActivatorUtilities.CreateInstance(provider); - private static void AddInterceptorsDependencies(IHttpClientBuilder builder, List<(ServiceDescriptor serviceDescriptor, Func? dependencies)> interceptors) + private static AuthorizationInterceptorStrategy CreateStrategy(IServiceProvider provider, IHttpClientBuilder builder, List<(Type interceptor, Func? dependencies)> interceptorsToBuild) + { + var interceptors = new IAuthorizationInterceptor[interceptorsToBuild.Count]; + + for (int index = 0; index < interceptorsToBuild.Count; index++) { - foreach (var (serviceDescriptor, dependencies) in interceptors) - { - if (builder.Services.Any(a => - a.ServiceType == typeof(IAuthorizationInterceptor) && a.ImplementationType == - serviceDescriptor.ImplementationType)) continue; - - builder.Services.Add(serviceDescriptor); - dependencies?.Invoke(builder.Services); - } + interceptors[index] = (IAuthorizationInterceptor)ActivatorUtilities.CreateInstance(provider, interceptorsToBuild[index].interceptor); + interceptorsToBuild[index].dependencies?.Invoke(builder.Services); } + + return new AuthorizationInterceptorStrategy(provider.GetRequiredService(), interceptors); } } diff --git a/src/AuthorizationInterceptor/Handlers/AuthorizationInterceptorHandler.cs b/src/AuthorizationInterceptor/Handlers/AuthorizationInterceptorHandler.cs index a3005f7..6c14319 100644 --- a/src/AuthorizationInterceptor/Handlers/AuthorizationInterceptorHandler.cs +++ b/src/AuthorizationInterceptor/Handlers/AuthorizationInterceptorHandler.cs @@ -1,84 +1,79 @@ using AuthorizationInterceptor.Extensions.Abstractions.Handlers; using AuthorizationInterceptor.Extensions.Abstractions.Headers; +using AuthorizationInterceptor.Log; using AuthorizationInterceptor.Strategies; using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -namespace AuthorizationInterceptor.Handlers +namespace AuthorizationInterceptor.Handlers; + +internal class AuthorizationInterceptorHandler : DelegatingHandler { - internal class AuthorizationInterceptorHandler : DelegatingHandler - { - private readonly string _name; - private readonly Func _unauthenticatedPredicate; - private readonly IAuthenticationHandler _authenticationHandler; - private readonly IAuthorizationInterceptorStrategy _strategy; - private readonly ILogger _logger; + private readonly string _name; + private readonly Func _unauthenticatedPredicate; + private readonly IAuthenticationHandler _authenticationHandler; + private readonly IAuthorizationInterceptorStrategy _strategy; + private readonly ILogger _logger; - public AuthorizationInterceptorHandler(string name, Func unauthenticatedPredicate, IAuthenticationHandler authenticationHandler, IAuthorizationInterceptorStrategy strategy, ILogger logger) - { - _name = name; - _logger = logger; - _strategy = strategy; - _authenticationHandler = authenticationHandler; - _unauthenticatedPredicate = unauthenticatedPredicate; - } + public AuthorizationInterceptorHandler(string name, Func unauthenticatedPredicate, IAuthenticationHandler authenticationHandler, IAuthorizationInterceptorStrategy strategy, ILoggerFactory loggerFactory) + { + _name = name; + _strategy = strategy; + _authenticationHandler = authenticationHandler; + _unauthenticatedPredicate = unauthenticatedPredicate; + _logger = loggerFactory.CreateLogger("AuthorizationInterceptorHandler"); + } - protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) - { - _logger.LogWarning("AuthorizationInterceptor is not available for synchronous requests. Consider using asynchronous requests!"); - return base.Send(request, cancellationToken); - } + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + _logger.LogUnavailableForSyncRequests(); + return base.Send(request, cancellationToken); + } - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - => await SendWithInterceptorAsync(request, cancellationToken); + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => await SendWithInterceptorAsync(request, cancellationToken); - private async Task SendWithInterceptorAsync(HttpRequestMessage request, CancellationToken cancellationToken) + private async Task SendWithInterceptorAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var headers = await _strategy.GetHeadersAsync(_name, _authenticationHandler, cancellationToken); + if (headers == null || !headers.Any()) { - var headers = await _strategy.GetHeadersAsync(_name, _authenticationHandler, cancellationToken); - if (headers == null || !headers.Any()) - { - LogDebug("No headers added to request with integration '{name}'", _name); - return await base.SendAsync(request, cancellationToken); - } - - request = AddHeaders(request, headers); - - var response = await base.SendAsync(request, cancellationToken); - if (!_unauthenticatedPredicate(response)) - return response; + LogDebug("No headers added to request with integration '{name}'", _name); + return await base.SendAsync(request, cancellationToken); + } - LogDebug("Caught unauthenticated predicate from response with integration '{name}'", _name); - headers = await _strategy.UpdateHeadersAsync(_name, headers, _authenticationHandler, cancellationToken); - if (headers == null || !headers.Any()) - { - LogDebug("No headers added to request with integration '{name}'", _name); - return response; - } + request = AddHeaders(request, headers); - request = AddHeaders(request, headers); + var response = await base.SendAsync(request, cancellationToken); + if (!_unauthenticatedPredicate(response)) + return response; - return await base.SendAsync(request, cancellationToken); + LogDebug("Caught unauthenticated predicate from response with integration '{name}'", _name); + headers = await _strategy.UpdateHeadersAsync(_name, headers, _authenticationHandler, cancellationToken); + if (headers == null || !headers.Any()) + { + LogDebug("No headers added to request with integration '{name}'", _name); + return response; } - private HttpRequestMessage AddHeaders(HttpRequestMessage request, AuthorizationHeaders headers) - { - foreach (var header in headers) - { - LogDebug("Adding header '{header}' to request with integration '{name}'", header.Key, _name); - request.Headers.TryAddWithoutValidation(header.Key, header.Value); - } + request = AddHeaders(request, headers); - return request; - } + return await base.SendAsync(request, cancellationToken); + } - private void LogDebug(string message, params object[] objs) + private HttpRequestMessage AddHeaders(HttpRequestMessage request, AuthorizationHeaders headers) + { + foreach (var header in headers) { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug(message, objs); + LogDebug("Adding header '{header}' to request with integration '{name}'", header.Key, _name); + request.Headers.TryAddWithoutValidation(header.Key, header.Value); } + + return request; + } + + private void LogDebug(string message, params object[] objs) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug(message, objs); } } diff --git a/src/AuthorizationInterceptor/Log/AuthorizationInterceptorLogDifinitions.cs b/src/AuthorizationInterceptor/Log/AuthorizationInterceptorLogDifinitions.cs index 5fc24b2..5480d57 100644 --- a/src/AuthorizationInterceptor/Log/AuthorizationInterceptorLogDifinitions.cs +++ b/src/AuthorizationInterceptor/Log/AuthorizationInterceptorLogDifinitions.cs @@ -4,7 +4,13 @@ namespace AuthorizationInterceptor.Log; public static partial class AuthorizationInterceptorLogDifinitions { - [LoggerMessage(Level = LogLevel.Warning, Message = "No interceptor was configured for HttpClient `{httpClientName}`. A Runtime interceptor was used instead. It is recommended to use at least the MemoryCache interceptor.")] + [LoggerMessage(EventId = 1, Level = LogLevel.Warning, Message = "No interceptor was configured for HttpClient `{httpClientName}`. A Runtime interceptor was used instead. It is recommended to use at least the MemoryCache interceptor.")] public static partial void LogNoInterceptorUsed(this ILogger logger, string httpClientName); + + [LoggerMessage(EventId = 2, Level = LogLevel.Warning, Message = "AuthorizationInterceptor is not available for synchronous requests. Consider using asynchronous requests!")] + public static partial void LogUnavailableForSyncRequests(this ILogger logger); + + [LoggerMessage(EventId = 3, Level = LogLevel.Warning, Message = "Operation canceled while getting headers from interceptor `{interceptor}` with integration `{httpClientName}`")] + public static partial void LogOperationCanceledInInterceptor(this ILogger logger, string interceptor, string httpClientName); } diff --git a/src/AuthorizationInterceptor/Options/AuthorizationInterceptorOptions.cs b/src/AuthorizationInterceptor/Options/AuthorizationInterceptorOptions.cs index 9332a41..ce75bbe 100644 --- a/src/AuthorizationInterceptor/Options/AuthorizationInterceptorOptions.cs +++ b/src/AuthorizationInterceptor/Options/AuthorizationInterceptorOptions.cs @@ -1,34 +1,28 @@ using AuthorizationInterceptor.Extensions.Abstractions.Interceptors; using AuthorizationInterceptor.Extensions.Abstractions.Options; using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; using System.Net; -using System.Net.Http; -namespace AuthorizationInterceptor.Options +namespace AuthorizationInterceptor.Options; + +/// +/// Options class that configures the authorization interceptors +/// +public class AuthorizationInterceptorOptions : IAuthorizationInterceptorOptions { + internal readonly List<(Type, Func?)> Interceptors = []; + /// - /// Options class that configures the authorization interceptors + /// Defines a predicate to know when the request was unauthenticated. If this happens, a new authorization header will be generated. Default is response with . /// - public class AuthorizationInterceptorOptions : IAuthorizationInterceptorOptions - { - internal readonly List<(ServiceDescriptor, Func?)> Interceptors = new(); - - /// - /// Defines a predicate to know when the request was unauthenticated. If this happens, a new authorization header will be generated. Default is response with . - /// - public Func UnauthenticatedPredicate { get; set; } = (response) => response.StatusCode == HttpStatusCode.Unauthorized; + public Func UnauthenticatedPredicate { get; set; } = (response) => response.StatusCode == HttpStatusCode.Unauthorized; - /// - /// Adds a custom interceptor to the interceptor sequence. Note that the interceptor addition sequence interferes with the headers query sequence. - /// - /// Implementation class of type - /// Access to if necessary - public void UseCustomInterceptor(Func? func = null) where T : IAuthorizationInterceptor - { - Interceptors.Add((new ServiceDescriptor(typeof(IAuthorizationInterceptor), typeof(T), ServiceLifetime.Transient), func)); - } - } + /// + /// Adds a custom interceptor to the interceptor sequence. Note that the interceptor addition sequence interferes with the headers query sequence. + /// + /// Implementation class of type + /// Access to if necessary inject some dependencies + public void UseCustomInterceptor(Func? func = null) where T : IAuthorizationInterceptor + => Interceptors.Add((typeof(T), func)); } diff --git a/src/AuthorizationInterceptor/Strategies/AuthorizationInterceptorStrategy.cs b/src/AuthorizationInterceptor/Strategies/AuthorizationInterceptorStrategy.cs index b51d09d..7fb41e2 100644 --- a/src/AuthorizationInterceptor/Strategies/AuthorizationInterceptorStrategy.cs +++ b/src/AuthorizationInterceptor/Strategies/AuthorizationInterceptorStrategy.cs @@ -1,121 +1,116 @@ using AuthorizationInterceptor.Extensions.Abstractions.Handlers; using AuthorizationInterceptor.Extensions.Abstractions.Headers; using AuthorizationInterceptor.Extensions.Abstractions.Interceptors; +using AuthorizationInterceptor.Log; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -namespace AuthorizationInterceptor.Strategies +namespace AuthorizationInterceptor.Strategies; + +internal class AuthorizationInterceptorStrategy : IAuthorizationInterceptorStrategy { - internal class AuthorizationInterceptorStrategy : IAuthorizationInterceptorStrategy - { - private readonly IAuthorizationInterceptor[] _interceptors; - private readonly ILogger _logger; + private readonly IAuthorizationInterceptor[] _interceptors; + private readonly ILogger _logger; - public AuthorizationInterceptorStrategy(ILogger logger, IEnumerable interceptors) - { - _logger = logger; - _interceptors = interceptors.ToArray(); - } + public AuthorizationInterceptorStrategy(ILoggerFactory loggerFactory, IAuthorizationInterceptor[] interceptors) + { + _logger = loggerFactory.CreateLogger("AuthorizationInterceptorStrategy"); + _interceptors = interceptors; + } - public async ValueTask GetHeadersAsync(string name, IAuthenticationHandler authenticationHandler, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + public async ValueTask GetHeadersAsync(string name, IAuthenticationHandler authenticationHandler, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - AuthorizationHeaders? headers = null; - int index; + AuthorizationHeaders? headers = null; + int index; - for (index = 0; index < _interceptors.Length; index++) + for (index = 0; index < _interceptors.Length; index++) + { + try { - try - { - LogDebug("Getting headers from interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - - cancellationToken.ThrowIfCancellationRequested(); + LogDebug("Getting headers from interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - headers = await _interceptors[index].GetHeadersAsync(name, cancellationToken); - if (headers == null) - continue; + cancellationToken.ThrowIfCancellationRequested(); - LogDebug("Headers found in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); + headers = await _interceptors[index].GetHeadersAsync(name, cancellationToken); + if (headers == null) + continue; - if (headers.IsHeadersValid()) - { - LogDebug("Headers still valid in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - return await UpdateHeadersInInterceptorsAsync(name, index, headers, cancellationToken); - } + LogDebug("Headers found in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - LogDebug("Headers is expired in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - break; - } - catch (OperationCanceledException) + if (headers.IsHeadersValid()) { - _logger.LogWarning("Operation canceled while getting headers from interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - throw; + LogDebug("Headers still valid in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); + return await UpdateHeadersInInterceptorsAsync(name, index, headers, cancellationToken); } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting headers from interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - } - } - return await UpdateHeadersAsync(name, headers, authenticationHandler, cancellationToken); + LogDebug("Headers is expired in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); + break; + } + catch (OperationCanceledException) + { + _logger.LogOperationCanceledInInterceptor(_interceptors[index].GetType().Name, name); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting headers from interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); + } } - public async ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, IAuthenticationHandler authenticationHandler, CancellationToken cancellationToken) - { - LogDebug("Getting new headers from AuthenticationHandler '{authenticationHandler}' with integration '{name}'", authenticationHandler.GetType().Name, name); + return await UpdateHeadersAsync(name, headers, authenticationHandler, cancellationToken); + } - cancellationToken.ThrowIfCancellationRequested(); + public async ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, IAuthenticationHandler authenticationHandler, CancellationToken cancellationToken) + { + LogDebug("Getting new headers from AuthenticationHandler '{authenticationHandler}' with integration '{name}'", authenticationHandler.GetType().Name, name); - var newHeaders = await authenticationHandler.AuthenticateAsync(expiredHeaders, cancellationToken); - if (newHeaders == null) - { - LogDebug("No new headers generated in AuthenticationHandler '{authenticationHandler}' with integration '{name}'", authenticationHandler.GetType().Name, name); - return null; - } + cancellationToken.ThrowIfCancellationRequested(); - return await UpdateHeadersInInterceptorsAsync(name, _interceptors.Length, newHeaders, cancellationToken); + var newHeaders = await authenticationHandler.AuthenticateAsync(expiredHeaders, cancellationToken); + if (newHeaders == null) + { + LogDebug("No new headers generated in AuthenticationHandler '{authenticationHandler}' with integration '{name}'", authenticationHandler.GetType().Name, name); + return null; } - private async ValueTask UpdateHeadersInInterceptorsAsync(string name, int startIndex, AuthorizationHeaders? headers, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + return await UpdateHeadersInInterceptorsAsync(name, _interceptors.Length, newHeaders, cancellationToken); + } + + private async ValueTask UpdateHeadersInInterceptorsAsync(string name, int startIndex, AuthorizationHeaders? headers, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - if (headers == null) - return null; + if (headers == null) + return null; - for (var index = startIndex - 1; index >= 0; index--) + for (var index = startIndex - 1; index >= 0; index--) + { + try { - try - { - LogDebug("Updating headers in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); + LogDebug("Updating headers in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - await _interceptors[index].UpdateHeadersAsync(name, null, headers, cancellationToken); - } - catch (OperationCanceledException) - { - _logger.LogWarning("Operation canceled while updating headers in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating headers in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); - } + await _interceptors[index].UpdateHeadersAsync(name, null, headers, cancellationToken); + } + catch (OperationCanceledException) + { + _logger.LogOperationCanceledInInterceptor(_interceptors[index].GetType().Name, name); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating headers in interceptor '{interceptor}' with integration '{name}'", _interceptors[index].GetType().Name, name); } - - return headers; } - private void LogDebug(string message, params object[] parameters) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug(message, parameters); - } + return headers; + } + + private void LogDebug(string message, params object[] parameters) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug(message, parameters); } } diff --git a/src/AuthorizationInterceptor/Strategies/IAuthorizationInterceptorStrategy.cs b/src/AuthorizationInterceptor/Strategies/IAuthorizationInterceptorStrategy.cs index 51b58f9..993ba32 100644 --- a/src/AuthorizationInterceptor/Strategies/IAuthorizationInterceptorStrategy.cs +++ b/src/AuthorizationInterceptor/Strategies/IAuthorizationInterceptorStrategy.cs @@ -1,14 +1,11 @@ using AuthorizationInterceptor.Extensions.Abstractions.Handlers; using AuthorizationInterceptor.Extensions.Abstractions.Headers; -using System.Threading; -using System.Threading.Tasks; -namespace AuthorizationInterceptor.Strategies +namespace AuthorizationInterceptor.Strategies; + +internal interface IAuthorizationInterceptorStrategy { - internal interface IAuthorizationInterceptorStrategy - { - ValueTask GetHeadersAsync(string name, IAuthenticationHandler authenticationHandler, CancellationToken cancellationToken); + ValueTask GetHeadersAsync(string name, IAuthenticationHandler authenticationHandler, CancellationToken cancellationToken); - ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, IAuthenticationHandler authenticationHandler, CancellationToken cancellationToken); - } + ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, IAuthenticationHandler authenticationHandler, CancellationToken cancellationToken); } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5e75bbe..4db61a9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,8 +3,8 @@ 13 enable enable - 5.0.0-beta - net5.0;net6.0;net7.0;net8.0;net9.0 + 5.0.0-preview-1 + net6.0;net7.0;net8.0;net9.0 Authorization Interceptor Adolfok3 MIT @@ -56,8 +56,4 @@ - - - - diff --git a/tests/AuthorizationInterceptor.Extensions.DistributedCache.Tests/Interceptors/DistributedCacheAuthorizationInterceptorTests.cs b/tests/AuthorizationInterceptor.Extensions.DistributedCache.Tests/Interceptors/DistributedCacheAuthorizationInterceptorTests.cs index 6c26ff4..2faec3d 100644 --- a/tests/AuthorizationInterceptor.Extensions.DistributedCache.Tests/Interceptors/DistributedCacheAuthorizationInterceptorTests.cs +++ b/tests/AuthorizationInterceptor.Extensions.DistributedCache.Tests/Interceptors/DistributedCacheAuthorizationInterceptorTests.cs @@ -33,7 +33,7 @@ public async Task GetHeadersAsync_ShouldGetFromCache() //Act headers = await _interceptor.GetHeadersAsync("test", CancellationToken.None); - // + //Assert headers.Should().NotBeNull(); headers!.AuthenticatedAt.Should().NotBe(DateTimeOffset.MinValue); headers.ExpiresIn.Should().Be(TimeSpan.FromMinutes(3)); diff --git a/tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/AuthorizationInterceptor.Extensions.HybridCache.Tests.csproj b/tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/AuthorizationInterceptor.Extensions.HybridCache.Tests.csproj new file mode 100644 index 0000000..ce10e66 --- /dev/null +++ b/tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/AuthorizationInterceptor.Extensions.HybridCache.Tests.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/Extensions/AuthorizationInterceptorBuilderExtensionsTests.cs b/tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/Extensions/AuthorizationInterceptorBuilderExtensionsTests.cs new file mode 100644 index 0000000..95fc825 --- /dev/null +++ b/tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/Extensions/AuthorizationInterceptorBuilderExtensionsTests.cs @@ -0,0 +1,22 @@ +using AuthorizationInterceptor.Extensions.Abstractions.Options; +using AuthorizationInterceptor.Extensions.HybridCache.Extensions; +using AuthorizationInterceptor.Extensions.HybridCache.Interceptors; +using Microsoft.Extensions.DependencyInjection; + +namespace AuthorizationInterceptor.Extensions.HybridCache.Tests.Extensions; + +public class AuthorizationInterceptorBuilderExtensionsTests +{ + [Fact] + public void UseDistributedCache_ShouldRegisterDistributedCacheDependencies_AndAddInterceptor() + { + // Arrange + var options = Substitute.For(); + + // Act + options.UseHybridCacheInterceptor(); + + // Assert + options.Received(1).UseCustomInterceptor(Arg.Any?>()); + } +} diff --git a/tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/Interceptors/HybridCacheAuthorizationInterceptorTests.cs b/tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/Interceptors/HybridCacheAuthorizationInterceptorTests.cs new file mode 100644 index 0000000..84f617a --- /dev/null +++ b/tests/AuthorizationInterceptor.Extensions.HybridCache.Tests/Interceptors/HybridCacheAuthorizationInterceptorTests.cs @@ -0,0 +1,103 @@ +using AuthorizationInterceptor.Extensions.Abstractions.Headers; +using AuthorizationInterceptor.Extensions.Abstractions.Json; +using AuthorizationInterceptor.Extensions.HybridCache.Interceptors; +using Microsoft.Extensions.Caching.Hybrid; + +namespace AuthorizationInterceptor.Extensions.HybridCache.Tests.Interceptors; + +public class HybridCacheAuthorizationInterceptorTests +{ + private const string Key = "authorization_interceptor_hybrid_cache_HybridCacheAuthorizationInterceptor_test"; + private readonly Microsoft.Extensions.Caching.Hybrid.HybridCache _hybridCache; + private readonly HybridCacheAuthorizationInterceptor _sut; + + public HybridCacheAuthorizationInterceptorTests() + { + _hybridCache = Substitute.For(); + _sut = new HybridCacheAuthorizationInterceptor(_hybridCache); + } + + [Fact] + public async Task GetHeadersAsync_ShouldGetFromCache() + { + //Arrange + var headers = new AuthorizationHeaders(TimeSpan.FromMinutes(3)) + { + { "Authorization", "Bearer token" } + }; + var cache = AuthorizationHeadersJsonSerializer.Serialize(headers); + _hybridCache.GetOrCreateAsync(Key, Arg.Any>>(), Arg.Any(), Arg.Any>(), Arg.Any()) + .Returns(ValueTask.FromResult(cache)); + + //Act + var result = await _sut.GetHeadersAsync("test", CancellationToken.None); + + //Assert + result.Should().NotBeNull(); + result!.AuthenticatedAt.Should().NotBe(DateTimeOffset.MinValue); + result.ExpiresIn.Should().Be(TimeSpan.FromMinutes(3)); + result.Should().Contain(a => a.Key == "Authorization" && a.Value == "Bearer token"); + await _hybridCache.Received(1).GetOrCreateAsync(Key, Arg.Any>>(), Arg.Any(), Arg.Any>(), Arg.Any()); + } + + [Fact] + public async Task GetHeadersAsync_WithOAuth_ShouldGetFromCache() + { + //Arrange + AuthorizationHeaders? headers = new OAuthHeaders("token", "type", 123, "refresh", 12345); + var cache = AuthorizationHeadersJsonSerializer.Serialize(headers); + _hybridCache.GetOrCreateAsync(Key, Arg.Any>>(), Arg.Any(), Arg.Any>(), Arg.Any()) + .Returns(ValueTask.FromResult(cache)); + + //Act + headers = await _sut.GetHeadersAsync("test", CancellationToken.None); + + //Assert + headers.Should().NotBeNull(); + headers.Should().NotBeEmpty(); + headers!.AuthenticatedAt.Should().NotBe(DateTimeOffset.MinValue); + headers.ExpiresIn.Should().Be(TimeSpan.FromSeconds(12345)); + headers.Should().Contain(a => a.Key == "Authorization" && a.Value == "type token"); + headers.OAuthHeaders.Should().NotBeNull(); + await _hybridCache.Received(1).GetOrCreateAsync(Key, Arg.Any>>(), Arg.Any(), Arg.Any>(), Arg.Any()); + } + + [Fact] + public async Task GetHeadersAsync_ShouldGetFromCache_AndReturnNull() + { + //Act + var headers = await _sut.GetHeadersAsync("test", CancellationToken.None); + + //Assert + headers.Should().BeNull(); + await _hybridCache.Received(1).GetOrCreateAsync(Key, Arg.Any>>(), Arg.Any(), Arg.Any>(), Arg.Any()); + } + + [Fact] + public async Task UpdateHeadersAsync_ShouldUpdateSuccessfully() + { + //Arrange + var headers = new AuthorizationHeaders(TimeSpan.FromMinutes(3)) + { + { "Authorization", "Bearer token" } + }; + + //Act + var act = async () => await _sut.UpdateHeadersAsync("test", null, headers, CancellationToken.None); + + //Assert + await act.Should().NotThrowAsync(); + await _hybridCache.Received(1).SetAsync(Key, Arg.Any(), Arg.Any(), null, Arg.Any()); + } + + [Fact] + public async Task UpdateHeadersAsync_WithNullHeaders_ShouldNotUpdate() + { + //Act + var act = async () => await _sut.UpdateHeadersAsync("test", null, null, CancellationToken.None); + + //Assert + await act.Should().NotThrowAsync(); + await _hybridCache.Received(0).SetAsync(Key, Arg.Any(), Arg.Any(), null, Arg.Any()); + } +} diff --git a/tests/AuthorizationInterceptor.Tests/Handlers/AuthorizationInterceptorHandlerTests.cs b/tests/AuthorizationInterceptor.Tests/Handlers/AuthorizationInterceptorHandlerTests.cs index d6a38b5..60d9126 100644 --- a/tests/AuthorizationInterceptor.Tests/Handlers/AuthorizationInterceptorHandlerTests.cs +++ b/tests/AuthorizationInterceptor.Tests/Handlers/AuthorizationInterceptorHandlerTests.cs @@ -1,6 +1,7 @@ using AuthorizationInterceptor.Extensions.Abstractions.Handlers; using AuthorizationInterceptor.Extensions.Abstractions.Headers; using AuthorizationInterceptor.Handlers; +using AuthorizationInterceptor.Log; using AuthorizationInterceptor.Strategies; using AuthorizationInterceptor.Tests.Utils; using Microsoft.Extensions.Logging; @@ -9,20 +10,24 @@ namespace AuthorizationInterceptor.Tests.Handlers; public class AuthorizationInterceptorHandlerTests { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IAuthorizationInterceptorStrategy _strategy; private readonly IAuthenticationHandler _authenticationHandler; private readonly HttpClient _client; public AuthorizationInterceptorHandlerTests() { - _logger = Substitute.For>(); + _logger = Substitute.For(); _logger.IsEnabled(LogLevel.Debug).Returns(true); + var loggerFactory = Substitute.For(); + loggerFactory.CreateLogger("AuthorizationInterceptorHandler").Returns(_logger); + + Func func = f => f.StatusCode == System.Net.HttpStatusCode.Unauthorized; _strategy = Substitute.For(); _authenticationHandler = Substitute.For(); - var handler = new AuthorizationInterceptorHandler("test", func, _authenticationHandler, _strategy, _logger); + var handler = new AuthorizationInterceptorHandler("test", func, _authenticationHandler, _strategy, loggerFactory); handler.InnerHandler = new MockAuthorizationInterceptorHandler(); _client = new HttpClient(handler); } @@ -37,7 +42,7 @@ public void SendSync_ShouldLogWarning() _client.Send(request); //Assert - _logger.Received(1).LogWarning("AuthorizationInterceptor is not available for synchronous requests. Consider using asynchronous requests!"); + _logger.Received(1).LogUnavailableForSyncRequests(); } [Fact] diff --git a/tests/AuthorizationInterceptor.Tests/Strategies/AuthorizationInterceptorStrategyTests.cs b/tests/AuthorizationInterceptor.Tests/Strategies/AuthorizationInterceptorStrategyTests.cs index 0302c8b..ef538fd 100644 --- a/tests/AuthorizationInterceptor.Tests/Strategies/AuthorizationInterceptorStrategyTests.cs +++ b/tests/AuthorizationInterceptor.Tests/Strategies/AuthorizationInterceptorStrategyTests.cs @@ -14,16 +14,18 @@ public class AuthorizationInterceptorStrategyTests private readonly IAuthorizationInterceptor _interceptor2; private readonly IAuthorizationInterceptor _interceptor3; private IAuthorizationInterceptorStrategy _stategy; - private readonly ILogger _logger; + private readonly ILogger _logger; public AuthorizationInterceptorStrategyTests() { _interceptor1 = Substitute.For(); _interceptor2 = Substitute.For(); _interceptor3 = Substitute.For(); - _logger = Substitute.For>(); + _logger = Substitute.For(); _logger.IsEnabled(LogLevel.Debug).Returns(true); - _stategy = new AuthorizationInterceptorStrategy(_logger, [_interceptor1, _interceptor2, _interceptor3]); + var loggerFactory = Substitute.For(); + loggerFactory.CreateLogger("AuthorizationInterceptorStrategy").Returns(_logger); + _stategy = new AuthorizationInterceptorStrategy(loggerFactory, [_interceptor1, _interceptor2, _interceptor3]); } [Fact] diff --git a/tests/AuthorizationInterceptor.Tests/Utils/MockAuthorizationInterceptor.cs b/tests/AuthorizationInterceptor.Tests/Utils/MockAuthorizationInterceptor.cs index 102c16f..42deb79 100644 --- a/tests/AuthorizationInterceptor.Tests/Utils/MockAuthorizationInterceptor.cs +++ b/tests/AuthorizationInterceptor.Tests/Utils/MockAuthorizationInterceptor.cs @@ -1,18 +1,17 @@ using AuthorizationInterceptor.Extensions.Abstractions.Headers; using AuthorizationInterceptor.Extensions.Abstractions.Interceptors; -namespace AuthorizationInterceptor.Tests.Utils +namespace AuthorizationInterceptor.Tests.Utils; + +public class MockAuthorizationInterceptor : IAuthorizationInterceptor { - public class MockAuthorizationInterceptor : IAuthorizationInterceptor + public ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken) { - public ValueTask GetHeadersAsync(string name, CancellationToken cancellationToken) - { - return ValueTask.FromResult(null); - } + return ValueTask.FromResult(null); + } - public ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, AuthorizationHeaders? newHeaders, CancellationToken cancellationToken) - { - return ValueTask.CompletedTask; - } + public ValueTask UpdateHeadersAsync(string name, AuthorizationHeaders? expiredHeaders, AuthorizationHeaders? newHeaders, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; } } diff --git a/tests/AuthorizationInterceptor.Tests/Utils/MockAuthorizationInterceptorAuthenticationHandler.cs b/tests/AuthorizationInterceptor.Tests/Utils/MockAuthorizationInterceptorAuthenticationHandler.cs index 0388bc8..cd63fb8 100644 --- a/tests/AuthorizationInterceptor.Tests/Utils/MockAuthorizationInterceptorAuthenticationHandler.cs +++ b/tests/AuthorizationInterceptor.Tests/Utils/MockAuthorizationInterceptorAuthenticationHandler.cs @@ -1,13 +1,12 @@ using AuthorizationInterceptor.Extensions.Abstractions.Handlers; using AuthorizationInterceptor.Extensions.Abstractions.Headers; -namespace AuthorizationInterceptor.Tests.Utils +namespace AuthorizationInterceptor.Tests.Utils; + +public class MockAuthorizationInterceptorAuthenticationHandler : IAuthenticationHandler { - public class MockAuthorizationInterceptorAuthenticationHandler : IAuthenticationHandler + public ValueTask AuthenticateAsync(AuthorizationHeaders? expiredHeaders, CancellationToken cancellationToken) { - public ValueTask AuthenticateAsync(AuthorizationHeaders? expiredHeaders, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } }