From d4cd6d79ebe1e0ef504aba20a4ba5b935b3fb263 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:47:44 +0000 Subject: [PATCH 01/19] Initial plan From 6442cd61c18f62c6719c77c18fa6992c30e61022 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:59:06 +0000 Subject: [PATCH 02/19] Add Roles support to ApiKey and Basic Authentication Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- README.md | 51 +++++++++++++++++++ .../ApiKey/ApiKeySettings.cs | 3 +- .../BasicAuthenticationSettings.cs | 3 +- .../ApiKey/ApiKeyAuthenticationHandler.cs | 11 +++- .../BasicAuthenticationHandler.cs | 11 +++- .../SimpleAuthentication.csproj | 2 +- 6 files changed, 76 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9e62775..ee520ea 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,57 @@ 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. +**Assigning roles to API Keys and Basic Authentication credentials** + +You can optionally specify roles for each API Key or Basic Authentication credential. When authentication succeeds, the specified roles will be automatically added as role claims to the user's identity: + +```json +"Authentication": { + "ApiKey": { + "ApiKeys": [ + { + "Value": "key-1", + "UserName": "UserName1", + "Roles": ["Admin", "User"] + }, + { + "Value": "key-2", + "UserName": "UserName2", + "Roles": ["User"] + } + ] + }, + "Basic": { + "Credentials": [ + { + "UserName": "UserName1", + "Password": "Password1", + "Roles": ["Manager", "User"] + }, + { + "UserName": "UserName2", + "Password": "Password2", + "Roles": ["User"] + } + ] + } +} +``` + +The `Roles` parameter is optional. If omitted, no role claims will be added to the user's identity. You can then use the standard ASP.NET Core authorization features to check for roles: + +```csharp +[Authorize(Roles = "Admin")] +public IActionResult AdminEndpoint() +{ + return Ok("Admin access granted"); +} + +// Or with minimal APIs +app.MapGet("/admin", () => "Admin access granted") + .RequireAuthorization(policy => policy.RequireRole("Admin")); +``` + **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): diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs index d168f26..10fbab5 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs @@ -87,4 +87,5 @@ public ICollection ApiKeys /// /// The API key value /// The user name associated with the current key -public record class ApiKey(string Value, string UserName); +/// The optional list of roles to assign to the user +public record class ApiKey(string Value, string UserName, string[]? Roles = null); diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs index f68aad9..0cce98a 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs @@ -77,4 +77,5 @@ public ICollection Credentials /// /// The user name /// The password -public record class Credential(string UserName, string Password); +/// The optional list of roles to assign to the user +public record class Credential(string UserName, string Password, string[]? Roles = null); diff --git a/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs b/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs index 60ade30..8ed98c2 100644 --- a/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs +++ b/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs @@ -56,7 +56,16 @@ protected override async Task HandleAuthenticateAsync() var apiKey = Options.ApiKeys.FirstOrDefault(c => c.Value == value); if (apiKey is not null) { - return CreateAuthenticationSuccessResult(apiKey.UserName); + var claims = new List(); + if (apiKey.Roles is not null) + { + foreach (var role in apiKey.Roles) + { + claims.Add(new Claim(Options.RoleClaimType, role)); + } + } + + return CreateAuthenticationSuccessResult(apiKey.UserName, claims); } return AuthenticateResult.Fail("Invalid API Key"); diff --git a/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs b/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs index f31c523..f61be2a 100644 --- a/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs +++ b/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs @@ -72,7 +72,16 @@ protected override async Task HandleAuthenticateAsync() var credential = Options.Credentials.FirstOrDefault(c => c.UserName == userName && c.Password == password); if (credential is not null) { - return CreateAuthenticationSuccessResult(credential.UserName); + var claims = new List(); + if (credential.Roles is not null) + { + foreach (var role in credential.Roles) + { + claims.Add(new Claim(Options.RoleClaimType, role)); + } + } + + return CreateAuthenticationSuccessResult(credential.UserName, claims); } return AuthenticateResult.Fail("Invalid user name or password"); diff --git a/src/SimpleAuthentication/SimpleAuthentication.csproj b/src/SimpleAuthentication/SimpleAuthentication.csproj index 5adb283..ecc96ef 100644 --- a/src/SimpleAuthentication/SimpleAuthentication.csproj +++ b/src/SimpleAuthentication/SimpleAuthentication.csproj @@ -35,7 +35,7 @@ - + From 1f7daa20d6531417e1b6b75f48ab154a9fa0c519 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:13:53 +0000 Subject: [PATCH 03/19] Update sample applications to test roles feature and fix project references Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- samples/MinimalApis/ApiKeySample/Program.cs | 13 +++++++++++-- samples/MinimalApis/ApiKeySample/appsettings.json | 6 ++++-- .../BasicAuthenticationSample/Program.cs | 13 +++++++++++-- .../BasicAuthenticationSample/appsettings.json | 6 ++++-- .../SimpleAuthentication.Swashbuckle.csproj | 2 +- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/samples/MinimalApis/ApiKeySample/Program.cs b/samples/MinimalApis/ApiKeySample/Program.cs index be9ea71..aa2353e 100644 --- a/samples/MinimalApis/ApiKeySample/Program.cs +++ b/samples/MinimalApis/ApiKeySample/Program.cs @@ -57,14 +57,23 @@ app.MapGet("api/me", (ClaimsPrincipal user) => { - return TypedResults.Ok(new User(user.Identity!.Name)); + var roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value); + return TypedResults.Ok(new User(user.Identity!.Name, roles)); }) .RequireAuthorization() .WithOpenApi(); +app.MapGet("api/admin", () => "Admin access granted") +.RequireAuthorization(policy => policy.RequireRole("Admin")) +.WithOpenApi(); + +app.MapGet("api/user", () => "User access granted") +.RequireAuthorization(policy => policy.RequireRole("User")) +.WithOpenApi(); + app.Run(); -public record class User(string? UserName); +public record class User(string? UserName, IEnumerable Roles); public class CustomApiKeyValidator : IApiKeyValidator { diff --git a/samples/MinimalApis/ApiKeySample/appsettings.json b/samples/MinimalApis/ApiKeySample/appsettings.json index 5be454a..95c1b36 100644 --- a/samples/MinimalApis/ApiKeySample/appsettings.json +++ b/samples/MinimalApis/ApiKeySample/appsettings.json @@ -15,11 +15,13 @@ "ApiKeys": [ { "Value": "ArAilHVOoL3upX78Cohq", - "UserName": "alice" + "UserName": "alice", + "Roles": [ "Admin", "User" ] }, { "Value": "DiUU5EqImTYkxPDAxBVS", - "UserName": "bob" + "UserName": "bob", + "Roles": [ "User" ] } ] // You can also combine both declarations. diff --git a/samples/MinimalApis/BasicAuthenticationSample/Program.cs b/samples/MinimalApis/BasicAuthenticationSample/Program.cs index 1e0ab2f..44a8f6b 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/Program.cs +++ b/samples/MinimalApis/BasicAuthenticationSample/Program.cs @@ -57,14 +57,23 @@ app.MapGet("api/me", (ClaimsPrincipal user) => { - return TypedResults.Ok(new User(user.Identity!.Name)); + var roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value); + return TypedResults.Ok(new User(user.Identity!.Name, roles)); }) .RequireAuthorization() .WithOpenApi(); +app.MapGet("api/admin", () => "Admin access granted") +.RequireAuthorization(policy => policy.RequireRole("Admin")) +.WithOpenApi(); + +app.MapGet("api/user", () => "User access granted") +.RequireAuthorization(policy => policy.RequireRole("User")) +.WithOpenApi(); + app.Run(); -public record class User(string? UserName); +public record class User(string? UserName, IEnumerable Roles); public class CustomBasicAuthenticationValidator : IBasicAuthenticationValidator { diff --git a/samples/MinimalApis/BasicAuthenticationSample/appsettings.json b/samples/MinimalApis/BasicAuthenticationSample/appsettings.json index 334fb96..4a804f7 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/appsettings.json +++ b/samples/MinimalApis/BasicAuthenticationSample/appsettings.json @@ -12,11 +12,13 @@ "Credentials": [ { "UserName": "alice", - "Password": "Password1" + "Password": "Password1", + "Roles": [ "Admin", "User" ] }, { "UserName": "bob", - "Password": "Password2" + "Password": "Password2", + "Roles": [ "User" ] } ] // You can also combine both declarations. diff --git a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj index 694b6d8..d501249 100644 --- a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj +++ b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj @@ -32,7 +32,7 @@ - + From 92de02f401632584306eec049da6b8f4183371d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:24:50 +0000 Subject: [PATCH 04/19] Address PR feedback: add Roles to single credentials, rename Admin to Administrator, use configured RoleClaimType Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- samples/MinimalApis/ApiKeySample/Program.cs | 9 +++++---- samples/MinimalApis/ApiKeySample/appsettings.json | 3 ++- samples/MinimalApis/BasicAuthenticationSample/Program.cs | 9 +++++---- .../BasicAuthenticationSample/appsettings.json | 3 ++- .../ApiKey/ApiKeySettings.cs | 9 ++++++++- .../BasicAuthentication/BasicAuthenticationSettings.cs | 9 ++++++++- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/samples/MinimalApis/ApiKeySample/Program.cs b/samples/MinimalApis/ApiKeySample/Program.cs index aa2353e..3dc3385 100644 --- a/samples/MinimalApis/ApiKeySample/Program.cs +++ b/samples/MinimalApis/ApiKeySample/Program.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using ApiKeySample.Authentication; using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; using SimpleAuthentication; using SimpleAuthentication.ApiKey; @@ -55,16 +56,16 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("api/me", (ClaimsPrincipal user) => +app.MapGet("api/me", (ClaimsPrincipal user, IOptions options) => { - var roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value); + var roles = user.FindAll(options.Value.RoleClaimType).Select(c => c.Value); return TypedResults.Ok(new User(user.Identity!.Name, roles)); }) .RequireAuthorization() .WithOpenApi(); -app.MapGet("api/admin", () => "Admin access granted") -.RequireAuthorization(policy => policy.RequireRole("Admin")) +app.MapGet("api/admin", () => "Administrator access granted") +.RequireAuthorization(policy => policy.RequireRole("Administrator")) .WithOpenApi(); app.MapGet("api/user", () => "User access granted") diff --git a/samples/MinimalApis/ApiKeySample/appsettings.json b/samples/MinimalApis/ApiKeySample/appsettings.json index 95c1b36..369677f 100644 --- a/samples/MinimalApis/ApiKeySample/appsettings.json +++ b/samples/MinimalApis/ApiKeySample/appsettings.json @@ -11,12 +11,13 @@ // You can set a fixed API Key for authentication. If you have a single value, you can just use the plain property: "ApiKeyValue": "f1I7S5GXa4wQDgLQWgz0", "UserName": "ApiUser", // Required if ApiKeyValue is used + "Roles": [ "Administrator" ], // Otherwise, you can create an array of ApiKeys: "ApiKeys": [ { "Value": "ArAilHVOoL3upX78Cohq", "UserName": "alice", - "Roles": [ "Admin", "User" ] + "Roles": [ "Administrator", "User" ] }, { "Value": "DiUU5EqImTYkxPDAxBVS", diff --git a/samples/MinimalApis/BasicAuthenticationSample/Program.cs b/samples/MinimalApis/BasicAuthenticationSample/Program.cs index 44a8f6b..00a4c02 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/Program.cs +++ b/samples/MinimalApis/BasicAuthenticationSample/Program.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using BasicAuthenticationSample.Authentication; using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; using SimpleAuthentication; using SimpleAuthentication.BasicAuthentication; @@ -55,16 +56,16 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("api/me", (ClaimsPrincipal user) => +app.MapGet("api/me", (ClaimsPrincipal user, IOptions options) => { - var roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value); + var roles = user.FindAll(options.Value.RoleClaimType).Select(c => c.Value); return TypedResults.Ok(new User(user.Identity!.Name, roles)); }) .RequireAuthorization() .WithOpenApi(); -app.MapGet("api/admin", () => "Admin access granted") -.RequireAuthorization(policy => policy.RequireRole("Admin")) +app.MapGet("api/admin", () => "Administrator access granted") +.RequireAuthorization(policy => policy.RequireRole("Administrator")) .WithOpenApi(); app.MapGet("api/user", () => "User access granted") diff --git a/samples/MinimalApis/BasicAuthenticationSample/appsettings.json b/samples/MinimalApis/BasicAuthenticationSample/appsettings.json index 4a804f7..c65434f 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/appsettings.json +++ b/samples/MinimalApis/BasicAuthenticationSample/appsettings.json @@ -8,12 +8,13 @@ //"RoleClaimType": "user_role", // Default: http://schemas.microsoft.com/ws/2008/06/identity/claims/role "UserName": "marco", "Password": "P@$$w0rd", + "Roles": [ "Administrator" ], // Otherwise, you can create an array of Credentials: "Credentials": [ { "UserName": "alice", "Password": "Password1", - "Roles": [ "Admin", "User" ] + "Roles": [ "Administrator", "User" ] }, { "UserName": "bob", diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs index 10fbab5..9e33590 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs @@ -41,6 +41,13 @@ public class ApiKeySettings : AuthenticationSchemeOptions /// public string? UserName { get; set; } + /// + /// Gets or sets the optional list of roles to assign to the user when using and . + /// + /// + /// + public string[]? Roles { get; set; } + private ICollection apiKeys = []; /// /// The collection of valid API keys. @@ -53,7 +60,7 @@ public ICollection ApiKeys if (!string.IsNullOrWhiteSpace(ApiKeyValue) && !string.IsNullOrWhiteSpace(UserName)) { // If necessary, add the API Key from the base properties. - apiKeys.Add(new(ApiKeyValue, UserName)); + apiKeys.Add(new(ApiKeyValue, UserName, Roles)); } return apiKeys; diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs index 0cce98a..74030f6 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs @@ -30,6 +30,13 @@ public class BasicAuthenticationSettings : AuthenticationSchemeOptions /// public string? Password { get; set; } + /// + /// Gets or sets the optional list of roles to assign to the user when using and . + /// + /// + /// + public string[]? Roles { get; set; } + private ICollection credentials = []; /// /// The collection of authorization credentials. @@ -42,7 +49,7 @@ public ICollection Credentials if (!string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password)) { // If necessary, add the credentials from the base properties. - credentials.Add(new Credential(UserName, Password)); + credentials.Add(new Credential(UserName, Password, Roles)); } return credentials; From 113053b05cb66396afb58682a0ecf71e743f17d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:25:42 +0000 Subject: [PATCH 05/19] Update README to document Roles support for single credentials Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- README.md | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ee520ea..02b9533 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,26 @@ With this configuration, authentication will succedd if any of these credentials **Assigning roles to API Keys and Basic Authentication credentials** -You can optionally specify roles for each API Key or Basic Authentication credential. When authentication succeeds, the specified roles will be automatically added as role claims to the user's identity: +You can optionally specify roles for each API Key or Basic Authentication credential. When authentication succeeds, the specified roles will be automatically added as role claims to the user's identity. + +For single credentials, you can specify roles directly: + +```json +"Authentication": { + "ApiKey": { + "ApiKeyValue": "f1I7S5GXa4wQDgLQWgz0", + "UserName": "ApiUser", + "Roles": ["Administrator"] + }, + "Basic": { + "UserName": "marco", + "Password": "P@$$w0rd", + "Roles": ["Administrator"] + } +} +``` + +For multiple credentials, you can specify roles for each credential: ```json "Authentication": { @@ -210,7 +229,7 @@ You can optionally specify roles for each API Key or Basic Authentication creden { "Value": "key-1", "UserName": "UserName1", - "Roles": ["Admin", "User"] + "Roles": ["Administrator", "User"] }, { "Value": "key-2", @@ -239,15 +258,15 @@ You can optionally specify roles for each API Key or Basic Authentication creden The `Roles` parameter is optional. If omitted, no role claims will be added to the user's identity. You can then use the standard ASP.NET Core authorization features to check for roles: ```csharp -[Authorize(Roles = "Admin")] +[Authorize(Roles = "Administrator")] public IActionResult AdminEndpoint() { - return Ok("Admin access granted"); + return Ok("Administrator access granted"); } // Or with minimal APIs -app.MapGet("/admin", () => "Admin access granted") - .RequireAuthorization(policy => policy.RequireRole("Admin")); +app.MapGet("/admin", () => "Administrator access granted") + .RequireAuthorization(policy => policy.RequireRole("Administrator")); ``` **Custom Authentication logic for API Keys and Basic Authentication** From 57fc9b8a4f354bdd1a6ac6573bece7c7043d1ba4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:42:01 +0000 Subject: [PATCH 06/19] Move ApiKey and Credential classes to separate files Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs | 9 +++++++++ .../ApiKey/ApiKeySettings.cs | 8 -------- .../BasicAuthentication/BasicAuthenticationSettings.cs | 8 -------- .../BasicAuthentication/Credential.cs | 9 +++++++++ 4 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs create mode 100644 src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs new file mode 100644 index 0000000..c8d1c35 --- /dev/null +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs @@ -0,0 +1,9 @@ +namespace SimpleAuthentication.ApiKey; + +/// +/// Store API Keys for API Key Authentication +/// +/// The API key value +/// The user name associated with the current key +/// The optional list of roles to assign to the user +public record class ApiKey(string Value, string UserName, string[]? Roles = null); diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs index 9e33590..69a7594 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs @@ -88,11 +88,3 @@ public ICollection ApiKeys /// 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 -/// The optional list of roles to assign to the user -public record class ApiKey(string Value, string UserName, string[]? Roles = null); diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs index 74030f6..f5542af 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs @@ -78,11 +78,3 @@ public ICollection Credentials public string RoleClaimType { get; set; } = ClaimsIdentity.DefaultRoleClaimType; } - -/// -/// Store credentials used for Basic Authentication. -/// -/// The user name -/// The password -/// The optional list of roles to assign to the user -public record class Credential(string UserName, string Password, string[]? Roles = null); diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs new file mode 100644 index 0000000..c16245c --- /dev/null +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs @@ -0,0 +1,9 @@ +namespace SimpleAuthentication.BasicAuthentication; + +/// +/// Store credentials used for Basic Authentication. +/// +/// The user name +/// The password +/// The optional list of roles to assign to the user +public record class Credential(string UserName, string Password, string[]? Roles = null); From a3567355de36b78f8c7a468b45e5c671d30dd51e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:03:00 +0000 Subject: [PATCH 07/19] Change Roles type from string[] to IEnumerable<string> Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs | 2 +- src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs | 2 +- .../BasicAuthentication/BasicAuthenticationSettings.cs | 2 +- .../BasicAuthentication/Credential.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs index c8d1c35..23c5085 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs @@ -6,4 +6,4 @@ namespace SimpleAuthentication.ApiKey; /// The API key value /// The user name associated with the current key /// The optional list of roles to assign to the user -public record class ApiKey(string Value, string UserName, string[]? Roles = null); +public record class ApiKey(string Value, string UserName, IEnumerable? Roles = null); diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs index 69a7594..388a1fa 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs @@ -46,7 +46,7 @@ public class ApiKeySettings : AuthenticationSchemeOptions /// /// /// - public string[]? Roles { get; set; } + public IEnumerable? Roles { get; set; } private ICollection apiKeys = []; /// diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs index f5542af..ee89b4f 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs @@ -35,7 +35,7 @@ public class BasicAuthenticationSettings : AuthenticationSchemeOptions /// /// /// - public string[]? Roles { get; set; } + public IEnumerable? Roles { get; set; } private ICollection credentials = []; /// diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs index c16245c..9de39f5 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs @@ -6,4 +6,4 @@ namespace SimpleAuthentication.BasicAuthentication; /// The user name /// The password /// The optional list of roles to assign to the user -public record class Credential(string UserName, string Password, string[]? Roles = null); +public record class Credential(string UserName, string Password, IEnumerable? Roles = null); From 01b8ee323df18251f633b1b06f958b1c8679ad10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:20:43 +0000 Subject: [PATCH 08/19] Make Roles property non-nullable with empty array default in settings classes Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs | 2 +- .../BasicAuthentication/BasicAuthenticationSettings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs index 388a1fa..1deeb6c 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs @@ -46,7 +46,7 @@ public class ApiKeySettings : AuthenticationSchemeOptions /// /// /// - public IEnumerable? Roles { get; set; } + public IEnumerable Roles { get; set; } = []; private ICollection apiKeys = []; /// diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs index ee89b4f..419d4d4 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs @@ -35,7 +35,7 @@ public class BasicAuthenticationSettings : AuthenticationSchemeOptions /// /// /// - public IEnumerable? Roles { get; set; } + public IEnumerable Roles { get; set; } = []; private ICollection credentials = []; /// From 2128ebcb165c9401a55270f8f817037e41d4e2fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:43:01 +0000 Subject: [PATCH 09/19] Remove WithOpenApi extension method calls from sample endpoints Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- samples/MinimalApis/ApiKeySample/Program.cs | 9 +++------ samples/MinimalApis/BasicAuthenticationSample/Program.cs | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/samples/MinimalApis/ApiKeySample/Program.cs b/samples/MinimalApis/ApiKeySample/Program.cs index 3dc3385..a8c1f42 100644 --- a/samples/MinimalApis/ApiKeySample/Program.cs +++ b/samples/MinimalApis/ApiKeySample/Program.cs @@ -61,16 +61,13 @@ var roles = user.FindAll(options.Value.RoleClaimType).Select(c => c.Value); return TypedResults.Ok(new User(user.Identity!.Name, roles)); }) -.RequireAuthorization() -.WithOpenApi(); +.RequireAuthorization(); app.MapGet("api/admin", () => "Administrator access granted") -.RequireAuthorization(policy => policy.RequireRole("Administrator")) -.WithOpenApi(); +.RequireAuthorization(policy => policy.RequireRole("Administrator")); app.MapGet("api/user", () => "User access granted") -.RequireAuthorization(policy => policy.RequireRole("User")) -.WithOpenApi(); +.RequireAuthorization(policy => policy.RequireRole("User")); app.Run(); diff --git a/samples/MinimalApis/BasicAuthenticationSample/Program.cs b/samples/MinimalApis/BasicAuthenticationSample/Program.cs index 00a4c02..f4d2410 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/Program.cs +++ b/samples/MinimalApis/BasicAuthenticationSample/Program.cs @@ -61,16 +61,13 @@ var roles = user.FindAll(options.Value.RoleClaimType).Select(c => c.Value); return TypedResults.Ok(new User(user.Identity!.Name, roles)); }) -.RequireAuthorization() -.WithOpenApi(); +.RequireAuthorization(); app.MapGet("api/admin", () => "Administrator access granted") -.RequireAuthorization(policy => policy.RequireRole("Administrator")) -.WithOpenApi(); +.RequireAuthorization(policy => policy.RequireRole("Administrator")); app.MapGet("api/user", () => "User access granted") -.RequireAuthorization(policy => policy.RequireRole("User")) -.WithOpenApi(); +.RequireAuthorization(policy => policy.RequireRole("User")); app.Run(); From 66784b8b0ceceecf4b3de75e9de778e741c468db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:54:15 +0000 Subject: [PATCH 10/19] Initial plan From 3e14dd7db5694a8e8db403c12cc189424077bd91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:01:52 +0000 Subject: [PATCH 11/19] Remove EnableJwtBearerService setting and always register IJwtBearerService Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- README.md | 5 ++--- samples/Controllers/JwtBearerSample/appsettings.json | 3 +-- samples/MinimalApis/JwtBearerSample/appsettings.json | 3 +-- samples/MinimalApis/Net8JwtBearerSample/appsettings.json | 3 +-- .../JwtBearer/JwtBearerSettings.cs | 6 ------ src/SimpleAuthentication/SimpleAuthenticationExtensions.cs | 5 +---- 6 files changed, 6 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9e62775..daf6382 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,7 @@ Authentication can be totally configured adding an _Authentication_ section in t "Issuers": [ "issuer" ], // Optional "Audiences": [ "audience" ], // Optional "ExpirationTime": "01:00:00", // Default: No expiration - "ClockSkew": "00:02:00", // Default: 5 minutes - "EnableJwtBearerService": true // Default: true + "ClockSkew": "00:02:00" // Default: 5 minutes }, "ApiKey": { "SchemeName": "ApiKey", // Default: ApiKey @@ -139,7 +138,7 @@ builder.Services.AddOpenApi(options => **Creating a JWT Bearer** -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: +When using JWT Bearer authentication, an implementation of the [IJwtBearerService](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/JwtBearer/IJwtBearerService.cs) interface is automatically registered to create a valid JWT Bearer, according to the settings you have specified in the _appsettings.json_ file: ```csharp app.MapPost("api/auth/login", (LoginRequest loginRequest, IJwtBearerService jwtBearerService) => diff --git a/samples/Controllers/JwtBearerSample/appsettings.json b/samples/Controllers/JwtBearerSample/appsettings.json index d62f7f5..0f40eb9 100644 --- a/samples/Controllers/JwtBearerSample/appsettings.json +++ b/samples/Controllers/JwtBearerSample/appsettings.json @@ -10,8 +10,7 @@ "Issuers": [ "issuer" ], // Optional "Audiences": [ "audience" ], // Optional "ExpirationTime": "01:00:00", // Default: No expiration - "ClockSkew": "00:02:00", // Default: 5 minutes - "EnableJwtBearerService": true // Default: true + "ClockSkew": "00:02:00" // Default: 5 minutes } }, "Logging": { diff --git a/samples/MinimalApis/JwtBearerSample/appsettings.json b/samples/MinimalApis/JwtBearerSample/appsettings.json index ce32950..182cce7 100644 --- a/samples/MinimalApis/JwtBearerSample/appsettings.json +++ b/samples/MinimalApis/JwtBearerSample/appsettings.json @@ -10,8 +10,7 @@ "Issuers": [ "issuer" ], // Optional "Audiences": [ "audience" ], // Optional "ExpirationTime": "01:00:00", // Default: No expiration - "ClockSkew": "00:02:00", // Default: 5 minutes - "EnableJwtBearerService": true // Default: true + "ClockSkew": "00:02:00" // Default: 5 minutes } }, "Logging": { diff --git a/samples/MinimalApis/Net8JwtBearerSample/appsettings.json b/samples/MinimalApis/Net8JwtBearerSample/appsettings.json index ce32950..182cce7 100644 --- a/samples/MinimalApis/Net8JwtBearerSample/appsettings.json +++ b/samples/MinimalApis/Net8JwtBearerSample/appsettings.json @@ -10,8 +10,7 @@ "Issuers": [ "issuer" ], // Optional "Audiences": [ "audience" ], // Optional "ExpirationTime": "01:00:00", // Default: No expiration - "ClockSkew": "00:02:00", // Default: 5 minutes - "EnableJwtBearerService": true // Default: true + "ClockSkew": "00:02:00" // Default: 5 minutes } }, "Logging": { diff --git a/src/SimpleAuthentication.Abstractions/JwtBearer/JwtBearerSettings.cs b/src/SimpleAuthentication.Abstractions/JwtBearer/JwtBearerSettings.cs index 952e631..dd98a76 100644 --- a/src/SimpleAuthentication.Abstractions/JwtBearer/JwtBearerSettings.cs +++ b/src/SimpleAuthentication.Abstractions/JwtBearer/JwtBearerSettings.cs @@ -67,10 +67,4 @@ public class JwtBearerSettings /// The default is . /// public string RoleClaimType { get; set; } = ClaimsIdentity.DefaultRoleClaimType; - - /// - /// to register the service in the (Default: true). - /// - /// - public bool EnableJwtBearerService { get; set; } = true; } diff --git a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs index 6881be7..e38ed7a 100644 --- a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs +++ b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs @@ -113,10 +113,7 @@ static void CheckAddJwtBearer(AuthenticationBuilder builder, IConfigurationSecti }; }); - if (settings.EnableJwtBearerService) - { - builder.Services.TryAddSingleton(); - } + builder.Services.TryAddSingleton(); } static void CheckAddApiKey(AuthenticationBuilder builder, IConfigurationSection section) From 1a5e0ae233de54915f220636f5939eceb1658bd8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:30:23 +0000 Subject: [PATCH 12/19] Merge branch 'develop' to resolve conflicts Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index daf6382..3275455 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": { @@ -106,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 @@ -125,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 => @@ -141,7 +141,7 @@ builder.Services.AddOpenApi(options => When using JWT Bearer authentication, an implementation of the [IJwtBearerService](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/JwtBearer/IJwtBearerService.cs) interface is automatically registered to create a valid JWT Bearer, according to the settings 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... @@ -152,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)); }); @@ -196,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(); @@ -246,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 @@ -316,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 on the repository, and we'll address them as we can. From e9accf0bb4985ab71327e149d7002bf1b87c041f Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Wed, 15 Oct 2025 11:53:04 +0200 Subject: [PATCH 13/19] Update package references to version 3.0.13 Updated `SimpleAuthentication.Swashbuckle.csproj` to use `SimpleAuthenticationTools.Abstractions` version 3.0.13. Added `SimpleAuthenticationTools.Abstractions` version 3.0.13 to `SimpleAuthentication.csproj`. Removed direct project reference to `SimpleAuthentication.Abstractions` in favor of package dependency. --- .../SimpleAuthentication.Swashbuckle.csproj | 2 +- src/SimpleAuthentication/SimpleAuthentication.csproj | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj index 55a05d3..594aa29 100644 --- a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj +++ b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj @@ -32,7 +32,7 @@ - + diff --git a/src/SimpleAuthentication/SimpleAuthentication.csproj b/src/SimpleAuthentication/SimpleAuthentication.csproj index 0ba57c8..ea375cb 100644 --- a/src/SimpleAuthentication/SimpleAuthentication.csproj +++ b/src/SimpleAuthentication/SimpleAuthentication.csproj @@ -34,6 +34,10 @@ + + + + True @@ -41,9 +45,5 @@ - - - - From c5e89237eecd37107d6cb944873669a4a34b369d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 10:00:31 +0000 Subject: [PATCH 14/19] Merge branch 'develop' to resolve conflicts with latest changes Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- .editorconfig | 6 +++- .github/copilot-instructions.md | 10 ++---- README.md | 2 +- .../ApiKeySample/ApiKeySample.csproj | 4 +-- .../BasicAuthenticationSample.csproj | 4 +-- .../JwtBearerSample/JwtBearerSample.csproj | 4 +-- .../ApiKeySample/ApiKeySample.csproj | 4 +-- .../BasicAuthenticationSample.csproj | 4 +-- .../JwtBearerSample/JwtBearerSample.csproj | 4 +-- .../Net8JwtBearerSample.csproj | 4 +-- src/Directory.Build.props | 2 +- .../ApiKey/ApiKey.cs | 8 +++++ .../ApiKey/ApiKeySettings.cs | 36 +++++++------------ .../BasicAuthenticationSettings.cs | 35 +++++++----------- .../BasicAuthentication/Credential.cs | 8 +++++ .../SimpleAuthentication.Abstractions.csproj | 4 +-- .../SimpleAuthentication.Swashbuckle.csproj | 4 +-- .../ApiKey/ApiKeyAuthenticationHandler.cs | 20 +++-------- .../BasicAuthenticationHandler.cs | 29 +++++---------- .../SimpleAuthentication.csproj | 4 +-- 20 files changed, 83 insertions(+), 113 deletions(-) create mode 100644 src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs create mode 100644 src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs 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 3275455..2a979b0 100644 --- a/README.md +++ b/README.md @@ -316,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 repository, 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..594aa29 100644 --- a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj +++ b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj @@ -32,8 +32,8 @@ - - + + 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 - + - + From afc88fbea5fb9cbb8ed6c9b53ec869f905c04a27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:23:05 +0000 Subject: [PATCH 15/19] Merge branch 'develop' into copilot/remove-jwt-bearer-setting Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a979b0..2b5739d 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Authentication can be fully configured adding an _Authentication_ section in the "Issuers": [ "issuer" ], // Optional "Audiences": [ "audience" ], // Optional "ExpirationTime": "01:00:00", // Default: No expiration - "ClockSkew": "00:02:00" // Default: 5 minutes + "ClockSkew": "00:02:00", // Default: 5 minutes }, "ApiKey": { "SchemeName": "ApiKey", // Default: ApiKey From 1dbe514081362f1c6d3f3e85aa9ac1267851f365 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 14:22:08 +0000 Subject: [PATCH 16/19] Merge develop branch and fix roles configuration binding Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs | 2 +- .../BasicAuthentication/BasicAuthenticationSettings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs index fd1c5a7..014ccb3 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs @@ -46,7 +46,7 @@ public class ApiKeySettings : AuthenticationSchemeOptions /// /// /// - public IEnumerable Roles { get; set; } = []; + public IEnumerable? Roles { get; set; } /// /// The collection of valid API keys. diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs index c587301..56b5471 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs @@ -35,7 +35,7 @@ public class BasicAuthenticationSettings : AuthenticationSchemeOptions /// /// /// - public IEnumerable Roles { get; set; } = []; + public IEnumerable? Roles { get; set; } /// /// The collection of authorization credentials. From 6fcbac8f64c087a0e8d69ffde6e160596f88dcf0 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Wed, 15 Oct 2025 17:11:17 +0200 Subject: [PATCH 17/19] Enhance authentication setup and endpoint clarity - Ensure roles in `ApiKey` and `Credential` classes are non-optional and add equality/hash code methods. - Initialize roles and claims with empty lists in settings and validation result classes to prevent null references. - Simplify role claim addition in authentication handlers by removing null checks. - Maintain consistency in `SimpleAuthenticationExtensions` by initializing roles with empty lists if null. --- samples/Controllers/ApiKeySample/Program.cs | 1 + .../BasicAuthenticationSample/Program.cs | 1 + samples/MinimalApis/ApiKeySample/Program.cs | 7 +++-- .../BasicAuthenticationSample/Program.cs | 7 +++-- .../ApiKey/ApiKey.cs | 27 +++++++++++++++++-- .../ApiKey/ApiKeySettings.cs | 4 +-- .../ApiKey/ApiKeyValidationResult.cs | 6 ++--- .../BasicAuthenticationSettings.cs | 4 +-- .../BasicAuthenticationValidationResult.cs | 4 +-- .../BasicAuthentication/Credential.cs | 27 +++++++++++++++++-- .../ApiKey/ApiKeyAuthenticationHandler.cs | 9 +++---- .../BasicAuthenticationHandler.cs | 9 +++---- .../SimpleAuthenticationExtensions.cs | 2 ++ 13 files changed, 79 insertions(+), 29 deletions(-) diff --git a/samples/Controllers/ApiKeySample/Program.cs b/samples/Controllers/ApiKeySample/Program.cs index 903b4e0..45a0b9c 100644 --- a/samples/Controllers/ApiKeySample/Program.cs +++ b/samples/Controllers/ApiKeySample/Program.cs @@ -24,6 +24,7 @@ // .Build()) // .AddPolicy("ApiKey", builder => builder.AddAuthenticationSchemes(ApiKeyDefaults.AuthenticationScheme).RequireAuthenticatedUser()); +// This service is used when there isn't a fixed API Key value in the configuration. builder.Services.AddTransient(); // Uncomment the following line if you have multiple authentication schemes and diff --git a/samples/Controllers/BasicAuthenticationSample/Program.cs b/samples/Controllers/BasicAuthenticationSample/Program.cs index e60b17e..4110079 100644 --- a/samples/Controllers/BasicAuthenticationSample/Program.cs +++ b/samples/Controllers/BasicAuthenticationSample/Program.cs @@ -25,6 +25,7 @@ // .Build()) // .AddPolicy("Basic", builder => builder.AddAuthenticationSchemes(BasicAuthenticationDefaults.AuthenticationScheme).RequireAuthenticatedUser()); +// This service is used when there isn't a fixed user/password value in the configuration. builder.Services.AddTransient(); // Uncomment the following line if you have multiple authentication schemes and diff --git a/samples/MinimalApis/ApiKeySample/Program.cs b/samples/MinimalApis/ApiKeySample/Program.cs index a8c1f42..27c4ba2 100644 --- a/samples/MinimalApis/ApiKeySample/Program.cs +++ b/samples/MinimalApis/ApiKeySample/Program.cs @@ -25,6 +25,7 @@ // .Build()) // .AddPolicy("ApiKey", builder => builder.AddAuthenticationSchemes(ApiKeyDefaults.AuthenticationScheme).RequireAuthenticatedUser()); +// This service is used when there isn't a fixed API Key value in the configuration. builder.Services.AddTransient(); // Uncomment the following line if you have multiple authentication schemes and @@ -63,10 +64,12 @@ }) .RequireAuthorization(); -app.MapGet("api/admin", () => "Administrator access granted") +app.MapGet("api/administrator", () => TypedResults.NoContent()) +.WithDescription("This endpoint requires the user to have the 'Administrator' role.") .RequireAuthorization(policy => policy.RequireRole("Administrator")); -app.MapGet("api/user", () => "User access granted") +app.MapGet("api/user", () => TypedResults.NoContent()) +.WithDescription("This endpoint requires the user to have the 'User' role.") .RequireAuthorization(policy => policy.RequireRole("User")); app.Run(); diff --git a/samples/MinimalApis/BasicAuthenticationSample/Program.cs b/samples/MinimalApis/BasicAuthenticationSample/Program.cs index f4d2410..e47f947 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/Program.cs +++ b/samples/MinimalApis/BasicAuthenticationSample/Program.cs @@ -25,6 +25,7 @@ // .Build()) // .AddPolicy("Basic", builder => builder.AddAuthenticationSchemes(BasicAuthenticationDefaults.AuthenticationScheme).RequireAuthenticatedUser()); +// This service is used when there isn't a fixed user/password value in the configuration. builder.Services.AddTransient(); // Uncomment the following line if you have multiple authentication schemes and @@ -63,10 +64,12 @@ }) .RequireAuthorization(); -app.MapGet("api/admin", () => "Administrator access granted") +app.MapGet("api/administrator", () => TypedResults.NoContent()) +.WithDescription("This endpoint requires the user to have the 'Administrator' role.") .RequireAuthorization(policy => policy.RequireRole("Administrator")); -app.MapGet("api/user", () => "User access granted") +app.MapGet("api/user", () => TypedResults.NoContent()) +.WithDescription("This endpoint requires the user to have the 'User' role.") .RequireAuthorization(policy => policy.RequireRole("User")); app.Run(); diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs index 23c5085..0cc38e4 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs @@ -5,5 +5,28 @@ namespace SimpleAuthentication.ApiKey; /// /// The API key value /// The user name associated with the current key -/// The optional list of roles to assign to the user -public record class ApiKey(string Value, string UserName, IEnumerable? Roles = null); +/// The list of roles to assign to the user +public record class ApiKey(string Value, string UserName, IEnumerable Roles) +{ + /// + public virtual bool Equals(ApiKey? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Value == other.Value && UserName == other.UserName; + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Value, UserName); + } +} diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs index 014ccb3..e64228f 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeySettings.cs @@ -46,7 +46,7 @@ public class ApiKeySettings : AuthenticationSchemeOptions /// /// /// - public IEnumerable? Roles { get; set; } + public IEnumerable Roles { get; set; } = []; /// /// The collection of valid API keys. @@ -79,7 +79,7 @@ internal IEnumerable GetAllApiKeys() if (!string.IsNullOrWhiteSpace(ApiKeyValue) && !string.IsNullOrWhiteSpace(UserName)) { // If necessary, add the API Key from the base properties. - apiKeys.Add(new(ApiKeyValue, UserName, Roles)); + apiKeys.Add(new(ApiKeyValue, UserName, Roles ?? [])); } return apiKeys; diff --git a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeyValidationResult.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeyValidationResult.cs index 09e6329..bb05644 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeyValidationResult.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKeyValidationResult.cs @@ -23,18 +23,18 @@ public class ApiKeyValidationResult /// /// Gets the claim list, if authentication was successful. /// - public IList? Claims { get; } + public IList Claims { get; } = []; /// /// Gets the failure message, if authentication was unsuccessful. /// public string? FailureMessage { get; } - private ApiKeyValidationResult(string userName, IList? claims) + private ApiKeyValidationResult(string userName, IList? claims = null) { Succeeded = true; UserName = userName; - Claims = claims; + Claims = claims ?? []; } private ApiKeyValidationResult(string failureMessage) diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs index 56b5471..42dc199 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationSettings.cs @@ -35,7 +35,7 @@ public class BasicAuthenticationSettings : AuthenticationSchemeOptions /// /// /// - public IEnumerable? Roles { get; set; } + public IEnumerable Roles { get; set; } = []; /// /// The collection of authorization credentials. @@ -68,7 +68,7 @@ internal IEnumerable GetAllCredentials() if (!string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password)) { // If necessary, add the Credentials from the base properties. - credentials.Add(new(UserName, Password, Roles)); + credentials.Add(new(UserName, Password, Roles ?? [])); } return credentials; diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationValidationResult.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationValidationResult.cs index ccc2e4b..d878bd9 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationValidationResult.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/BasicAuthenticationValidationResult.cs @@ -23,7 +23,7 @@ public class BasicAuthenticationValidationResult /// /// Gets the claim list, if authentication was successful. /// - public IList? Claims { get; } + public IList Claims { get; } = []; /// /// Gets the failure message, if authentication was unsuccessful. @@ -34,7 +34,7 @@ private BasicAuthenticationValidationResult(string userName, IList? claim { Succeeded = true; UserName = userName; - Claims = claims; + Claims = claims ?? []; } private BasicAuthenticationValidationResult(string failureMessage) diff --git a/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs b/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs index 9de39f5..8b1471e 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs @@ -5,5 +5,28 @@ namespace SimpleAuthentication.BasicAuthentication; /// /// The user name /// The password -/// The optional list of roles to assign to the user -public record class Credential(string UserName, string Password, IEnumerable? Roles = null); +/// The list of roles to assign to the user +public record class Credential(string UserName, string Password, IEnumerable Roles) +{ + /// + public virtual bool Equals(Credential? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return UserName == other.UserName && Password == other.Password; + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(UserName, Password); + } +} diff --git a/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs b/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs index e32d0c1..8f764c9 100644 --- a/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs +++ b/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs @@ -45,14 +45,11 @@ protected override async Task HandleAuthenticateAsync() if (apiKey is not null) { var claims = new List(); - if (apiKey.Roles is not null) + foreach (var role in apiKey.Roles) { - foreach (var role in apiKey.Roles) - { - claims.Add(new Claim(Options.RoleClaimType, role)); - } + claims.Add(new(Options.RoleClaimType, role)); } - + return CreateAuthenticationSuccessResult(apiKey.UserName, claims); } diff --git a/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs b/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs index 305d425..21bd814 100644 --- a/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs +++ b/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs @@ -59,14 +59,11 @@ protected override async Task HandleAuthenticateAsync() if (credential is not null) { var claims = new List(); - if (credential.Roles is not null) + foreach (var role in credential.Roles) { - foreach (var role in credential.Roles) - { - claims.Add(new Claim(Options.RoleClaimType, role)); - } + claims.Add(new(Options.RoleClaimType, role)); } - + return CreateAuthenticationSuccessResult(credential.UserName, claims); } diff --git a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs index e38ed7a..212ad67 100644 --- a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs +++ b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs @@ -153,6 +153,7 @@ static void CheckAddApiKey(AuthenticationBuilder builder, IConfigurationSection options.ApiKeyValue = settings.ApiKeyValue; options.UserName = settings.UserName; options.ApiKeys = settings.ApiKeys; + options.Roles = settings.Roles ?? []; options.NameClaimType = settings.NameClaimType; options.RoleClaimType = settings.RoleClaimType; }); @@ -192,6 +193,7 @@ static void CheckAddBasicAuthentication(AuthenticationBuilder builder, IConfigur options.SchemeName = settings.SchemeName; options.UserName = settings.UserName; options.Password = settings.Password; + options.Roles = settings.Roles ?? []; options.Credentials = settings.Credentials; options.NameClaimType = settings.NameClaimType; options.RoleClaimType = settings.RoleClaimType; From c80526e93a203d7e67ff67d3b87b99c39811b839 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Wed, 15 Oct 2025 17:38:11 +0200 Subject: [PATCH 18/19] Add role-based authorization to MeController Updated MeController to include role-based authorization by modifying the Get method to accept role settings and return user roles. Added AdministratorOnly and UserOnly endpoints with role-based access. Updated User record to include roles. Extended appsettings.json for role configuration. Adjusted Program.cs for new authorization requirements. --- .../ApiKeySample/Controllers/MeController.cs | 27 ++++++++++++++++--- .../Controllers/ApiKeySample/appsettings.json | 9 +++++-- .../Controllers/MeController.cs | 27 ++++++++++++++++--- .../appsettings.json | 7 +++-- samples/MinimalApis/ApiKeySample/Program.cs | 4 +-- .../BasicAuthenticationSample/Program.cs | 4 +-- .../version.json | 2 +- .../version.json | 2 +- src/SimpleAuthentication/version.json | 2 +- 9 files changed, 67 insertions(+), 17 deletions(-) diff --git a/samples/Controllers/ApiKeySample/Controllers/MeController.cs b/samples/Controllers/ApiKeySample/Controllers/MeController.cs index 40e45ea..8be871c 100644 --- a/samples/Controllers/ApiKeySample/Controllers/MeController.cs +++ b/samples/Controllers/ApiKeySample/Controllers/MeController.cs @@ -1,6 +1,8 @@ using System.Net.Mime; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using SimpleAuthentication.ApiKey; namespace ApiKeySample.Controllers; @@ -13,8 +15,27 @@ public class MeController : ControllerBase [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public ActionResult Get() - => new User(User.Identity!.Name); + public ActionResult Get(IOptions apiKeySettingsOptions) + { + // Get roles using the configured role claim type from options (default is ClaimTypes.Role) + var roles = User.FindAll(apiKeySettingsOptions.Value.RoleClaimType).Select(c => c.Value); + + return new User(User.Identity!.Name, roles); + } + + [Authorize(Roles = "Administrator")] + [HttpGet("administrator")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [EndpointDescription("This endpoint requires the user to have the 'Administrator' role")] + public IActionResult AdministratorOnly() + => NoContent(); + + [Authorize(Roles = "User")] + [HttpGet("user")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [EndpointDescription("This endpoint requires the user to have the 'User' role")] + public IActionResult UserOnly() + => NoContent(); } -public record class User(string? UserName); \ No newline at end of file +public record class User(string? UserName, IEnumerable Roles); \ No newline at end of file diff --git a/samples/Controllers/ApiKeySample/appsettings.json b/samples/Controllers/ApiKeySample/appsettings.json index 80dcbaa..369677f 100644 --- a/samples/Controllers/ApiKeySample/appsettings.json +++ b/samples/Controllers/ApiKeySample/appsettings.json @@ -6,18 +6,23 @@ // You can specify either HeaderName, QueryStringKey or both "HeaderName": "x-api-key", "QueryStringKey": "code", + //"NameClaimType": "user_name", // Default: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name + //"RoleClaimType": "user_role", // Default: http://schemas.microsoft.com/ws/2008/06/identity/claims/role // You can set a fixed API Key for authentication. If you have a single value, you can just use the plain property: "ApiKeyValue": "f1I7S5GXa4wQDgLQWgz0", "UserName": "ApiUser", // Required if ApiKeyValue is used + "Roles": [ "Administrator" ], // Otherwise, you can create an array of ApiKeys: "ApiKeys": [ { "Value": "ArAilHVOoL3upX78Cohq", - "UserName": "alice" + "UserName": "alice", + "Roles": [ "Administrator", "User" ] }, { "Value": "DiUU5EqImTYkxPDAxBVS", - "UserName": "bob" + "UserName": "bob", + "Roles": [ "User" ] } ] // You can also combine both declarations. diff --git a/samples/Controllers/BasicAuthenticationSample/Controllers/MeController.cs b/samples/Controllers/BasicAuthenticationSample/Controllers/MeController.cs index f7a3479..f4d4401 100644 --- a/samples/Controllers/BasicAuthenticationSample/Controllers/MeController.cs +++ b/samples/Controllers/BasicAuthenticationSample/Controllers/MeController.cs @@ -1,6 +1,8 @@ using System.Net.Mime; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using SimpleAuthentication.BasicAuthentication; namespace BasicAuthenticationSample.Controllers; @@ -13,8 +15,27 @@ public class MeController : ControllerBase [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public ActionResult Get() - => new User(User.Identity!.Name); + public ActionResult Get(IOptions basicAuthenticationSettingsOptions) + { + // Get roles using the configured role claim type from options (default is ClaimTypes.Role) + var roles = User.FindAll(basicAuthenticationSettingsOptions.Value.RoleClaimType).Select(c => c.Value); + + return new User(User.Identity!.Name, roles); + } + + [Authorize(Roles = "Administrator")] + [HttpGet("administrator")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [EndpointDescription("This endpoint requires the user to have the 'Administrator' role")] + public IActionResult AdministratorOnly() + => NoContent(); + + [Authorize(Roles = "User")] + [HttpGet("user")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [EndpointDescription("This endpoint requires the user to have the 'User' role")] + public IActionResult UserOnly() + => NoContent(); } -public record class User(string? UserName); \ No newline at end of file +public record class User(string? UserName, IEnumerable Roles); \ No newline at end of file diff --git a/samples/Controllers/BasicAuthenticationSample/appsettings.json b/samples/Controllers/BasicAuthenticationSample/appsettings.json index 334fb96..c65434f 100644 --- a/samples/Controllers/BasicAuthenticationSample/appsettings.json +++ b/samples/Controllers/BasicAuthenticationSample/appsettings.json @@ -8,15 +8,18 @@ //"RoleClaimType": "user_role", // Default: http://schemas.microsoft.com/ws/2008/06/identity/claims/role "UserName": "marco", "Password": "P@$$w0rd", + "Roles": [ "Administrator" ], // Otherwise, you can create an array of Credentials: "Credentials": [ { "UserName": "alice", - "Password": "Password1" + "Password": "Password1", + "Roles": [ "Administrator", "User" ] }, { "UserName": "bob", - "Password": "Password2" + "Password": "Password2", + "Roles": [ "User" ] } ] // You can also combine both declarations. diff --git a/samples/MinimalApis/ApiKeySample/Program.cs b/samples/MinimalApis/ApiKeySample/Program.cs index 27c4ba2..ee0c051 100644 --- a/samples/MinimalApis/ApiKeySample/Program.cs +++ b/samples/MinimalApis/ApiKeySample/Program.cs @@ -65,11 +65,11 @@ .RequireAuthorization(); app.MapGet("api/administrator", () => TypedResults.NoContent()) -.WithDescription("This endpoint requires the user to have the 'Administrator' role.") +.WithDescription("This endpoint requires the user to have the 'Administrator' role") .RequireAuthorization(policy => policy.RequireRole("Administrator")); app.MapGet("api/user", () => TypedResults.NoContent()) -.WithDescription("This endpoint requires the user to have the 'User' role.") +.WithDescription("This endpoint requires the user to have the 'User' role") .RequireAuthorization(policy => policy.RequireRole("User")); app.Run(); diff --git a/samples/MinimalApis/BasicAuthenticationSample/Program.cs b/samples/MinimalApis/BasicAuthenticationSample/Program.cs index e47f947..e1608f9 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/Program.cs +++ b/samples/MinimalApis/BasicAuthenticationSample/Program.cs @@ -65,11 +65,11 @@ .RequireAuthorization(); app.MapGet("api/administrator", () => TypedResults.NoContent()) -.WithDescription("This endpoint requires the user to have the 'Administrator' role.") +.WithDescription("This endpoint requires the user to have the 'Administrator' role") .RequireAuthorization(policy => policy.RequireRole("Administrator")); app.MapGet("api/user", () => TypedResults.NoContent()) -.WithDescription("This endpoint requires the user to have the 'User' role.") +.WithDescription("This endpoint requires the user to have the 'User' role") .RequireAuthorization(policy => policy.RequireRole("User")); app.Run(); diff --git a/src/SimpleAuthentication.Abstractions/version.json b/src/SimpleAuthentication.Abstractions/version.json index 13a806f..62a74ce 100644 --- a/src/SimpleAuthentication.Abstractions/version.json +++ b/src/SimpleAuthentication.Abstractions/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.0", + "version": "3.1", "publicReleaseRefSpec": [ "^refs/heads/master$" // we release out of master ], diff --git a/src/SimpleAuthentication.Swashbuckle/version.json b/src/SimpleAuthentication.Swashbuckle/version.json index 13a806f..62a74ce 100644 --- a/src/SimpleAuthentication.Swashbuckle/version.json +++ b/src/SimpleAuthentication.Swashbuckle/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.0", + "version": "3.1", "publicReleaseRefSpec": [ "^refs/heads/master$" // we release out of master ], diff --git a/src/SimpleAuthentication/version.json b/src/SimpleAuthentication/version.json index 13a806f..62a74ce 100644 --- a/src/SimpleAuthentication/version.json +++ b/src/SimpleAuthentication/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.0", + "version": "3.1", "publicReleaseRefSpec": [ "^refs/heads/master$" // we release out of master ], From 8943e6dcd60cae3062cffce13d945eaec45e36b9 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Thu, 16 Oct 2025 09:36:08 +0200 Subject: [PATCH 19/19] Enhance parameter validation logic Replaced `ArgumentNullException.ThrowIfNull` with `ArgumentException.ThrowIfNullOrWhiteSpace` across multiple files to ensure parameters are not null, empty, or whitespace. Updated `SwaggerExtensions.cs` and `OpenApiExtensions.cs` to use this validation for `sectionName`. In `SimpleAuthenticationExtensions.cs`, adjusted `defaultAuthenticationScheme` handling and improved validation for `JwtBearerSettings`, `ApiKeySettings`, and `BasicAuthenticationSettings` properties. These changes prevent runtime errors by ensuring meaningful content in string parameters. --- .../SwaggerExtensions.cs | 2 +- src/SimpleAuthentication/OpenApiExtensions.cs | 2 +- .../SimpleAuthenticationExtensions.cs | 38 ++++++++++--------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/SimpleAuthentication.Swashbuckle/SwaggerExtensions.cs b/src/SimpleAuthentication.Swashbuckle/SwaggerExtensions.cs index 2294f81..e20dc20 100644 --- a/src/SimpleAuthentication.Swashbuckle/SwaggerExtensions.cs +++ b/src/SimpleAuthentication.Swashbuckle/SwaggerExtensions.cs @@ -77,7 +77,7 @@ public static void AddSimpleAuthentication(this SwaggerGenOptions options, IConf { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(sectionName); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); // Adds a security definition for each authentication method that has been configured. CheckAddJwtBearer(options, configuration.GetSection($"{sectionName}:JwtBearer")); diff --git a/src/SimpleAuthentication/OpenApiExtensions.cs b/src/SimpleAuthentication/OpenApiExtensions.cs index 5e3bbfa..4837d46 100644 --- a/src/SimpleAuthentication/OpenApiExtensions.cs +++ b/src/SimpleAuthentication/OpenApiExtensions.cs @@ -73,7 +73,7 @@ public static void AddSimpleAuthentication(this OpenApiOptions options, IConfigu { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(sectionName); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); options.AddDocumentTransformer(new AuthenticationDocumentTransformer(configuration, sectionName, additionalSecurityRequirements)); options.AddDocumentTransformer(); diff --git a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs index 212ad67..e5d76d5 100644 --- a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs +++ b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs @@ -35,9 +35,13 @@ public static AuthenticationBuilder AddSimpleAuthentication(this IServiceCollect { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(sectionName); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); - var defaultAuthenticationScheme = configuration.GetValue($"{sectionName}:DefaultScheme"); + var defaultAuthenticationScheme = configuration.GetValue($"{sectionName}:DefaultScheme"); + if (string.IsNullOrWhiteSpace(defaultAuthenticationScheme)) + { + defaultAuthenticationScheme = null; // treat empty/whitespace as no value. + } var builder = services.AddAuthentication(options => { @@ -65,7 +69,7 @@ public static AuthenticationBuilder AddSimpleAuthentication(this AuthenticationB { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(sectionName); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); if (addAuthorizationServices) { @@ -86,11 +90,11 @@ static void CheckAddJwtBearer(AuthenticationBuilder builder, IConfigurationSecti return; } - ArgumentNullException.ThrowIfNull(settings.SchemeName, nameof(JwtBearerSettings.SchemeName)); - ArgumentNullException.ThrowIfNull(settings.SecurityKey, nameof(JwtBearerSettings.SecurityKey)); - ArgumentNullException.ThrowIfNull(settings.Algorithm, nameof(JwtBearerSettings.Algorithm)); - ArgumentNullException.ThrowIfNull(settings.NameClaimType, nameof(JwtBearerSettings.NameClaimType)); - ArgumentNullException.ThrowIfNull(settings.RoleClaimType, nameof(JwtBearerSettings.RoleClaimType)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.SchemeName, nameof(JwtBearerSettings.SchemeName)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.SecurityKey, nameof(JwtBearerSettings.SecurityKey)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.Algorithm, nameof(JwtBearerSettings.Algorithm)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.NameClaimType, nameof(JwtBearerSettings.NameClaimType)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.RoleClaimType, nameof(JwtBearerSettings.RoleClaimType)); builder.Services.Configure(section); @@ -124,9 +128,9 @@ static void CheckAddApiKey(AuthenticationBuilder builder, IConfigurationSection return; } - ArgumentNullException.ThrowIfNull(settings.SchemeName, nameof(ApiKeySettings.SchemeName)); - ArgumentNullException.ThrowIfNull(settings.NameClaimType, nameof(JwtBearerSettings.NameClaimType)); - ArgumentNullException.ThrowIfNull(settings.RoleClaimType, nameof(JwtBearerSettings.RoleClaimType)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.SchemeName, nameof(ApiKeySettings.SchemeName)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.NameClaimType, nameof(JwtBearerSettings.NameClaimType)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.RoleClaimType, nameof(JwtBearerSettings.RoleClaimType)); if (string.IsNullOrWhiteSpace(settings.HeaderName) && string.IsNullOrWhiteSpace(settings.QueryStringKey)) { @@ -135,7 +139,7 @@ static void CheckAddApiKey(AuthenticationBuilder builder, IConfigurationSection if (!string.IsNullOrWhiteSpace(settings.ApiKeyValue)) { - ArgumentNullException.ThrowIfNull(settings.UserName, nameof(ApiKeySettings.UserName)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.UserName, nameof(ApiKeySettings.UserName)); } if (settings.ApiKeys.Any(k => string.IsNullOrWhiteSpace(k.Value) || string.IsNullOrWhiteSpace(k.UserName))) @@ -167,18 +171,18 @@ static void CheckAddBasicAuthentication(AuthenticationBuilder builder, IConfigur return; } - ArgumentNullException.ThrowIfNull(settings.SchemeName, nameof(BasicAuthenticationSettings.SchemeName)); - ArgumentNullException.ThrowIfNull(settings.NameClaimType, nameof(JwtBearerSettings.NameClaimType)); - ArgumentNullException.ThrowIfNull(settings.RoleClaimType, nameof(JwtBearerSettings.RoleClaimType)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.SchemeName, nameof(BasicAuthenticationSettings.SchemeName)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.NameClaimType, nameof(JwtBearerSettings.NameClaimType)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.RoleClaimType, nameof(JwtBearerSettings.RoleClaimType)); if (!string.IsNullOrWhiteSpace(settings.UserName)) { - ArgumentNullException.ThrowIfNull(settings.Password, nameof(BasicAuthenticationSettings.Password)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.Password, nameof(BasicAuthenticationSettings.Password)); } if (!string.IsNullOrWhiteSpace(settings.Password)) { - ArgumentNullException.ThrowIfNull(settings.UserName, nameof(BasicAuthenticationSettings.UserName)); + ArgumentException.ThrowIfNullOrWhiteSpace(settings.UserName, nameof(BasicAuthenticationSettings.UserName)); } if (settings.Credentials.Any(c => string.IsNullOrWhiteSpace(c.UserName) || string.IsNullOrWhiteSpace(c.Password)))