diff --git a/README.md b/README.md index cadcb55..393cbf6 100644 --- a/README.md +++ b/README.md @@ -138,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", async (LoginRequest loginRequest, IJwtBearerService jwtBearerService) => @@ -198,6 +198,76 @@ When using API Key or Basic Authentication, you can specify multiple fixed value With this configuration, authentication will succeed 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. + +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": { + "ApiKey": { + "ApiKeys": [ + { + "Value": "key-1", + "UserName": "UserName1", + "Roles": ["Administrator", "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 = "Administrator")] +public IActionResult AdminEndpoint() +{ + return Ok("Administrator access granted"); +} + +// Or with minimal APIs +app.MapGet("/admin", () => "Administrator access granted") + .RequireAuthorization(policy => policy.RequireRole("Administrator")); +``` + **Custom Authentication logic for API Keys and Basic Authentication** 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): @@ -316,4 +386,4 @@ app.MapGet("api/me", (ClaimsPrincipal user) => ## Contribute -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. +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/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/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/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/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/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/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/ApiKeySample/Program.cs b/samples/MinimalApis/ApiKeySample/Program.cs index be9ea71..ee0c051 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; @@ -24,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 @@ -55,16 +57,24 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("api/me", (ClaimsPrincipal user) => +app.MapGet("api/me", (ClaimsPrincipal user, IOptions options) => { - return TypedResults.Ok(new User(user.Identity!.Name)); + 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/administrator", () => TypedResults.NoContent()) +.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") +.RequireAuthorization(policy => policy.RequireRole("User")); 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..369677f 100644 --- a/samples/MinimalApis/ApiKeySample/appsettings.json +++ b/samples/MinimalApis/ApiKeySample/appsettings.json @@ -11,15 +11,18 @@ // 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/MinimalApis/BasicAuthenticationSample/Program.cs b/samples/MinimalApis/BasicAuthenticationSample/Program.cs index 1e0ab2f..e1608f9 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; @@ -24,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 @@ -55,16 +57,24 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("api/me", (ClaimsPrincipal user) => +app.MapGet("api/me", (ClaimsPrincipal user, IOptions options) => { - return TypedResults.Ok(new User(user.Identity!.Name)); + 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/administrator", () => TypedResults.NoContent()) +.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") +.RequireAuthorization(policy => policy.RequireRole("User")); 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..c65434f 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/appsettings.json +++ b/samples/MinimalApis/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/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/ApiKey/ApiKey.cs b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs index 428322e..0cc38e4 100644 --- a/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs +++ b/src/SimpleAuthentication.Abstractions/ApiKey/ApiKey.cs @@ -1,8 +1,32 @@ -namespace SimpleAuthentication.ApiKey; +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); +/// 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 86c8b21..e64228f 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 IEnumerable Roles { get; set; } = []; + /// /// The collection of valid API keys. /// @@ -72,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)); + 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 27e6757..42dc199 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 IEnumerable Roles { get; set; } = []; + /// /// The collection of authorization credentials. /// @@ -61,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)); + 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 3519fb1..8b1471e 100644 --- a/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs +++ b/src/SimpleAuthentication.Abstractions/BasicAuthentication/Credential.cs @@ -1,8 +1,32 @@ -namespace SimpleAuthentication.BasicAuthentication; +namespace SimpleAuthentication.BasicAuthentication; /// /// Store credentials used for Basic Authentication. /// /// The user name /// The password -public record class Credential(string UserName, string Password); +/// 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.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.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/SimpleAuthentication.Swashbuckle.csproj b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj index 55a05d3..2fe4a5a 100644 --- a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj +++ b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj @@ -32,7 +32,7 @@ - + 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.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/ApiKey/ApiKeyAuthenticationHandler.cs b/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs index 733949b..8f764c9 100644 --- a/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs +++ b/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs @@ -44,7 +44,13 @@ protected override async Task HandleAuthenticateAsync() var apiKey = apiKeys.FirstOrDefault(c => c.Value == value); if (apiKey is not null) { - return CreateAuthenticationSuccessResult(apiKey.UserName); + var claims = new List(); + foreach (var role in apiKey.Roles) + { + claims.Add(new(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 ab9e6e8..21bd814 100644 --- a/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs +++ b/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs @@ -58,7 +58,13 @@ protected override async Task HandleAuthenticateAsync() var credential = credentials.FirstOrDefault(c => c.UserName == userName && c.Password == password); if (credential is not null) { - return CreateAuthenticationSuccessResult(credential.UserName); + var claims = new List(); + foreach (var role in credential.Roles) + { + claims.Add(new(Options.RoleClaimType, role)); + } + + return CreateAuthenticationSuccessResult(credential.UserName, claims); } return AuthenticateResult.Fail("Invalid user name or password"); 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/SimpleAuthentication.csproj b/src/SimpleAuthentication/SimpleAuthentication.csproj index 0ba57c8..279270f 100644 --- a/src/SimpleAuthentication/SimpleAuthentication.csproj +++ b/src/SimpleAuthentication/SimpleAuthentication.csproj @@ -34,6 +34,10 @@ + + + + True @@ -41,9 +45,5 @@ - - - - diff --git a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs index 6881be7..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); @@ -113,10 +117,7 @@ static void CheckAddJwtBearer(AuthenticationBuilder builder, IConfigurationSecti }; }); - if (settings.EnableJwtBearerService) - { - builder.Services.TryAddSingleton(); - } + builder.Services.TryAddSingleton(); } static void CheckAddApiKey(AuthenticationBuilder builder, IConfigurationSection section) @@ -127,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)) { @@ -138,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))) @@ -156,6 +157,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; }); @@ -169,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))) @@ -195,6 +197,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; 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 ],