diff --git a/.editorconfig b/.editorconfig index f6b7d3a..8f8512a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -82,6 +82,7 @@ csharp_style_prefer_local_over_anonymous_function = true:silent csharp_style_prefer_extended_property_pattern = true:suggestion csharp_style_implicit_object_creation_when_type_is_apparent = true:silent csharp_style_prefer_tuple_swap = true:silent +csharp_style_prefer_simple_property_accessors = true:suggestion # Field preferences dotnet_style_readonly_field = true:suggestion @@ -299,4 +300,7 @@ dotnet_diagnostic.IDE0010.severity = none dotnet_diagnostic.IDE0072.severity = none # IDE0305: Simplify collection initialization -dotnet_diagnostic.IDE0305.severity = none \ No newline at end of file +dotnet_diagnostic.IDE0305.severity = none + +# CA1873: Avoid potentially expensive logging +dotnet_diagnostic.CA1873.severity = none \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 610f98a..928e9f0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -59,7 +59,7 @@ ### Implementation Guidelines - Write code that is secure by default. Avoid exposing potentially private or sensitive data. -- Make code NativeAOT compatible when possible. This means avoiding dynamic code generation, reflection, and other features that are not compatible. with NativeAOT. If not possible, mark the code with an appropriate annotation or throw an exception. +- Make code NativeAOT compatible when possible. This means avoiding dynamic code generation, reflection, and other features that are not compatible with NativeAOT. If not possible, mark the code with an appropriate annotation or throw an exception. ## Documentation @@ -81,10 +81,4 @@ - Use NSubstitute for mocking in tests. - Copy existing style in nearby files for test method names and capitalization. - When running tests, if possible use filters and check test run counts, or look at test logs, to ensure they actually ran. -- Do not finish work with any tests commented out or disabled that were not previously commented out or disabled. - -## Azure - -- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools. -- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first. -- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it. +- Do not finish work with any tests commented out or disabled that were not previously commented out or disabled. \ No newline at end of file diff --git a/README.md b/README.md index 27e678a..1b8041c 100644 --- a/README.md +++ b/README.md @@ -14,20 +14,20 @@ A library to easily integrate Authentication in ASP.NET Core projects. Currently ## Installation -The library is available on [NuGet](https://www.nuget.org/packages/SimpleAuthenticationTools). Just search for *SimpleAuthenticationTools* in the **Package Manager GUI** or run the following command in the **.NET CLI**: +The library is available on [NuGet](https://www.nuget.org/packages/SimpleAuthenticationTools). Search for *SimpleAuthenticationTools* in the **Package Manager GUI** or run the following command in the **.NET CLI**: ```shell dotnet add package SimpleAuthenticationTools ``` ## Usage video -Take a look to a quick demo showing how to integrate the library: +Take a look at a quick demo showing how to integrate the library: [![Simple Authentication for ASP.NET Core](https://raw.githubusercontent.com/marcominerva/SimpleAuthentication/master/Screenshot.jpg)](https://www.youtube.com/watch?v=SVZuaPE2yNc) ## Configuration -Authentication can be totally configured adding an _Authentication_ section in the _appsettings.json_ file: +Authentication can be fully configured adding an _Authentication_ section in the _appsettings.json_ file: ``` "Authentication": { @@ -42,7 +42,6 @@ Authentication can be totally configured adding an _Authentication_ section in t "Audiences": [ "audience" ], // Optional "ExpirationTime": "01:00:00", // Default: No expiration "ClockSkew": "00:02:00", // Default: 5 minutes - "EnableJwtBearerService": true // Default: true }, "ApiKey": { "SchemeName": "ApiKey", // Default: ApiKey @@ -107,7 +106,7 @@ app.Run(); **Integrating with Swashbuckle** -If you're using Swashbuckle (Swagger) to document your API, you can integrate the authentication configuration with the Swagger documentation. Just search for *SimpleAuthenticationTools.Swashbuckle* in the **Package Manager GUI** or run the following command in the **.NET CLI**: +If you're using Swashbuckle (Swagger) to document your API, you can integrate the authentication configuration with the Swagger documentation. Search for *SimpleAuthenticationTools.Swashbuckle* in the **Package Manager GUI** or run the following command in the **.NET CLI**: ```shell dotnet add package SimpleAuthenticationTools.Swashbuckle @@ -126,7 +125,7 @@ builder.Services.AddSwaggerGen(options => **Integrating with Microsoft.AspNetCore.OpenApi (.NET 9 or later)** -Starting from version 9, .NET offer a built-in support for OpenAPI. If you're using the `AddOpenApi` extension method to provide OpenAPI support, you just need to add the corresponding extension method in its declaration (no extra package required): +Starting from version 9, .NET offers built-in support for OpenAPI. If you're using the `AddOpenApi` extension method to provide OpenAPI support, you just need to add the corresponding extension method in its declaration (no extra package required): ```csharp builder.Services.AddOpenApi(options => @@ -142,7 +141,7 @@ builder.Services.AddOpenApi(options => When using JWT Bearer authentication, you can set the _EnableJwtBearerService_ setting to _true_ to automatically register an implementation of the [IJwtBearerService](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/JwtBearer/IJwtBearerService.cs) interface to create a valid JWT Bearer, according to the setting you have specified in the _appsettings.json_ file: ```csharp -app.MapPost("api/auth/login", (LoginRequest loginRequest, IJwtBearerService jwtBearerService) => +app.MapPost("api/auth/login", async (LoginRequest loginRequest, IJwtBearerService jwtBearerService) => { // Check for login rights... @@ -153,7 +152,7 @@ app.MapPost("api/auth/login", (LoginRequest loginRequest, IJwtBearerService jwtB new(ClaimTypes.Surname, "Minerva") }; - var token = jwtBearerService.CreateToken(loginRequest.UserName, claims); + var token = await jwtBearerService.CreateTokenAsync(loginRequest.UserName, claims); return TypedResults.Ok(new LoginResponse(token)); }); @@ -197,11 +196,11 @@ When using API Key or Basic Authentication, you can specify multiple fixed value } ``` -With this configuration, authentication will succedd if any of these credentials are provided. +With this configuration, authentication will succeed if any of these credentials are provided. **Custom Authentication logic for API Keys and Basic Authentication** -If you need to implement custom authentication login, for example validating credentials with dynamic values and adding claims to identity, you can omit all the credentials in the _appsettings.json_ file and then provide an implementation of [IApiKeyValidator.cs](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/ApiKey/IApiKeyValidator.cs) or [IBasicAuthenticationValidator.cs](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/BasicAuthentication/IBasicAuthenticationValidator.cs): +If you need to implement custom authentication logic, for example validating credentials with dynamic values and adding claims to identity, you can omit all the credentials in the _appsettings.json_ file and then provide an implementation of [IApiKeyValidator.cs](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/ApiKey/IApiKeyValidator.cs) or [IBasicAuthenticationValidator.cs](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/BasicAuthentication/IBasicAuthenticationValidator.cs): ```csharp builder.Services.AddTransient(); @@ -247,7 +246,7 @@ The library provides services for adding permission-based authorization to an AS builder.Services.AddPermissions(); ``` -The **AddPermissions** extension method requires an implementation of the [IPermissionHandler interface](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/Permissions/IPermissionHandler.cs), that is responsible to check if the user owns the required permissions: +The **AddPermissions** extension method requires an implementation of the [IPermissionHandler interface](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/Permissions/IPermissionHandler.cs), which is responsible to check if the user owns the required permissions: ```csharp public interface IPermissionHandler @@ -317,4 +316,4 @@ app.MapGet("api/me", (ClaimsPrincipal user) => ## Contribute -The project is constantly evolving. Contributions are welcome. Feel free to file issues and pull requests on the repo and we'll address them as we can. +The project is constantly evolving. Contributions are welcome. Feel free to file issues and pull requests in the repository, and we'll address them as we can. diff --git a/samples/Controllers/ApiKeySample/ApiKeySample.csproj b/samples/Controllers/ApiKeySample/ApiKeySample.csproj index 7f34c8f..15b70ac 100644 --- a/samples/Controllers/ApiKeySample/ApiKeySample.csproj +++ b/samples/Controllers/ApiKeySample/ApiKeySample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj b/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj index 7f34c8f..15b70ac 100644 --- a/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj +++ b/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj b/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj index 7f34c8f..15b70ac 100644 --- a/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj +++ b/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj b/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj index 7f34c8f..15b70ac 100644 --- a/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj +++ b/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj b/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj index 7f34c8f..15b70ac 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj +++ b/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj b/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj index 407bc22..f98eec3 100644 --- a/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj +++ b/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj b/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj index ad43b10..31d32fe 100644 --- a/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj +++ b/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 578c8fc..979fdda 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -9,7 +9,7 @@ - + diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs new file mode 100644 index 0000000..428322e --- /dev/null +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs @@ -0,0 +1,8 @@ +namespace SimpleAuthentication.ApiKey; + +/// +/// Store API Keys for API Key Authentication +/// +/// The API key value +/// The user name associated with the current key +public record class ApiKey(string Value, string UserName); diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs index d168f26..86c8b21 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs @@ -41,26 +41,11 @@ public class ApiKeySettings : AuthenticationSchemeOptions /// public string? UserName { get; set; } - private ICollection apiKeys = []; /// /// The collection of valid API keys. /// /// - public ICollection ApiKeys - { - get - { - if (!string.IsNullOrWhiteSpace(ApiKeyValue) && !string.IsNullOrWhiteSpace(UserName)) - { - // If necessary, add the API Key from the base properties. - apiKeys.Add(new(ApiKeyValue, UserName)); - } - - return apiKeys; - } - - internal set => apiKeys = value ?? []; - } + public IEnumerable ApiKeys { get; set; } = []; /// /// Gets or sets a that defines the . @@ -80,11 +65,16 @@ public ICollection ApiKeys /// The default is . /// public string RoleClaimType { get; set; } = ClaimsIdentity.DefaultRoleClaimType; -} -/// -/// Store API Keys for API Key Authentication -/// -/// The API key value -/// The user name associated with the current key -public record class ApiKey(string Value, string UserName); + internal IEnumerable GetAllApiKeys() + { + var apiKeys = (ApiKeys ?? []).ToHashSet(); + if (!string.IsNullOrWhiteSpace(ApiKeyValue) && !string.IsNullOrWhiteSpace(UserName)) + { + // If necessary, add the API Key from the base properties. + apiKeys.Add(new(ApiKeyValue, UserName)); + } + + return apiKeys; + } +} diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs index f68aad9..27e6757 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs @@ -30,26 +30,11 @@ public class BasicAuthenticationSettings : AuthenticationSchemeOptions /// public string? Password { get; set; } - private ICollection credentials = []; /// /// The collection of authorization credentials. /// /// - public ICollection Credentials - { - get - { - if (!string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password)) - { - // If necessary, add the credentials from the base properties. - credentials.Add(new Credential(UserName, Password)); - } - - return credentials; - } - - internal set => credentials = value ?? []; - } + public IEnumerable Credentials { get; set; } = []; /// /// Gets or sets a that defines the . @@ -70,11 +55,15 @@ public ICollection Credentials /// public string RoleClaimType { get; set; } = ClaimsIdentity.DefaultRoleClaimType; -} + internal IEnumerable GetAllCredentials() + { + var credentials = (Credentials ?? []).ToHashSet(); + if (!string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password)) + { + // If necessary, add the Credentials from the base properties. + credentials.Add(new(UserName, Password)); + } -/// -/// Store credentials used for Basic Authentication. -/// -/// The user name -/// The password -public record class Credential(string UserName, string Password); + return credentials; + } +} diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs new file mode 100644 index 0000000..3519fb1 --- /dev/null +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs @@ -0,0 +1,8 @@ +namespace SimpleAuthentication.BasicAuthentication; + +/// +/// Store credentials used for Basic Authentication. +/// +/// The user name +/// The password +public record class Credential(string UserName, string Password); diff --git a/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj b/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj index 0e63f17..eea5a73 100644 --- a/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj +++ b/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj @@ -28,11 +28,11 @@ - + - + diff --git a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj index 694b6d8..55a05d3 100644 --- a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj +++ b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs b/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs index 60ade30..733949b 100644 --- a/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs +++ b/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs @@ -7,21 +7,8 @@ namespace SimpleAuthentication.ApiKey; -internal class ApiKeyAuthenticationHandler : AuthenticationHandler +internal class ApiKeyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IServiceProvider serviceProvider) : AuthenticationHandler(options, logger, encoder) { - private readonly IServiceProvider serviceProvider; - -#if NET8_0_OR_GREATER - public ApiKeyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IServiceProvider serviceProvider) - : base(options, logger, encoder) -#else - public ApiKeyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IServiceProvider serviceProvider) - : base(options, logger, encoder, clock) -#endif - { - this.serviceProvider = serviceProvider; - } - protected override async Task HandleAuthenticateAsync() { var request = Context.Request; @@ -39,7 +26,8 @@ protected override async Task HandleAuthenticateAsync() request.Query.TryGetValue(Options.QueryStringKey ?? string.Empty, out value); } - if (!Options.ApiKeys.Any()) + var apiKeys = Options.GetAllApiKeys(); + if (!apiKeys.Any()) { // There is no fixed values, so it tries to get an external service to validate the API Key. var validator = serviceProvider.GetService() ?? throw new InvalidOperationException("There isn't a default value for API Key and no custom validator has been provided"); @@ -53,7 +41,7 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Fail(validationResult.FailureMessage); } - var apiKey = Options.ApiKeys.FirstOrDefault(c => c.Value == value); + var apiKey = apiKeys.FirstOrDefault(c => c.Value == value); if (apiKey is not null) { return CreateAuthenticationSuccessResult(apiKey.UserName); diff --git a/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs b/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs index f31c523..ab9e6e8 100644 --- a/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs +++ b/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs @@ -10,21 +10,8 @@ namespace SimpleAuthentication.BasicAuthentication; -internal class BasicAuthenticationHandler : AuthenticationHandler +internal partial class BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IServiceProvider serviceProvider) : AuthenticationHandler(options, logger, encoder) { - private readonly IServiceProvider serviceProvider; - -#if NET8_0_OR_GREATER - public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IServiceProvider serviceProvider) - : base(options, logger, encoder) -#else - public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IServiceProvider serviceProvider) - : base(options, logger, encoder, clock) -#endif - { - this.serviceProvider = serviceProvider; - } - protected override async Task HandleAuthenticateAsync() { var request = Context.Request; @@ -39,14 +26,12 @@ protected override async Task HandleAuthenticateAsync() // Get Authorization header. var authorizationHeader = request.Headers.Authorization.ToString(); - var authorizationHeaderRegex = new Regex(@"Basic (.*)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - if (!authorizationHeaderRegex.IsMatch(authorizationHeader)) + if (!BasicAuthorizationHeaderRegex().IsMatch(authorizationHeader)) { return AuthenticateResult.Fail("Basic Authorization header is not properly formatted"); } - var values = Encoding.UTF8.GetString(Convert.FromBase64String(authorizationHeaderRegex.Replace(authorizationHeader, "$1"))).Split(':', count: 2); + var values = Encoding.UTF8.GetString(Convert.FromBase64String(BasicAuthorizationHeaderRegex().Replace(authorizationHeader, "$1"))).Split(':', count: 2); var userName = values.ElementAtOrDefault(0); var password = values.ElementAtOrDefault(1); @@ -55,7 +40,8 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Fail("Invalid user name or password"); } - if (!Options.Credentials.Any()) + var credentials = Options.GetAllCredentials(); + if (!credentials.Any()) { // There is no fixed values, so it tries to get an external service to validate user name and password. var validator = serviceProvider.GetService() ?? throw new InvalidOperationException("There isn't a default user name and password for authentication and no custom validator has been provided"); @@ -69,7 +55,7 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Fail(validationResult.FailureMessage); } - var credential = Options.Credentials.FirstOrDefault(c => c.UserName == userName && c.Password == password); + var credential = credentials.FirstOrDefault(c => c.UserName == userName && c.Password == password); if (credential is not null) { return CreateAuthenticationSuccessResult(credential.UserName); @@ -90,4 +76,7 @@ AuthenticateResult CreateAuthenticationSuccessResult(string userName, IList - - - - - + @@ -45,5 +41,9 @@ + + + +