From 6259a6e6bfa1092da4945386aaa3d622609f668f Mon Sep 17 00:00:00 2001 From: Mihir Dilip Date: Wed, 22 Nov 2023 23:01:04 +0000 Subject: [PATCH 1/2] - net8.0 support added - Sample project for net8.0 added - ApiKeySamplesClient.http file added for testing sample projects - Readme updated --- LICENSE | 2 +- README.md | 3 +- samples/ApiKeySamplesClient.http | 27 +++ .../Controllers/ValuesController.cs | 36 ++++ samples/SampleWebApi_8_0/Program.cs | 162 ++++++++++++++++++ .../Properties/launchSettings.json | 41 +++++ .../SampleWebApi_8_0/SampleWebApi_8_0.csproj | 20 +++ .../appsettings.Development.json | 8 + samples/SampleWebApi_8_0/appsettings.json | 9 + src/AspNetCore.Authentication.ApiKey.sln | 11 +- .../ApiKeyHandlerBase.cs | 13 +- .../ApiKeyInAuthorizationHeaderHandler.cs | 15 +- .../ApiKeyInHeaderHandler.cs | 13 +- .../ApiKeyInHeaderOrQueryParamsHandler.cs | 14 +- .../ApiKeyInQueryParamsHandler.cs | 11 +- .../AspNetCore.Authentication.ApiKey.csproj | 23 +-- .../CompatibilitySuppressions.xml | 31 ++++ .../GlobalSuppressions.cs | 9 + ...NetCore.Authentication.ApiKey.Tests.csproj | 51 ++++-- 19 files changed, 458 insertions(+), 41 deletions(-) create mode 100644 samples/ApiKeySamplesClient.http create mode 100644 samples/SampleWebApi_8_0/Controllers/ValuesController.cs create mode 100644 samples/SampleWebApi_8_0/Program.cs create mode 100644 samples/SampleWebApi_8_0/Properties/launchSettings.json create mode 100644 samples/SampleWebApi_8_0/SampleWebApi_8_0.csproj create mode 100644 samples/SampleWebApi_8_0/appsettings.Development.json create mode 100644 samples/SampleWebApi_8_0/appsettings.json create mode 100644 src/AspNetCore.Authentication.ApiKey/GlobalSuppressions.cs diff --git a/LICENSE b/LICENSE index f7116cf..749d2ed 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Mihir Dilip +Copyright (c) 2023 Mihir Dilip Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3b6034d..c873554 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Easy to use and very light weight Microsoft style API Key Authentication Impleme ## .NET (Core) Frameworks Supported .NET Framework 4.6.1 and/or NetStandard 2.0 onwards -Multi targeted: net7.0; net6.0; net5.0; netcoreapp3.1; netcoreapp3.0; netstandard2.0; net461 +Multi targeted: net8.0; net7.0; net6.0; net5.0; netcoreapp3.1; netcoreapp3.0; netstandard2.0; net461
@@ -380,6 +380,7 @@ public void ConfigureServices(IServiceCollection services) ## Release Notes | Version |           Notes | |---------|-------| +|8.0.0 | | |7.0.0 | | |6.0.1 | | |5.1.0 | | diff --git a/samples/ApiKeySamplesClient.http b/samples/ApiKeySamplesClient.http new file mode 100644 index 0000000..c0ae6ed --- /dev/null +++ b/samples/ApiKeySamplesClient.http @@ -0,0 +1,27 @@ +@HostAddress = https://localhost:44304 +@ApiKey = Key1 + +# Get Values No Auth +GET {{HostAddress}}/api/values +Accept: application/json + +### + +# Get Values +GET {{HostAddress}}/api/values +Accept: application/json +X-API-Key: {{ApiKey}} + +### + +# Claims +GET {{HostAddress}}/api/values/claims +Accept: application/json +X-API-Key: {{ApiKey}} + +### + +# Forbid +GET {{HostAddress}}/api/values/forbid +Accept: application/json +X-API-Key: {{ApiKey}} \ No newline at end of file diff --git a/samples/SampleWebApi_8_0/Controllers/ValuesController.cs b/samples/SampleWebApi_8_0/Controllers/ValuesController.cs new file mode 100644 index 0000000..6e18acf --- /dev/null +++ b/samples/SampleWebApi_8_0/Controllers/ValuesController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Text; + +namespace SampleWebApi_8_0.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ValuesController : ControllerBase + { + // GET api/values + [AllowAnonymous] + [HttpGet] + public ActionResult> Get() + { + return new string[] { "value1", "value2" }; + } + + [HttpGet("claims")] + public ActionResult Claims() + { + var sb = new StringBuilder(); + foreach (var claim in User.Claims) + { + sb.AppendLine($"{claim.Type}: {claim.Value}"); + } + return sb.ToString(); + } + + [HttpGet("forbid")] + public new IActionResult Forbid() + { + return base.Forbid(); + } + } +} diff --git a/samples/SampleWebApi_8_0/Program.cs b/samples/SampleWebApi_8_0/Program.cs new file mode 100644 index 0000000..3ddcf4c --- /dev/null +++ b/samples/SampleWebApi_8_0/Program.cs @@ -0,0 +1,162 @@ +using AspNetCore.Authentication.ApiKey; +using Microsoft.AspNetCore.Authorization; +using SampleWebApi.Repositories; +using SampleWebApi.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Add User repository to the dependency container. +builder.Services.AddTransient(); + +// Add the ApiKey scheme authentication here.. +// It requires Realm to be set in the options if SuppressWWWAuthenticateHeader is not set. +// If an implementation of IApiKeyProvider interface is registered in the dependency register as well as OnValidateKey delegete on options.Events is also set then this delegate will be used instead of an implementation of IApiKeyProvider. +builder.Services.AddAuthentication(ApiKeyDefaults.AuthenticationScheme) + + // The below AddApiKeyInHeaderOrQueryParams without type parameter will require OnValidateKey delegete on options.Events to be set unless an implementation of IApiKeyProvider interface is registered in the dependency register. + // Please note if OnValidateKey delegete on options.Events is also set then this delegate will be used instead of ApiKeyProvider.* + //.AddApiKeyInHeaderOrQueryParams(options => + + // The below AddApiKeyInHeaderOrQueryParams with type parameter will add the ApiKeyProvider to the dependency register. + // Please note if OnValidateKey delegete on options.Events is also set then this delegate will be used instead of ApiKeyProvider. + .AddApiKeyInHeaderOrQueryParams(options => + { + options.Realm = "Sample Web API"; + options.KeyName = "X-API-KEY"; + + //// Optional option to suppress the browser login dialog for ajax calls. + //options.SuppressWWWAuthenticateHeader = true; + + //// Optional option to ignore extra check of ApiKey string after it is validated. + //options.ForLegacyIgnoreExtraValidatedApiKeyCheck = true; + + //// Optional option to ignore authentication if AllowAnonumous metadata/filter attribute is added to an endpoint. + //options.IgnoreAuthenticationIfAllowAnonymous = true; + + //// Optional events to override the ApiKey original logic with custom logic. + //// Only use this if you know what you are doing at your own risk. Any of the events can be assigned. + options.Events = new ApiKeyEvents + { + + //// A delegate assigned to this property will be invoked just before validating the api key. + //OnValidateKey = async (context) => + //{ + // // custom code to handle the api key, create principal and call Success method on context. + // var apiKeyRepository = context.HttpContext.RequestServices.GetRequiredService(); + // var apiKey = await apiKeyRepository.GetApiKeyAsync(context.ApiKey); + // var isValid = apiKey != null && apiKey.Key.Equals(context.ApiKey, StringComparison.OrdinalIgnoreCase); + // if (isValid) + // { + // context.Response.Headers.Add("ValidationCustomHeader", "From OnValidateKey"); + // var claims = new[] + // { + // new Claim(ClaimTypes.NameIdentifier, apiKey.OwnerName, ClaimValueTypes.String, context.Options.ClaimsIssuer), + // new Claim(ClaimTypes.Name, apiKey.OwnerName, ClaimValueTypes.String, context.Options.ClaimsIssuer), + // new Claim("CustomClaimType", "Custom Claim Value - from OnValidateKey") + // }; + // context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + // context.Success(); + // } + // else + // { + // context.NoResult(); + // } + //}, + + //// A delegate assigned to this property will be invoked just before validating the api key. + //// NOTE: Same as above delegate but slightly different implementation which will give same result. + //OnValidateKey = async (context) => + //{ + // // custom code to handle the api key, create principal and call Success method on context. + // var apiKeyRepository = context.HttpContext.RequestServices.GetRequiredService(); + // var apiKey = await apiKeyRepository.GetApiKeyAsync(context.ApiKey); + // var isValid = apiKey != null && apiKey.Key.Equals(context.ApiKey, StringComparison.OrdinalIgnoreCase); + // if (isValid) + // { + // context.Response.Headers.Add("ValidationCustomHeader", "From OnValidateKey"); + // var claims = new[] + // { + // new Claim("CustomClaimType", "Custom Claim Value - from OnValidateKey") + // }; + // context.ValidationSucceeded(apiKey.OwnerName, claims); // claims are optional + // } + // else + // { + // context.ValidationFailed(); + // } + //}, + + //// A delegate assigned to this property will be invoked before a challenge is sent back to the caller when handling unauthorized response. + //OnHandleChallenge = async (context) => + //{ + // // custom code to handle authentication challenge unauthorized response. + // context.Response.StatusCode = StatusCodes.Status401Unauthorized; + // context.Response.Headers.Add("ChallengeCustomHeader", "From OnHandleChallenge"); + // await context.Response.WriteAsync("{\"CustomBody\":\"From OnHandleChallenge\"}"); + // context.Handled(); // important! do not forget to call this method at the end. + //}, + + //// A delegate assigned to this property will be invoked if Authorization fails and results in a Forbidden response. + //OnHandleForbidden = async (context) => + //{ + // // custom code to handle forbidden response. + // context.Response.StatusCode = StatusCodes.Status403Forbidden; + // context.Response.Headers.Add("ForbidCustomHeader", "From OnHandleForbidden"); + // await context.Response.WriteAsync("{\"CustomBody\":\"From OnHandleForbidden\"}"); + // context.Handled(); // important! do not forget to call this method at the end. + //}, + + //// A delegate assigned to this property will be invoked when the authentication succeeds. It will not be called if OnValidateKey delegate is assigned. + //// It can be used for adding claims, headers, etc to the response. + //OnAuthenticationSucceeded = (context) => + //{ + // //custom code to add extra bits to the success response. + // context.Response.Headers.Add("SuccessCustomHeader", "From OnAuthenticationSucceeded"); + // var customClaims = new List + // { + // new Claim("CustomClaimType", "Custom Claim Value - from OnAuthenticationSucceeded") + // }; + // context.AddClaims(customClaims); + // //or can add like this - context.Principal.AddIdentity(new ClaimsIdentity(customClaims)); + // return Task.CompletedTask; + //}, + + //// A delegate assigned to this property will be invoked when the authentication fails. + //OnAuthenticationFailed = (context) => + //{ + // // custom code to handle failed authentication. + // context.Fail("Failed to authenticate"); + // return Task.CompletedTask; + //} + + }; + }); + +builder.Services.AddControllers(options => +{ + // ALWAYS USE HTTPS (SSL) protocol in production when using ApiKey authentication. + //options.Filters.Add(); + +}); //.AddXmlSerializerFormatters() // To enable XML along with JSON; + +// All the requests will need to be authorized. +// Alternatively, add [Authorize] attribute to Controller or Action Method where necessary. +builder.Services.AddAuthorizationBuilder() + .AddFallbackPolicy( + "FallbackPolicy", + new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build() + ); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.UseHttpsRedirection(); + +app.UseAuthentication(); // NOTE: DEFAULT TEMPLATE DOES NOT HAVE THIS, THIS LINE IS REQUIRED AND HAS TO BE ADDED!!! + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/samples/SampleWebApi_8_0/Properties/launchSettings.json b/samples/SampleWebApi_8_0/Properties/launchSettings.json new file mode 100644 index 0000000..18b1392 --- /dev/null +++ b/samples/SampleWebApi_8_0/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3920", + "sslPort": 44304 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "api/values", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "api/values", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/SampleWebApi_8_0/SampleWebApi_8_0.csproj b/samples/SampleWebApi_8_0/SampleWebApi_8_0.csproj new file mode 100644 index 0000000..637f4a2 --- /dev/null +++ b/samples/SampleWebApi_8_0/SampleWebApi_8_0.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + diff --git a/samples/SampleWebApi_8_0/appsettings.Development.json b/samples/SampleWebApi_8_0/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/SampleWebApi_8_0/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/SampleWebApi_8_0/appsettings.json b/samples/SampleWebApi_8_0/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/SampleWebApi_8_0/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/AspNetCore.Authentication.ApiKey.sln b/src/AspNetCore.Authentication.ApiKey.sln index 43ae4ed..1ed81b5 100644 --- a/src/AspNetCore.Authentication.ApiKey.sln +++ b/src/AspNetCore.Authentication.ApiKey.sln @@ -14,6 +14,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{3C777BBB-7464-43FB-A046-EA465791AB0C}" ProjectSection(SolutionItems) = preProject + ..\samples\ApiKeySamplesClient.http = ..\samples\ApiKeySamplesClient.http ..\samples\ApiKeySamplesClient.postman_collection.json = ..\samples\ApiKeySamplesClient.postman_collection.json EndProjectSection EndProject @@ -33,7 +34,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A15FB7AB-5 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWebApi_6_0", "..\samples\SampleWebApi_6_0\SampleWebApi_6_0.csproj", "{D1056CEF-550A-4EEB-B12B-1093DD3125AC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWebApi_7_0", "..\samples\SampleWebApi_7_0\SampleWebApi_7_0.csproj", "{BE461B80-9EB5-41AF-9C18-39740A7BD057}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWebApi_7_0", "..\samples\SampleWebApi_7_0\SampleWebApi_7_0.csproj", "{BE461B80-9EB5-41AF-9C18-39740A7BD057}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWebApi_8_0", "..\samples\SampleWebApi_8_0\SampleWebApi_8_0.csproj", "{5F0968BA-062C-4C7F-B40C-B1E1641FCEF6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -73,6 +76,10 @@ Global {BE461B80-9EB5-41AF-9C18-39740A7BD057}.Debug|Any CPU.Build.0 = Debug|Any CPU {BE461B80-9EB5-41AF-9C18-39740A7BD057}.Release|Any CPU.ActiveCfg = Release|Any CPU {BE461B80-9EB5-41AF-9C18-39740A7BD057}.Release|Any CPU.Build.0 = Release|Any CPU + {5F0968BA-062C-4C7F-B40C-B1E1641FCEF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F0968BA-062C-4C7F-B40C-B1E1641FCEF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F0968BA-062C-4C7F-B40C-B1E1641FCEF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F0968BA-062C-4C7F-B40C-B1E1641FCEF6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -86,12 +93,14 @@ Global {EA2A367F-2D2D-4C20-8C32-C19F67E73187} = {A15FB7AB-5B7A-4428-BEBA-32DEE3C88C39} {D1056CEF-550A-4EEB-B12B-1093DD3125AC} = {3C777BBB-7464-43FB-A046-EA465791AB0C} {BE461B80-9EB5-41AF-9C18-39740A7BD057} = {3C777BBB-7464-43FB-A046-EA465791AB0C} + {5F0968BA-062C-4C7F-B40C-B1E1641FCEF6} = {3C777BBB-7464-43FB-A046-EA465791AB0C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {70815049-1680-480A-BF5A-00536D6C9C20} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{1e1e202b-efb2-40fd-8271-659f36084916}*SharedItemsImports = 5 + ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{5f0968ba-062c-4c7f-b40c-b1e1641fcef6}*SharedItemsImports = 5 ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{be461b80-9eb5-41af-9c18-39740a7bd057}*SharedItemsImports = 5 ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{cabeeeae-3974-4cc4-97f1-18c8d2188daf}*SharedItemsImports = 5 ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{d1056cef-550a-4eeb-b12b-1093dd3125ac}*SharedItemsImports = 5 diff --git a/src/AspNetCore.Authentication.ApiKey/ApiKeyHandlerBase.cs b/src/AspNetCore.Authentication.ApiKey/ApiKeyHandlerBase.cs index dc559cf..9d8e785 100644 --- a/src/AspNetCore.Authentication.ApiKey/ApiKeyHandlerBase.cs +++ b/src/AspNetCore.Authentication.ApiKey/ApiKeyHandlerBase.cs @@ -18,12 +18,21 @@ namespace AspNetCore.Authentication.ApiKey /// public abstract class ApiKeyHandlerBase : AuthenticationHandler { - protected ApiKeyHandlerBase(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + +#if NET8_0_OR_GREATER + protected ApiKeyHandlerBase(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + [Obsolete("ISystemClock is obsolete, use TimeProvider on AuthenticationSchemeOptions instead.")] +#endif + protected ApiKeyHandlerBase(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } - private string Challenge => $"{GetWwwAuthenticateSchemeName()} realm=\"{Options.Realm}\", charset=\"UTF-8\", in=\"{GetWwwAuthenticateInParameter()}\", key_name=\"{Options.KeyName}\""; + private string Challenge => $"{GetWwwAuthenticateSchemeName()} realm=\"{Options.Realm}\", charset=\"UTF-8\", in=\"{GetWwwAuthenticateInParameter()}\", key_name=\"{Options.KeyName}\""; /// /// Get or set . diff --git a/src/AspNetCore.Authentication.ApiKey/ApiKeyInAuthorizationHeaderHandler.cs b/src/AspNetCore.Authentication.ApiKey/ApiKeyInAuthorizationHeaderHandler.cs index ca4b08e..7997507 100644 --- a/src/AspNetCore.Authentication.ApiKey/ApiKeyInAuthorizationHeaderHandler.cs +++ b/src/AspNetCore.Authentication.ApiKey/ApiKeyInAuthorizationHeaderHandler.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using System; using System.Net.Http.Headers; @@ -14,15 +15,23 @@ namespace AspNetCore.Authentication.ApiKey { public class ApiKeyInAuthorizationHeaderHandler : ApiKeyHandlerBase { - public ApiKeyInAuthorizationHeaderHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) +#if NET8_0_OR_GREATER + protected ApiKeyInAuthorizationHeaderHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + [Obsolete("ISystemClock is obsolete, use TimeProvider on AuthenticationSchemeOptions instead.")] +#endif + public ApiKeyInAuthorizationHeaderHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task ParseApiKeyAsync() { - if (Request.Headers.ContainsKey(HeaderNames.Authorization) - && AuthenticationHeaderValue.TryParse(Request.Headers[HeaderNames.Authorization], out var headerValue) + if (Request.Headers.TryGetValue(HeaderNames.Authorization, out StringValues value) + && AuthenticationHeaderValue.TryParse(value, out var headerValue) && (headerValue.Scheme.Equals(Scheme.Name, StringComparison.OrdinalIgnoreCase) || headerValue.Scheme.Equals(Options.KeyName, StringComparison.OrdinalIgnoreCase) ) diff --git a/src/AspNetCore.Authentication.ApiKey/ApiKeyInHeaderHandler.cs b/src/AspNetCore.Authentication.ApiKey/ApiKeyInHeaderHandler.cs index 6f0b9ff..794f29f 100644 --- a/src/AspNetCore.Authentication.ApiKey/ApiKeyInHeaderHandler.cs +++ b/src/AspNetCore.Authentication.ApiKey/ApiKeyInHeaderHandler.cs @@ -4,15 +4,24 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System; using System.Linq; using System.Text.Encodings.Web; using System.Threading.Tasks; namespace AspNetCore.Authentication.ApiKey { - public class ApiKeyInHeaderHandler : ApiKeyHandlerBase + public class ApiKeyInHeaderHandler : ApiKeyHandlerBase { - public ApiKeyInHeaderHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) +#if NET8_0_OR_GREATER + protected ApiKeyInHeaderHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + [Obsolete("ISystemClock is obsolete, use TimeProvider on AuthenticationSchemeOptions instead.")] +#endif + public ApiKeyInHeaderHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } diff --git a/src/AspNetCore.Authentication.ApiKey/ApiKeyInHeaderOrQueryParamsHandler.cs b/src/AspNetCore.Authentication.ApiKey/ApiKeyInHeaderOrQueryParamsHandler.cs index f5f50fd..29777c3 100644 --- a/src/AspNetCore.Authentication.ApiKey/ApiKeyInHeaderOrQueryParamsHandler.cs +++ b/src/AspNetCore.Authentication.ApiKey/ApiKeyInHeaderOrQueryParamsHandler.cs @@ -15,7 +15,15 @@ namespace AspNetCore.Authentication.ApiKey { public class ApiKeyInHeaderOrQueryParamsHandler : ApiKeyHandlerBase { - public ApiKeyInHeaderOrQueryParamsHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) +#if NET8_0_OR_GREATER + protected ApiKeyInHeaderOrQueryParamsHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + [Obsolete("ISystemClock is obsolete, use TimeProvider on AuthenticationSchemeOptions instead.")] +#endif + public ApiKeyInHeaderOrQueryParamsHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } @@ -35,8 +43,8 @@ protected override Task ParseApiKeyAsync() } // No ApiKey query parameter or header found then try Authorization header - if (Request.Headers.ContainsKey(HeaderNames.Authorization) - && AuthenticationHeaderValue.TryParse(Request.Headers[HeaderNames.Authorization], out var authHeaderValue) + if (Request.Headers.TryGetValue(HeaderNames.Authorization, out Microsoft.Extensions.Primitives.StringValues authHeaderStringValue) + && AuthenticationHeaderValue.TryParse(authHeaderStringValue, out var authHeaderValue) && authHeaderValue.Scheme.Equals(Options.KeyName, StringComparison.OrdinalIgnoreCase) ) { diff --git a/src/AspNetCore.Authentication.ApiKey/ApiKeyInQueryParamsHandler.cs b/src/AspNetCore.Authentication.ApiKey/ApiKeyInQueryParamsHandler.cs index 42ab982..46739f1 100644 --- a/src/AspNetCore.Authentication.ApiKey/ApiKeyInQueryParamsHandler.cs +++ b/src/AspNetCore.Authentication.ApiKey/ApiKeyInQueryParamsHandler.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System; using System.Linq; using System.Text.Encodings.Web; using System.Threading.Tasks; @@ -12,7 +13,15 @@ namespace AspNetCore.Authentication.ApiKey { public class ApiKeyInQueryParamsHandler : ApiKeyHandlerBase { - public ApiKeyInQueryParamsHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) +#if NET8_0_OR_GREATER + protected ApiKeyInQueryParamsHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + [Obsolete("ISystemClock is obsolete, use TimeProvider on AuthenticationSchemeOptions instead.")] +#endif + public ApiKeyInQueryParamsHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } diff --git a/src/AspNetCore.Authentication.ApiKey/AspNetCore.Authentication.ApiKey.csproj b/src/AspNetCore.Authentication.ApiKey/AspNetCore.Authentication.ApiKey.csproj index dc872fc..09cb44f 100644 --- a/src/AspNetCore.Authentication.ApiKey/AspNetCore.Authentication.ApiKey.csproj +++ b/src/AspNetCore.Authentication.ApiKey/AspNetCore.Authentication.ApiKey.csproj @@ -1,21 +1,20 @@  - net7.0;net6.0;net5.0;netcoreapp3.1;netcoreapp3.0;netstandard2.0;net461 - 7.0.0 + net8.0;net7.0;net6.0;net5.0;netcoreapp3.1;netcoreapp3.0;netstandard2.0;net461 + 8.0.0 https://github.com/mihirdilip/aspnetcore-authentication-apiKey/tree/$(Version) https://github.com/mihirdilip/aspnetcore-authentication-apiKey/tree/$(Version) - aspnetcore, security, authentication, microsoft, microsoft.aspnetcore.authentication, microsoft-aspnetcore-authentication, microsoft.aspnetcore.authentication.apikey, microsoft-aspnetcore-authentication-apikey, asp-net-core, netstandard, netstandard20, apikey-authentication, api-key-authentication, apikeyauthentication, dotnetcore, dotnetcore3.1, net5, net5.0, net6, net6.0, net7, net7.0, asp-net-core-apikey-authentication, aspnetcore-apikey-authentication, net5-apikey-authentication, asp-net-core-authentication, aspnetcore-authentication, net5-authentication, asp, aspnet, apikey, api-key, authentication-scheme - - net7.0 support added -- Information log on handler is changed to Debug log when API Key is not found on the request -- Added package validations -- Readme updated -- Readme added to package + aspnetcore, security, authentication, microsoft, microsoft.aspnetcore.authentication, microsoft-aspnetcore-authentication, microsoft.aspnetcore.authentication.apikey, microsoft-aspnetcore-authentication-apikey, asp-net-core, netstandard, netstandard20, apikey-authentication, api-key-authentication, apikeyauthentication, dotnetcore, dotnetcore3.1, net5, net5.0, net6, net6.0, net7, net7.0, net8, net8.0, asp-net-core-apikey-authentication, aspnetcore-apikey-authentication, net5-apikey-authentication, asp-net-core-authentication, aspnetcore-authentication, net5-authentication, asp, aspnet, apikey, api-key, authentication-scheme + - net8.0 support added +- Sample project for net8.0 added +- ApiKeySamplesClient.http file added for testing sample projects +- Readme updated Easy to use and very light weight Microsoft style API Key Authentication Implementation for ASP.NET Core. It can be setup so that it can accept API Key either in Header, Authorization Header, QueryParams or HeaderOrQueryParams. Mihir Dilip Mihir Dilip - Copyright (c) 2022 Mihir Dilip + Copyright (c) 2023 Mihir Dilip true $(AssemblyName) git @@ -30,6 +29,10 @@ true + + false + + true @@ -66,7 +69,7 @@ - + diff --git a/src/AspNetCore.Authentication.ApiKey/CompatibilitySuppressions.xml b/src/AspNetCore.Authentication.ApiKey/CompatibilitySuppressions.xml index ab186ee..5fac6d1 100644 --- a/src/AspNetCore.Authentication.ApiKey/CompatibilitySuppressions.xml +++ b/src/AspNetCore.Authentication.ApiKey/CompatibilitySuppressions.xml @@ -1,5 +1,36 @@  + + + CP0002 + M:AspNetCore.Authentication.ApiKey.ApiKeyHandlerBase.#ctor(Microsoft.Extensions.Options.IOptionsMonitor{AspNetCore.Authentication.ApiKey.ApiKeyOptions},Microsoft.Extensions.Logging.ILoggerFactory,System.Text.Encodings.Web.UrlEncoder) + lib/net7.0/AspNetCore.Authentication.ApiKey.dll + lib/net8.0/AspNetCore.Authentication.ApiKey.dll + + + CP0002 + M:AspNetCore.Authentication.ApiKey.ApiKeyInAuthorizationHeaderHandler.#ctor(Microsoft.Extensions.Options.IOptionsMonitor{AspNetCore.Authentication.ApiKey.ApiKeyOptions},Microsoft.Extensions.Logging.ILoggerFactory,System.Text.Encodings.Web.UrlEncoder) + lib/net7.0/AspNetCore.Authentication.ApiKey.dll + lib/net8.0/AspNetCore.Authentication.ApiKey.dll + + + CP0002 + M:AspNetCore.Authentication.ApiKey.ApiKeyInHeaderHandler.#ctor(Microsoft.Extensions.Options.IOptionsMonitor{AspNetCore.Authentication.ApiKey.ApiKeyOptions},Microsoft.Extensions.Logging.ILoggerFactory,System.Text.Encodings.Web.UrlEncoder) + lib/net7.0/AspNetCore.Authentication.ApiKey.dll + lib/net8.0/AspNetCore.Authentication.ApiKey.dll + + + CP0002 + M:AspNetCore.Authentication.ApiKey.ApiKeyInHeaderOrQueryParamsHandler.#ctor(Microsoft.Extensions.Options.IOptionsMonitor{AspNetCore.Authentication.ApiKey.ApiKeyOptions},Microsoft.Extensions.Logging.ILoggerFactory,System.Text.Encodings.Web.UrlEncoder) + lib/net7.0/AspNetCore.Authentication.ApiKey.dll + lib/net8.0/AspNetCore.Authentication.ApiKey.dll + + + CP0002 + M:AspNetCore.Authentication.ApiKey.ApiKeyInQueryParamsHandler.#ctor(Microsoft.Extensions.Options.IOptionsMonitor{AspNetCore.Authentication.ApiKey.ApiKeyOptions},Microsoft.Extensions.Logging.ILoggerFactory,System.Text.Encodings.Web.UrlEncoder) + lib/net7.0/AspNetCore.Authentication.ApiKey.dll + lib/net8.0/AspNetCore.Authentication.ApiKey.dll + CP0002 M:AspNetCore.Authentication.ApiKey.ApiKeyOptions.get_IgnoreAuthenticationIfAllowAnonymous diff --git a/src/AspNetCore.Authentication.ApiKey/GlobalSuppressions.cs b/src/AspNetCore.Authentication.ApiKey/GlobalSuppressions.cs new file mode 100644 index 0000000..323efae --- /dev/null +++ b/src/AspNetCore.Authentication.ApiKey/GlobalSuppressions.cs @@ -0,0 +1,9 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Maintainability", "CA1510:Use ArgumentNullException throw helper", Justification = "", Scope = "member", Target = "~M:AspNetCore.Authentication.ApiKey.ApiKeyAuthenticationSucceededContext.AddClaim(System.Security.Claims.Claim)")] +[assembly: SuppressMessage("Maintainability", "CA1510:Use ArgumentNullException throw helper", Justification = "", Scope = "member", Target = "~M:AspNetCore.Authentication.ApiKey.ApiKeyAuthenticationSucceededContext.AddClaims(System.Collections.Generic.IEnumerable{System.Security.Claims.Claim})")] diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/AspNetCore.Authentication.ApiKey.Tests.csproj b/test/AspNetCore.Authentication.ApiKey.Tests/AspNetCore.Authentication.ApiKey.Tests.csproj index e2e5734..caea524 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/AspNetCore.Authentication.ApiKey.Tests.csproj +++ b/test/AspNetCore.Authentication.ApiKey.Tests/AspNetCore.Authentication.ApiKey.Tests.csproj @@ -1,9 +1,16 @@  - net7.0;net6.0;net5.0;netcoreapp3.1;netcoreapp3.0;netcoreapp2.1;net461 + net8.0;net7.0;net6.0;net5.0;netcoreapp3.1;netcoreapp3.0;netcoreapp2.1;net461 + enable + enable false latest + true + + + + true @@ -14,34 +21,43 @@ - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -49,8 +65,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -58,7 +74,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -67,6 +83,7 @@ + @@ -79,11 +96,11 @@ - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all From e6fa97819059118b31c74850ec5ceed9f6e97d66 Mon Sep 17 00:00:00 2001 From: Mihir Dilip Date: Wed, 22 Nov 2023 23:01:58 +0000 Subject: [PATCH 2/2] Code cleanup and refactoring done --- .../SampleWebApi.Shared/GlobalSuppressions.cs | 15 +++++ samples/SampleWebApi.Shared/Models/ApiKey.cs | 8 ++- .../Repositories/IApiKeyRepository.cs | 2 +- .../Repositories/InMemoryApiKeyRepository.cs | 22 ++++--- .../SampleWebApi.Shared.projitems | 1 + .../SampleWebApi_2_0/SampleWebApi_2_0.csproj | 2 + .../SampleWebApi_2_2/SampleWebApi_2_2.csproj | 2 + .../SampleWebApi_3_1/SampleWebApi_3_1.csproj | 1 + .../SampleWebApi_5_0/SampleWebApi_5_0.csproj | 1 + .../ApiKeyExtensionsTests.cs | 2 +- .../ApiKeyHandlerBaseTests.cs | 16 ++--- ...ApiKeyInAuthorizationHeaderHandlerTests.cs | 51 +++++++++++---- .../ApiKeyInHeaderHandlerTests.cs | 51 +++++++++++---- ...ApiKeyInHeaderOrQueryParamsHandlerTests.cs | 51 +++++++++++---- .../ApiKeyInQueryParamsHandlerTests.cs | 51 +++++++++++---- .../ApiKeyOptionsTests.cs | 4 +- .../ApiKeyPostConfigureOptionsTests.cs | 6 +- .../ApiKeyUtilsTests.cs | 12 ++-- ...iKeyAuthenticationSucceededContextTests.cs | 62 +++++++++++++------ .../ApiKeyHandleChallengeContextTests.cs | 39 ++++++++++-- .../ApiKeyHandleForbiddenContextTests.cs | 39 ++++++++++-- .../Events/ApiKeyValidateKeyContextTests.cs | 61 +++++++++++++----- .../GlobalSuppressions.cs | 11 ++++ .../Infrastructure/ClaimsPrincipalDto.cs | 7 ++- .../Infrastructure/FakeApiKeyProvider.cs | 14 ++--- .../Infrastructure/TestServerBuilder.cs | 18 +++--- 26 files changed, 400 insertions(+), 149 deletions(-) create mode 100644 samples/SampleWebApi.Shared/GlobalSuppressions.cs create mode 100644 test/AspNetCore.Authentication.ApiKey.Tests/GlobalSuppressions.cs diff --git a/samples/SampleWebApi.Shared/GlobalSuppressions.cs b/samples/SampleWebApi.Shared/GlobalSuppressions.cs new file mode 100644 index 0000000..50f4708 --- /dev/null +++ b/samples/SampleWebApi.Shared/GlobalSuppressions.cs @@ -0,0 +1,15 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "", Scope = "member", Target = "~F:SampleWebApi.Repositories.InMemoryApiKeyRepository._cache")] +[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "", Scope = "member", Target = "~F:SampleWebApi.Repositories.InMemoryApiKeyRepository._cache")] +[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:SampleWebApi.Models.ApiKey.#ctor(System.String,System.String,System.Collections.Generic.List{System.Security.Claims.Claim})")] +[assembly: SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "", Scope = "member", Target = "~M:SampleWebApi.Models.ApiKey.#ctor(System.String,System.String,System.Collections.Generic.List{System.Security.Claims.Claim})")] +[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:SampleWebApi.Services.ApiKeyProvider.#ctor(Microsoft.Extensions.Logging.ILogger{AspNetCore.Authentication.ApiKey.IApiKeyProvider},SampleWebApi.Repositories.IApiKeyRepository)")] + +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:SampleWebApi.Services.ApiKeyProvider.ProvideAsync(System.String)~System.Threading.Tasks.Task{AspNetCore.Authentication.ApiKey.IApiKey}")] + diff --git a/samples/SampleWebApi.Shared/Models/ApiKey.cs b/samples/SampleWebApi.Shared/Models/ApiKey.cs index a2bfcbc..54f74bb 100644 --- a/samples/SampleWebApi.Shared/Models/ApiKey.cs +++ b/samples/SampleWebApi.Shared/Models/ApiKey.cs @@ -1,4 +1,5 @@ -using AspNetCore.Authentication.ApiKey; +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. +using AspNetCore.Authentication.ApiKey; using System.Collections.Generic; using System.Security.Claims; @@ -6,8 +7,8 @@ namespace SampleWebApi.Models { class ApiKey : IApiKey { - public ApiKey(string key, string owner, List claims = null) - { + public ApiKey(string key, string owner, List claims = null) + { Key = key; OwnerName = owner; Claims = claims ?? new List(); @@ -18,3 +19,4 @@ public ApiKey(string key, string owner, List claims = null) public IReadOnlyCollection Claims { get; } } } +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. diff --git a/samples/SampleWebApi.Shared/Repositories/IApiKeyRepository.cs b/samples/SampleWebApi.Shared/Repositories/IApiKeyRepository.cs index a2e1cd1..e0471ea 100644 --- a/samples/SampleWebApi.Shared/Repositories/IApiKeyRepository.cs +++ b/samples/SampleWebApi.Shared/Repositories/IApiKeyRepository.cs @@ -8,6 +8,6 @@ namespace SampleWebApi.Repositories /// public interface IApiKeyRepository { - Task GetApiKeyAsync(string key); + Task GetApiKeyAsync(string key); } } \ No newline at end of file diff --git a/samples/SampleWebApi.Shared/Repositories/InMemoryApiKeyRepository.cs b/samples/SampleWebApi.Shared/Repositories/InMemoryApiKeyRepository.cs index 1a0b1da..51043f5 100644 --- a/samples/SampleWebApi.Shared/Repositories/InMemoryApiKeyRepository.cs +++ b/samples/SampleWebApi.Shared/Repositories/InMemoryApiKeyRepository.cs @@ -1,4 +1,5 @@ -using AspNetCore.Authentication.ApiKey; +#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type. +using AspNetCore.Authentication.ApiKey; using SampleWebApi.Models; using System; using System.Collections.Generic; @@ -7,13 +8,13 @@ namespace SampleWebApi.Repositories { - /// - /// NOTE: DO NOT USE THIS IMPLEMENTATION. THIS IS FOR DEMO PURPOSE ONLY - /// - public class InMemoryApiKeyRepository : IApiKeyRepository + /// + /// NOTE: DO NOT USE THIS IMPLEMENTATION. THIS IS FOR DEMO PURPOSE ONLY + /// + public class InMemoryApiKeyRepository : IApiKeyRepository { - private List _cache = new List - { + private readonly List _cache = new List() + { new ApiKey("Key1", "Admin"), new ApiKey("Key2", "User"), }; @@ -21,7 +22,8 @@ public class InMemoryApiKeyRepository : IApiKeyRepository public Task GetApiKeyAsync(string key) { var apiKey = _cache.FirstOrDefault(k => k.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - return Task.FromResult(apiKey); - } - } + return Task.FromResult(apiKey); + } + } } +#pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type. diff --git a/samples/SampleWebApi.Shared/SampleWebApi.Shared.projitems b/samples/SampleWebApi.Shared/SampleWebApi.Shared.projitems index 093651a..7582e0d 100644 --- a/samples/SampleWebApi.Shared/SampleWebApi.Shared.projitems +++ b/samples/SampleWebApi.Shared/SampleWebApi.Shared.projitems @@ -9,6 +9,7 @@ SampleWebApi.Shared + diff --git a/samples/SampleWebApi_2_0/SampleWebApi_2_0.csproj b/samples/SampleWebApi_2_0/SampleWebApi_2_0.csproj index 430ff52..8ac49e3 100644 --- a/samples/SampleWebApi_2_0/SampleWebApi_2_0.csproj +++ b/samples/SampleWebApi_2_0/SampleWebApi_2_0.csproj @@ -2,6 +2,8 @@ netcoreapp2.0 + false + false diff --git a/samples/SampleWebApi_2_2/SampleWebApi_2_2.csproj b/samples/SampleWebApi_2_2/SampleWebApi_2_2.csproj index 08c7d61..0dafbeb 100644 --- a/samples/SampleWebApi_2_2/SampleWebApi_2_2.csproj +++ b/samples/SampleWebApi_2_2/SampleWebApi_2_2.csproj @@ -3,6 +3,8 @@ netcoreapp2.2 InProcess + false + false diff --git a/samples/SampleWebApi_3_1/SampleWebApi_3_1.csproj b/samples/SampleWebApi_3_1/SampleWebApi_3_1.csproj index 0454d54..5fc5145 100644 --- a/samples/SampleWebApi_3_1/SampleWebApi_3_1.csproj +++ b/samples/SampleWebApi_3_1/SampleWebApi_3_1.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 + false diff --git a/samples/SampleWebApi_5_0/SampleWebApi_5_0.csproj b/samples/SampleWebApi_5_0/SampleWebApi_5_0.csproj index 64d99f7..451e988 100644 --- a/samples/SampleWebApi_5_0/SampleWebApi_5_0.csproj +++ b/samples/SampleWebApi_5_0/SampleWebApi_5_0.csproj @@ -2,6 +2,7 @@ net5.0 + false diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyExtensionsTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyExtensionsTests.cs index 6b82330..0dba8bd 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyExtensionsTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyExtensionsTests.cs @@ -1192,7 +1192,7 @@ public void AddApiKeyInHeaderOrQueryParams_TApiKeyProvider_allows_chaining_with_ #endregion // API Key - In Header Or Query Parameters - private Task GetSchemeAsync(Action authenticationBuilderAction, string schemeName = ApiKeyDefaults.AuthenticationScheme) + private static Task GetSchemeAsync(Action authenticationBuilderAction, string schemeName = ApiKeyDefaults.AuthenticationScheme) { var services = new ServiceCollection(); authenticationBuilderAction(services.AddAuthentication()); diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyHandlerBaseTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyHandlerBaseTests.cs index 0b22619..87864c4 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyHandlerBaseTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyHandlerBaseTests.cs @@ -47,7 +47,7 @@ public async Task HandleForbidden_using_OnHandleForbidden() options.KeyName = FakeApiKeys.KeyName; options.Events.OnHandleForbidden = context => { - context.HttpContext.Response.Headers.Add(HeaderFromEventsKey, HeaderFromEventsValue); + context.HttpContext.Response.Headers[HeaderFromEventsKey] = HeaderFromEventsValue; return Task.CompletedTask; }; }); @@ -87,8 +87,8 @@ public async Task HandleChallange_using_OnHandleChallenge() options.KeyName = FakeApiKeys.KeyName; options.Events.OnHandleChallenge = context => { - context.HttpContext.Response.Headers.Add(HeaderFromEventsKey, HeaderFromEventsValue); - return Task.CompletedTask; + context.HttpContext.Response.Headers[HeaderFromEventsKey] = HeaderFromEventsValue; + return Task.CompletedTask; }; }); using var client = server.CreateClient(); @@ -128,7 +128,7 @@ public async Task HandleChallange_using_OnHandleChallenge_and_SuppressWWWAuthent options.SuppressWWWAuthenticateHeader = true; options.Events.OnHandleChallenge = context => { - context.HttpContext.Response.Headers.Add(HeaderFromEventsKey, HeaderFromEventsValue); + context.HttpContext.Response.Headers[HeaderFromEventsKey] = HeaderFromEventsValue; return Task.CompletedTask; }; }); @@ -420,7 +420,7 @@ public async Task MultiScheme() options.KeyName = keyName1; options.Events.OnValidateKey = context => { - context.Response.Headers.Add("X-Custom", "InHeader Scheme"); + context.Response.Headers["X-Custom"] = "InHeader Scheme"; context.ValidationSucceeded(); return Task.CompletedTask; }; @@ -497,7 +497,7 @@ public async Task MultiScheme() #endregion // Multi-Scheme - private async Task DeserializeClaimsPrincipalAsync(HttpResponseMessage response) + private static async Task DeserializeClaimsPrincipalAsync(HttpResponseMessage response) { return JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync()); } @@ -506,7 +506,7 @@ private class FakeApiKeyProviderLocal_1 : IApiKeyProvider { public Task ProvideAsync(string key) { - return Task.FromResult((IApiKey)new FakeApiKey(key, "Test", new List { new Claim("Provider", "1") })); + return Task.FromResult((IApiKey)new FakeApiKey(key, "Test", new List { new("Provider", "1") })); } } @@ -514,7 +514,7 @@ private class FakeApiKeyProviderLocal_2 : IApiKeyProvider { public Task ProvideAsync(string key) { - return Task.FromResult((IApiKey)new FakeApiKey(key, "Test", new List { new Claim("Provider", "2") })); + return Task.FromResult((IApiKey)new FakeApiKey(key, "Test", new List { new("Provider", "2") })); } } } diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInAuthorizationHeaderHandlerTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInAuthorizationHeaderHandlerTests.cs index 4d8912f..f7bd386 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInAuthorizationHeaderHandlerTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInAuthorizationHeaderHandlerTests.cs @@ -21,6 +21,7 @@ public class ApiKeyInAuthorizationHeaderHandlerTests : IDisposable private readonly HttpClient _client; private readonly TestServer _serverWithProvider; private readonly HttpClient _clientWithProvider; + private bool _disposedValue; public ApiKeyInAuthorizationHeaderHandlerTests() { @@ -31,15 +32,6 @@ public ApiKeyInAuthorizationHeaderHandlerTests() _clientWithProvider = _serverWithProvider.CreateClient(); } - public void Dispose() - { - _client?.Dispose(); - _server?.Dispose(); - - _clientWithProvider?.Dispose(); - _serverWithProvider?.Dispose(); - } - [Fact] public async Task Verify_Handler() { @@ -52,7 +44,7 @@ public async Task Verify_Handler() Assert.Equal(typeof(ApiKeyInAuthorizationHeaderHandler), scheme.HandlerType); var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(scheme.Name); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(scheme.Name); Assert.NotNull(apiKeyOptions); Assert.NotNull(apiKeyOptions.Events?.OnValidateKey); Assert.Null(apiKeyOptions.ApiKeyProviderType); @@ -73,7 +65,7 @@ public async Task TApiKeyProvider_Verify_Handler() Assert.Equal(typeof(ApiKeyInAuthorizationHeaderHandler), scheme.HandlerType); var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(scheme.Name); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(scheme.Name); Assert.NotNull(apiKeyOptions); Assert.Null(apiKeyOptions.Events?.OnValidateKey); Assert.NotNull(apiKeyOptions.ApiKeyProviderType); @@ -218,5 +210,40 @@ public async Task TApiKeyProvider_invalid_key_unauthotized_with_key_name() Assert.False(response.IsSuccessStatusCode); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } - } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + + _client?.Dispose(); + _server?.Dispose(); + + _clientWithProvider?.Dispose(); + _serverWithProvider?.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~ApiKeyInAuthorizationHeaderHandlerTests() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } } diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInHeaderHandlerTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInHeaderHandlerTests.cs index b837ace..f1c3f99 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInHeaderHandlerTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInHeaderHandlerTests.cs @@ -20,6 +20,7 @@ public class ApiKeyInHeaderHandlerTests : IDisposable private readonly HttpClient _client; private readonly TestServer _serverWithProvider; private readonly HttpClient _clientWithProvider; + private bool _disposedValue; public ApiKeyInHeaderHandlerTests() { @@ -30,15 +31,6 @@ public ApiKeyInHeaderHandlerTests() _clientWithProvider = _serverWithProvider.CreateClient(); } - public void Dispose() - { - _client?.Dispose(); - _server?.Dispose(); - - _clientWithProvider?.Dispose(); - _serverWithProvider?.Dispose(); - } - [Fact] public async Task Verify_Handler() { @@ -51,7 +43,7 @@ public async Task Verify_Handler() Assert.Equal(typeof(ApiKeyInHeaderHandler), scheme.HandlerType); var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(scheme.Name); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(scheme.Name); Assert.NotNull(apiKeyOptions); Assert.NotNull(apiKeyOptions.Events?.OnValidateKey); Assert.Null(apiKeyOptions.ApiKeyProviderType); @@ -72,7 +64,7 @@ public async Task TApiKeyProvider_Verify_Handler() Assert.Equal(typeof(ApiKeyInHeaderHandler), scheme.HandlerType); var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(scheme.Name); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(scheme.Name); Assert.NotNull(apiKeyOptions); Assert.Null(apiKeyOptions.Events?.OnValidateKey); Assert.NotNull(apiKeyOptions.ApiKeyProviderType); @@ -156,5 +148,40 @@ public async Task TApiKeyProvider_invalid_key_unauthotized() Assert.False(response.IsSuccessStatusCode); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } - } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + + _client?.Dispose(); + _server?.Dispose(); + + _clientWithProvider?.Dispose(); + _serverWithProvider?.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~ApiKeyInHeaderHandlerTests() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } } diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInHeaderOrQueryParamsHandlerTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInHeaderOrQueryParamsHandlerTests.cs index 9a93eff..30cc48d 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInHeaderOrQueryParamsHandlerTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInHeaderOrQueryParamsHandlerTests.cs @@ -21,6 +21,7 @@ public class ApiKeyInHeaderOrQueryParamsHandlerTests : IDisposable private readonly HttpClient _client; private readonly TestServer _serverWithProvider; private readonly HttpClient _clientWithProvider; + private bool _disposedValue; public ApiKeyInHeaderOrQueryParamsHandlerTests() { @@ -31,15 +32,6 @@ public ApiKeyInHeaderOrQueryParamsHandlerTests() _clientWithProvider = _serverWithProvider.CreateClient(); } - public void Dispose() - { - _client?.Dispose(); - _server?.Dispose(); - - _clientWithProvider?.Dispose(); - _serverWithProvider?.Dispose(); - } - [Fact] public async Task Verify_Handler() { @@ -52,7 +44,7 @@ public async Task Verify_Handler() Assert.Equal(typeof(ApiKeyInHeaderOrQueryParamsHandler), scheme.HandlerType); var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(scheme.Name); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(scheme.Name); Assert.NotNull(apiKeyOptions); Assert.NotNull(apiKeyOptions.Events?.OnValidateKey); Assert.Null(apiKeyOptions.ApiKeyProviderType); @@ -73,7 +65,7 @@ public async Task TApiKeyProvider_Verify_Handler() Assert.Equal(typeof(ApiKeyInHeaderOrQueryParamsHandler), scheme.HandlerType); var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(scheme.Name); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(scheme.Name); Assert.NotNull(apiKeyOptions); Assert.Null(apiKeyOptions.Events?.OnValidateKey); Assert.NotNull(apiKeyOptions.ApiKeyProviderType); @@ -238,5 +230,40 @@ public async Task TApiKeyProvider_InQueryParams_invalid_key_unauthotized() Assert.False(response.IsSuccessStatusCode); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } - } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + + _client?.Dispose(); + _server?.Dispose(); + + _clientWithProvider?.Dispose(); + _serverWithProvider?.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~ApiKeyInHeaderOrQueryParamsHandlerTests() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } } diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInQueryParamsHandlerTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInQueryParamsHandlerTests.cs index 14e4741..92a4fb7 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInQueryParamsHandlerTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyInQueryParamsHandlerTests.cs @@ -20,6 +20,7 @@ public class ApiKeyInQueryParamsHandlerTests : IDisposable private readonly HttpClient _client; private readonly TestServer _serverWithProvider; private readonly HttpClient _clientWithProvider; + private bool _disposedValue; public ApiKeyInQueryParamsHandlerTests() { @@ -30,15 +31,6 @@ public ApiKeyInQueryParamsHandlerTests() _clientWithProvider = _serverWithProvider.CreateClient(); } - public void Dispose() - { - _client?.Dispose(); - _server?.Dispose(); - - _clientWithProvider?.Dispose(); - _serverWithProvider?.Dispose(); - } - [Fact] public async Task Verify_Handler() { @@ -51,7 +43,7 @@ public async Task Verify_Handler() Assert.Equal(typeof(ApiKeyInQueryParamsHandler), scheme.HandlerType); var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(scheme.Name); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(scheme.Name); Assert.NotNull(apiKeyOptions); Assert.NotNull(apiKeyOptions.Events?.OnValidateKey); Assert.Null(apiKeyOptions.ApiKeyProviderType); @@ -72,7 +64,7 @@ public async Task TApiKeyProvider_Verify_Handler() Assert.Equal(typeof(ApiKeyInQueryParamsHandler), scheme.HandlerType); var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(scheme.Name); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(scheme.Name); Assert.NotNull(apiKeyOptions); Assert.Null(apiKeyOptions.Events?.OnValidateKey); Assert.NotNull(apiKeyOptions.ApiKeyProviderType); @@ -157,5 +149,40 @@ public async Task TApiKeyProvider_invalid_key_unauthotized() Assert.False(response.IsSuccessStatusCode); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } - } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + + _client?.Dispose(); + _server?.Dispose(); + + _clientWithProvider?.Dispose(); + _serverWithProvider?.Dispose(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + _disposedValue = true; + } + } + + // // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~ApiKeyInQueryParamsHandlerTests() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } } diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyOptionsTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyOptionsTests.cs index d2a1c5d..2da2b78 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyOptionsTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyOptionsTests.cs @@ -195,7 +195,7 @@ public void ApiKeyProviderType_verify_null() var services = server.Host.Services; var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(ApiKeyDefaults.AuthenticationScheme); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(ApiKeyDefaults.AuthenticationScheme); Assert.NotNull(apiKeyOptions); Assert.Null(apiKeyOptions.ApiKeyProviderType); @@ -210,7 +210,7 @@ public void ApiKeyProviderType_verify_not_null() var services = server.Host.Services; var apiKeyOptionsSnapshot = services.GetService>(); - var apiKeyOptions = apiKeyOptionsSnapshot.Get(ApiKeyDefaults.AuthenticationScheme); + var apiKeyOptions = apiKeyOptionsSnapshot?.Get(ApiKeyDefaults.AuthenticationScheme); Assert.NotNull(apiKeyOptions); Assert.NotNull(apiKeyOptions.ApiKeyProviderType); Assert.Equal(typeof(FakeApiKeyProvider), apiKeyOptions.ApiKeyProviderType); diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyPostConfigureOptionsTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyPostConfigureOptionsTests.cs index e33f117..b11e963 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyPostConfigureOptionsTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyPostConfigureOptionsTests.cs @@ -11,7 +11,7 @@ namespace AspNetCore.Authentication.ApiKey.Tests { public class ApiKeyPostConfigureOptionsTests { - static string KeyName = "X-API-KEY"; + static readonly string KeyName = "X-API-KEY"; [Fact] public async Task PostConfigure_no_option_set_throws_exception() @@ -111,13 +111,13 @@ await RunAuthInitWithProviderAsync(options => } - private async Task RunAuthInitAsync(Action configureOptions) + private static async Task RunAuthInitAsync(Action configureOptions) { var server = TestServerBuilder.BuildInHeaderOrQueryParamsServer(configureOptions); await server.CreateClient().GetAsync(TestServerBuilder.BaseUrl); } - private async Task RunAuthInitWithProviderAsync(Action configureOptions) + private static async Task RunAuthInitWithProviderAsync(Action configureOptions) { var server = TestServerBuilder.BuildInHeaderOrQueryParamsServerWithProvider(configureOptions); await server.CreateClient().GetAsync(TestServerBuilder.BaseUrl); diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyUtilsTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyUtilsTests.cs index 557d534..86fd24e 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyUtilsTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/ApiKeyUtilsTests.cs @@ -57,8 +57,8 @@ public static void BuildClaimsPrincipal_adds_single_identity_with_claims() var schemeName = "Test"; var claims = new List { - new Claim(ClaimTypes.Email, "abc@xyz.com") , - new Claim(ClaimTypes.Role, "admin") + new(ClaimTypes.Email, "abc@xyz.com") , + new(ClaimTypes.Role, "admin") }; var claimsPrincipal = ApiKeyUtils.BuildClaimsPrincipal(null, schemeName, null, claims); Assert.NotNull(claimsPrincipal); @@ -89,8 +89,8 @@ public static void BuildClaimsPrincipal_ownerName_adds_Name_and_NameIdentifier_c var schemeName = "Test"; var claims = new List { - new Claim(ClaimTypes.Email, "abc@xyz.com") , - new Claim(ClaimTypes.Role, "admin") + new(ClaimTypes.Email, "abc@xyz.com") , + new(ClaimTypes.Role, "admin") }; var claimsPrincipal = ApiKeyUtils.BuildClaimsPrincipal(ownerName, schemeName, null, claims); Assert.NotNull(claimsPrincipal); @@ -108,8 +108,8 @@ public static void BuildClaimsPrincipal_ownerName_adds_Name_and_NameIdentifier_c var schemeName = "Test"; var claims = new List { - new Claim(ClaimTypes.Name, "Admin"), - new Claim(ClaimTypes.Role, "admin") + new(ClaimTypes.Name, "Admin"), + new(ClaimTypes.Role, "admin") }; var claimsPrincipal = ApiKeyUtils.BuildClaimsPrincipal(ownerName, schemeName, null, claims); Assert.NotNull(claimsPrincipal); diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyAuthenticationSucceededContextTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyAuthenticationSucceededContextTests.cs index e25e803..ee0748a 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyAuthenticationSucceededContextTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyAuthenticationSucceededContextTests.cs @@ -3,25 +3,18 @@ using AspNetCore.Authentication.ApiKey.Tests.Infrastructure; using Microsoft.AspNetCore.TestHost; -using System; -using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Security.Claims; using System.Text.Json; -using System.Threading.Tasks; using Xunit; namespace AspNetCore.Authentication.ApiKey.Tests.Events { public class ApiKeyAuthenticationSucceededContextTests : IDisposable { - private readonly List _serversToDispose = new List(); - - public void Dispose() - { - _serversToDispose.ForEach(s => s.Dispose()); - } + private readonly List _serversToDispose = []; + private bool _disposedValue; [Fact] public async Task Principal_not_null() @@ -35,7 +28,7 @@ public async Task Principal_not_null() } ); - var principal = await RunSuccessTests(client); + var principal = await ApiKeyAuthenticationSucceededContextTests.RunSuccessTests(client); Assert.True(principal.Identity.IsAuthenticated); } @@ -50,7 +43,7 @@ public async Task ReplacePrincipal_null_throws_argument_null_exception() } ); - await RunSuccessTests(client); + await ApiKeyAuthenticationSucceededContextTests.RunSuccessTests(client); } [Fact] @@ -69,7 +62,7 @@ public async Task ReplacePrincipal() } ); - await RunUnauthorizedTests(client); + await ApiKeyAuthenticationSucceededContextTests.RunUnauthorizedTests(client); } [Fact] @@ -86,7 +79,7 @@ public async Task RejectPrincipal() } ); - await RunUnauthorizedTests(client); + await ApiKeyAuthenticationSucceededContextTests.RunUnauthorizedTests(client); } [Fact] @@ -105,7 +98,7 @@ public async Task AddClaim() } ); - var principal = await RunSuccessTests(client); + var principal = await ApiKeyAuthenticationSucceededContextTests.RunSuccessTests(client); Assert.Contains(new ClaimDto(claim), principal.Claims); } @@ -113,8 +106,8 @@ public async Task AddClaim() public async Task AddClaims() { var claims = new List{ - new Claim(ClaimTypes.Actor, "Actor"), - new Claim(ClaimTypes.Country, "Country") + new(ClaimTypes.Actor, "Actor"), + new(ClaimTypes.Country, "Country") }; using var client = BuildClient( @@ -129,7 +122,7 @@ public async Task AddClaims() } ); - var principal = await RunSuccessTests(client); + var principal = await ApiKeyAuthenticationSucceededContextTests.RunSuccessTests(client); Assert.Contains(new ClaimDto(claims[0]), principal.Claims); Assert.Contains(new ClaimDto(claims[1]), principal.Claims); } @@ -149,7 +142,7 @@ private HttpClient BuildClient(Func return server.CreateClient(); } - private async Task RunUnauthorizedTests(HttpClient client) + private static async Task RunUnauthorizedTests(HttpClient client) { using var request = new HttpRequestMessage(HttpMethod.Get, TestServerBuilder.ClaimsPrincipalUrl); request.Headers.Add(FakeApiKeys.KeyName, FakeApiKeys.FakeKey); @@ -158,7 +151,7 @@ private async Task RunUnauthorizedTests(HttpClient client) Assert.Equal(HttpStatusCode.Unauthorized, response_unauthorized.StatusCode); } - private async Task RunSuccessTests(HttpClient client) + private static async Task RunSuccessTests(HttpClient client) { using var request = new HttpRequestMessage(HttpMethod.Get, TestServerBuilder.ClaimsPrincipalUrl); request.Headers.Add(FakeApiKeys.KeyName, FakeApiKeys.FakeKey); @@ -170,5 +163,36 @@ private async Task RunSuccessTests(HttpClient client) Assert.False(string.IsNullOrWhiteSpace(content)); return JsonSerializer.Deserialize(content); } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + + _serversToDispose.ForEach(s => s.Dispose()); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~ApiKeyAuthenticationSucceededContextTests() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyHandleChallengeContextTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyHandleChallengeContextTests.cs index 08effe3..72da071 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyHandleChallengeContextTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyHandleChallengeContextTests.cs @@ -15,12 +15,8 @@ namespace AspNetCore.Authentication.ApiKey.Tests.Events { public class ApiKeyHandleChallengeContextTests : IDisposable { - private readonly List _serversToDispose = new List(); - - public void Dispose() - { - _serversToDispose.ForEach(s => s.Dispose()); - } + private readonly List _serversToDispose = []; + private bool _disposedValue; [Fact] public async Task Handled() @@ -79,5 +75,36 @@ private HttpClient BuildClient(Func onHandle _serversToDispose.Add(server); return server.CreateClient(); } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + + _serversToDispose.ForEach(s => s.Dispose()); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~ApiKeyHandleChallengeContextTests() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyHandleForbiddenContextTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyHandleForbiddenContextTests.cs index 80e3838..c16d927 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyHandleForbiddenContextTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyHandleForbiddenContextTests.cs @@ -15,12 +15,8 @@ namespace AspNetCore.Authentication.ApiKey.Tests.Events { public class ApiKeyHandleForbiddenContextTests : IDisposable { - private readonly List _serversToDispose = new List(); - - public void Dispose() - { - _serversToDispose.ForEach(s => s.Dispose()); - } + private readonly List _serversToDispose = []; + private bool _disposedValue; [Fact] public async Task Handled() @@ -83,5 +79,36 @@ private HttpClient BuildClient(Func onHandle _serversToDispose.Add(server); return server.CreateClient(); } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + + _serversToDispose.ForEach(s => s.Dispose()); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + _disposedValue = true; + } + } + + // // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~ApiKeyHandleForbiddenContextTests() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyValidateKeyContextTests.cs b/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyValidateKeyContextTests.cs index 099e3b9..a56a8f4 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyValidateKeyContextTests.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/Events/ApiKeyValidateKeyContextTests.cs @@ -17,12 +17,8 @@ namespace AspNetCore.Authentication.ApiKey.Tests.Events { public class ApiKeyValidateKeyContextTests : IDisposable { - private readonly List _serversToDispose = new List(); - - public void Dispose() - { - _serversToDispose.ForEach(s => s.Dispose()); - } + private readonly List _serversToDispose = []; + private bool _disposedValue; [Fact] public async Task Success_and_NoResult() @@ -59,10 +55,10 @@ public async Task Success_and_NoResult() } ); - var principal = await RunSuccessTests(client); + var principal = await ApiKeyValidateKeyContextTests.RunSuccessTests(client); Assert.Empty(principal.Claims); - await RunUnauthorizedTests(client); + await ApiKeyValidateKeyContextTests.RunUnauthorizedTests(client); } [Fact] @@ -95,10 +91,10 @@ public async Task ValidationSucceeded_and_ValidationFailed() } ); - var principal = await RunSuccessTests(client); + var principal = await ApiKeyValidateKeyContextTests.RunSuccessTests(client); Assert.Empty(principal.Claims); - await RunUnauthorizedTests(client); + await ApiKeyValidateKeyContextTests.RunUnauthorizedTests(client); } [Fact] @@ -124,7 +120,7 @@ public async Task ValidationSucceeded_with_claims() } ); - var principal = await RunSuccessTests(client); + var principal = await ApiKeyValidateKeyContextTests.RunSuccessTests(client); Assert.NotEmpty(principal.Claims); Assert.Equal(claimsSource.Count, principal.Claims.Count()); @@ -151,7 +147,7 @@ public async Task ValidationSucceeded_with_ownerName() } ); - var principal = await RunSuccessTests(client); + var principal = await ApiKeyValidateKeyContextTests.RunSuccessTests(client); Assert.NotEmpty(principal.Claims); Assert.Equal(2, principal.Claims.Count()); @@ -183,7 +179,7 @@ public async Task ValidationSucceeded_with_ownerName_and_claims() } ); - var principal = await RunSuccessTests(client); + var principal = await ApiKeyValidateKeyContextTests.RunSuccessTests(client); Assert.NotEmpty(principal.Claims); Assert.Equal(claimsSource.Count + 1, principal.Claims.Count()); @@ -212,7 +208,7 @@ public async Task ValidationFailed_with_failureMessage() } ); - await RunUnauthorizedTests(client); + await ApiKeyValidateKeyContextTests.RunUnauthorizedTests(client); } [Fact] @@ -235,7 +231,7 @@ public async Task ValidationFailed_with_failureException() } ); - await RunUnauthorizedTests(client); + await ApiKeyValidateKeyContextTests.RunUnauthorizedTests(client); } @@ -253,14 +249,14 @@ private HttpClient BuildClient(Func onValidateKe return server.CreateClient(); } - private async Task RunUnauthorizedTests(HttpClient client) + private static async Task RunUnauthorizedTests(HttpClient client) { using var response_unauthorized = await client.GetAsync(TestServerBuilder.ClaimsPrincipalUrl); Assert.False(response_unauthorized.IsSuccessStatusCode); Assert.Equal(HttpStatusCode.Unauthorized, response_unauthorized.StatusCode); } - private async Task RunSuccessTests(HttpClient client) + private static async Task RunSuccessTests(HttpClient client) { using var request = new HttpRequestMessage(HttpMethod.Get, TestServerBuilder.ClaimsPrincipalUrl); request.Headers.Add(FakeApiKeys.KeyName, FakeApiKeys.FakeKey); @@ -272,5 +268,36 @@ private async Task RunSuccessTests(HttpClient client) Assert.False(string.IsNullOrWhiteSpace(content)); return JsonSerializer.Deserialize(content); } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + + _serversToDispose.ForEach(s => s.Dispose()); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + _disposedValue = true; + } + } + + // // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~ApiKeyValidateKeyContextTests() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/GlobalSuppressions.cs b/test/AspNetCore.Authentication.ApiKey.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..284d804 --- /dev/null +++ b/test/AspNetCore.Authentication.ApiKey.Tests/GlobalSuppressions.cs @@ -0,0 +1,11 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:AspNetCore.Authentication.ApiKey.Tests.Infrastructure.ClaimsPrincipalDto.#ctor(System.Security.Claims.ClaimsPrincipal)")] +[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:AspNetCore.Authentication.ApiKey.Tests.Infrastructure.ClaimDto.#ctor(System.Security.Claims.Claim)")] +[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:AspNetCore.Authentication.ApiKey.Tests.Infrastructure.FakeApiKey.#ctor(System.String,System.String,System.Collections.Generic.IReadOnlyCollection{System.Security.Claims.Claim})")] +[assembly: SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "", Scope = "member", Target = "~M:AspNetCore.Authentication.ApiKey.Tests.ApiKeyHandlerBaseTests.MultiScheme~System.Threading.Tasks.Task")] diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/ClaimsPrincipalDto.cs b/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/ClaimsPrincipalDto.cs index cc843e2..48ee328 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/ClaimsPrincipalDto.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/ClaimsPrincipalDto.cs @@ -27,16 +27,17 @@ public ClaimsPrincipalDto(ClaimsPrincipal user) [Serializable] struct ClaimsIdentityDto { - public ClaimsIdentityDto(IIdentity identity) + public ClaimsIdentityDto(IIdentity? identity) { + if (identity == null) throw new ArgumentNullException(nameof(identity)); Name = identity.Name; IsAuthenticated = identity.IsAuthenticated; AuthenticationType = identity.AuthenticationType; } - public string Name { get; set; } + public string? Name { get; set; } public bool IsAuthenticated { get; set; } - public string AuthenticationType { get; set; } + public string? AuthenticationType { get; set; } } [Serializable] diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/FakeApiKeyProvider.cs b/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/FakeApiKeyProvider.cs index 5cc6b58..62a2808 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/FakeApiKeyProvider.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/FakeApiKeyProvider.cs @@ -11,7 +11,7 @@ namespace AspNetCore.Authentication.ApiKey.Tests.Infrastructure { class FakeApiKeyProvider : IApiKeyProvider { - public Task ProvideAsync(string key) + public Task ProvideAsync(string key) { var apiKey = FakeApiKeys.Keys.FirstOrDefault(k => k.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); if (apiKey != null) @@ -38,11 +38,11 @@ public Task ProvideAsync(string key) class FakeApiKey : IApiKey { - public FakeApiKey(string key, string ownerName, IReadOnlyCollection claims = null) + public FakeApiKey(string key, string ownerName, IReadOnlyCollection? claims = null) { Key = key; OwnerName = ownerName; - Claims = claims; + Claims = claims ?? new List(); } public string Key { get; } @@ -62,11 +62,11 @@ class FakeApiKeys internal static string FakeKeyForLegacyIgnoreExtraValidatedApiKeyCheck = "ForLegacyIgnoreExtraValidatedApiKeyCheck"; internal static string FakeKeyIgnoreAuthenticationIfAllowAnonymous = "IgnoreAuthenticationIfAllowAnonymous"; internal static string FakeKeyOwner = "Fake Key"; - internal static Claim FakeNameClaim = new Claim(ClaimTypes.Name, "FakeNameClaim", ClaimValueTypes.String); - internal static Claim FakeNameIdentifierClaim = new Claim(ClaimTypes.NameIdentifier, "FakeNameIdentifierClaim", ClaimValueTypes.String); - internal static Claim FakeRoleClaim = new Claim(ClaimTypes.Role, "FakeRoleClaim", ClaimValueTypes.String); + internal static Claim FakeNameClaim = new(ClaimTypes.Name, "FakeNameClaim", ClaimValueTypes.String); + internal static Claim FakeNameIdentifierClaim = new(ClaimTypes.NameIdentifier, "FakeNameIdentifierClaim", ClaimValueTypes.String); + internal static Claim FakeRoleClaim = new(ClaimTypes.Role, "FakeRoleClaim", ClaimValueTypes.String); - internal static List Keys => new List + internal static List Keys => new() { new FakeApiKey(FakeKey, FakeKeyOwner, new List { FakeNameClaim, FakeNameIdentifierClaim, FakeRoleClaim }), new FakeApiKey(FakeKeyThrowsNotImplemented, FakeKeyOwner, new List { FakeNameClaim, FakeNameIdentifierClaim, FakeRoleClaim }), diff --git a/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/TestServerBuilder.cs b/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/TestServerBuilder.cs index 0fca718..951e23a 100644 --- a/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/TestServerBuilder.cs +++ b/test/AspNetCore.Authentication.ApiKey.Tests/Infrastructure/TestServerBuilder.cs @@ -26,7 +26,7 @@ partial class TestServerBuilder internal static string ClaimsPrincipalUrl = $"{BaseUrl}claims-principal"; internal static string Realm = "ApiKeyTests"; - internal static TestServer BuildInAuthorizationHeaderServer(Action configureOptions = null) + internal static TestServer BuildInAuthorizationHeaderServer(Action? configureOptions = null) { return BuildTestServer( services => @@ -37,7 +37,7 @@ internal static TestServer BuildInAuthorizationHeaderServer(Action configureOptions = null) + internal static TestServer BuildInAuthorizationHeaderServerWithProvider(Action? configureOptions = null) { return BuildTestServer( services => @@ -48,7 +48,7 @@ internal static TestServer BuildInAuthorizationHeaderServerWithProvider(Action configureOptions = null) + internal static TestServer BuildInHeaderServer(Action? configureOptions = null) { return BuildTestServer( services => @@ -59,7 +59,7 @@ internal static TestServer BuildInHeaderServer(Action configureOp ); } - internal static TestServer BuildInHeaderServerWithProvider(Action configureOptions = null) + internal static TestServer BuildInHeaderServerWithProvider(Action? configureOptions = null) { return BuildTestServer( services => @@ -70,7 +70,7 @@ internal static TestServer BuildInHeaderServerWithProvider(Action ); } - internal static TestServer BuildInQueryParamsServer(Action configureOptions = null) + internal static TestServer BuildInQueryParamsServer(Action? configureOptions = null) { return BuildTestServer( services => @@ -81,7 +81,7 @@ internal static TestServer BuildInQueryParamsServer(Action config ); } - internal static TestServer BuildInQueryParamsServerWithProvider(Action configureOptions = null) + internal static TestServer BuildInQueryParamsServerWithProvider(Action? configureOptions = null) { return BuildTestServer( services => @@ -92,7 +92,7 @@ internal static TestServer BuildInQueryParamsServerWithProvider(Action configureOptions = null) + internal static TestServer BuildInHeaderOrQueryParamsServer(Action? configureOptions = null) { return BuildTestServer( services => @@ -103,7 +103,7 @@ internal static TestServer BuildInHeaderOrQueryParamsServer(Action configureOptions = null) + internal static TestServer BuildInHeaderOrQueryParamsServerWithProvider(Action? configureOptions = null) { return BuildTestServer( services => @@ -114,7 +114,7 @@ internal static TestServer BuildInHeaderOrQueryParamsServerWithProvider(Action configureServices, Action configure = null) + internal static TestServer BuildTestServer(Action configureServices, Action? configure = null) { if (configureServices == null) throw new ArgumentNullException(nameof(configureServices));