diff --git a/README.md b/README.md
index 393cbf6..d2cbe52 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://github.com/marcominerva/SimpleAuthentication/actions/workflows/linter.yml)
[](https://github.com/marcominerva/SimpleAuthentication/actions/workflows/github-code-scanning/codeql)
[](https://www.nuget.org/packages/SimpleAuthenticationTools)
-[](https://www.nuget.org/packages/SimpleAuthenticationTools)
+[](https://www.nuget.org/packages/SimpleAuthenticationTools)
[](https://github.com/marcominerva/SimpleAuthentication/blob/master/LICENSE)
A library to easily integrate Authentication in ASP.NET Core projects. Currently it supports JWT Bearer, API Key and Basic Authentication in both Controller-based and Minimal API projects.
diff --git a/samples/Controllers/ApiKeySample/ApiKeySample.csproj b/samples/Controllers/ApiKeySample/ApiKeySample.csproj
index a312592..b83d877 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 a312592..b83d877 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 a312592..b83d877 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 b50f1e3..aa34a16 100644
--- a/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj
+++ b/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/samples/MinimalApis/ApiKeySample/Program.cs b/samples/MinimalApis/ApiKeySample/Program.cs
index ee0c051..9b3f128 100644
--- a/samples/MinimalApis/ApiKeySample/Program.cs
+++ b/samples/MinimalApis/ApiKeySample/Program.cs
@@ -80,10 +80,15 @@ public class CustomApiKeyValidator : IApiKeyValidator
{
public Task ValidateAsync(string apiKey)
{
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.Role, "User")
+ };
+
var result = apiKey switch
{
- "ArAilHVOoL3upX78Cohq" => ApiKeyValidationResult.Success("User 1"),
- "DiUU5EqImTYkxPDAxBVS" => ApiKeyValidationResult.Success("User 2"),
+ "ArAilHVOoL3upX78Cohq" => ApiKeyValidationResult.Success("User 1", claims),
+ "DiUU5EqImTYkxPDAxBVS" => ApiKeyValidationResult.Success("User 2", claims),
_ => ApiKeyValidationResult.Fail("Invalid User")
};
diff --git a/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj b/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj
index b50f1e3..aa34a16 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 b50f1e3..aa34a16 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 17c6dda..f718c99 100644
--- a/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj
+++ b/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj
index c05a05e..cfe44cb 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 a3acb67..4a79bc2 100644
--- a/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs
+++ b/src/SimpleAuthentication/ApiKey/ApiKeyAuthenticationHandler.cs
@@ -1,4 +1,6 @@
-using System.Security.Claims;
+using System.Runtime.InteropServices;
+using System.Security.Claims;
+using System.Security.Cryptography;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
@@ -33,37 +35,40 @@ protected override async Task HandleAuthenticateAsync()
var validator = serviceProvider.GetService() ?? throw new InvalidOperationException("There isn't a default value for API Key and no custom validator has been provided");
var validationResult = await validator.ValidateAsync(value.ToString());
- if (validationResult.Succeeded)
+ if (!validationResult.Succeeded)
{
- return CreateAuthenticationSuccessResult(validationResult.UserName, validationResult.Claims);
+ return AuthenticateResult.Fail(validationResult.FailureMessage);
}
- return AuthenticateResult.Fail(validationResult.FailureMessage);
+ return CreateAuthenticationSuccessResult(validationResult.UserName, validationResult.Claims);
}
- var apiKey = apiKeys.FirstOrDefault(c => c.Value == value);
- if (apiKey is not null)
+ var providedApiKey = value.ToString();
+ var apiKey = apiKeys.FirstOrDefault(a => CryptographicOperations.FixedTimeEquals(MemoryMarshal.AsBytes(a.Value.AsSpan()), MemoryMarshal.AsBytes(providedApiKey.AsSpan())));
+
+ if (apiKey is null)
+ {
+
+ return AuthenticateResult.Fail("Invalid API Key");
+ }
+
+ var claims = new List();
+ if (apiKey.Roles 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(Options.RoleClaimType, role));
- }
+ claims.Add(new(Options.RoleClaimType, role));
}
-
- return CreateAuthenticationSuccessResult(apiKey.UserName, claims);
}
- return AuthenticateResult.Fail("Invalid API Key");
+ return CreateAuthenticationSuccessResult(apiKey.UserName, claims);
- AuthenticateResult CreateAuthenticationSuccessResult(string userName, IList? claims = null)
+ AuthenticateResult CreateAuthenticationSuccessResult(string userName, IEnumerable? claims = null)
{
- claims ??= [];
- claims.Update(Options.NameClaimType, userName);
+ var claimsList = claims?.ToList() ?? [];
+ claimsList.Update(Options.NameClaimType, userName);
- var identity = new ClaimsIdentity(claims, Scheme.Name, Options.NameClaimType, Options.RoleClaimType);
+ var identity = new ClaimsIdentity(claimsList, Scheme.Name, Options.NameClaimType, Options.RoleClaimType);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
diff --git a/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs b/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs
index f8e54c0..ef377be 100644
--- a/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs
+++ b/src/SimpleAuthentication/BasicAuthentication/BasicAuthenticationHandler.cs
@@ -1,4 +1,6 @@
-using System.Security.Claims;
+using System.Runtime.InteropServices;
+using System.Security.Claims;
+using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
@@ -47,37 +49,39 @@ protected override async Task HandleAuthenticateAsync()
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");
var validationResult = await validator.ValidateAsync(userName, password);
- if (validationResult.Succeeded)
+ if (!validationResult.Succeeded)
{
- return CreateAuthenticationSuccessResult(validationResult.UserName, validationResult.Claims);
+ return AuthenticateResult.Fail(validationResult.FailureMessage);
}
- return AuthenticateResult.Fail(validationResult.FailureMessage);
+ return CreateAuthenticationSuccessResult(validationResult.UserName, validationResult.Claims);
}
- var credential = credentials.FirstOrDefault(c => c.UserName == userName && c.Password == password);
- if (credential is not null)
+ var credential = credentials.FirstOrDefault(c => CryptographicOperations.FixedTimeEquals(MemoryMarshal.AsBytes(c.UserName.AsSpan()), MemoryMarshal.AsBytes(userName.AsSpan()))
+ && CryptographicOperations.FixedTimeEquals(MemoryMarshal.AsBytes(c.Password.AsSpan()), MemoryMarshal.AsBytes(password.AsSpan())));
+
+ if (credential is null)
+ {
+ return AuthenticateResult.Fail("Invalid user name or password");
+ }
+
+ var claims = new List();
+ if (credential.Roles 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(Options.RoleClaimType, role));
- }
+ claims.Add(new(Options.RoleClaimType, role));
}
-
- return CreateAuthenticationSuccessResult(credential.UserName, claims);
}
- return AuthenticateResult.Fail("Invalid user name or password");
+ return CreateAuthenticationSuccessResult(credential.UserName, claims);
- AuthenticateResult CreateAuthenticationSuccessResult(string userName, IList? claims = null)
+ AuthenticateResult CreateAuthenticationSuccessResult(string userName, IEnumerable? claims = null)
{
- claims ??= [];
- claims.Update(Options.NameClaimType, userName);
+ var claimsList = claims?.ToList() ?? [];
+ claimsList.Update(Options.NameClaimType, userName);
- var identity = new ClaimsIdentity(claims, Scheme.Name, Options.NameClaimType, Options.RoleClaimType);
+ var identity = new ClaimsIdentity(claimsList, Scheme.Name, Options.NameClaimType, Options.RoleClaimType);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
@@ -86,6 +90,6 @@ AuthenticateResult CreateAuthenticationSuccessResult(string userName, IList jwtBearerSettingsOptio
protected JwtBearerSettings JwtBearerSettings { get; } = jwtBearerSettingsOptions?.Value ?? throw new ArgumentNullException(nameof(jwtBearerSettingsOptions));
///
- public virtual Task CreateTokenAsync(string userName, IList? claims = null, string? issuer = null, string? audience = null, DateTime? absoluteExpiration = null)
+ public virtual Task CreateTokenAsync(string userName, IEnumerable? claims = null, string? issuer = null, string? audience = null, DateTime? absoluteExpiration = null)
{
var now = DateTime.UtcNow;
@@ -27,13 +27,13 @@ public virtual Task CreateTokenAsync(string userName, IList? clai
throw new ArgumentException("The expiration date must be greater than or equal to the current date and time.", nameof(absoluteExpiration));
}
- claims ??= [];
- claims.Update(JwtBearerSettings.NameClaimType, userName);
- claims.Update(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString());
+ var claimsList = claims?.ToList() ?? [];
+ claimsList.Update(JwtBearerSettings.NameClaimType, userName);
+ claimsList.Update(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString());
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
- Subject = new ClaimsIdentity(claims, JwtBearerSettings.SchemeName, JwtBearerSettings.NameClaimType, JwtBearerSettings.RoleClaimType),
+ Subject = new ClaimsIdentity(claimsList, JwtBearerSettings.SchemeName, JwtBearerSettings.NameClaimType, JwtBearerSettings.RoleClaimType),
Issuer = issuer ?? JwtBearerSettings.Issuers?.FirstOrDefault(),
Audience = audience ?? JwtBearerSettings.Audiences?.FirstOrDefault(),
IssuedAt = now,
@@ -63,9 +63,9 @@ public virtual async Task ValidateTokenAsync(string token, bool
AuthenticationType = JwtBearerSettings.SchemeName,
NameClaimType = JwtBearerSettings.NameClaimType,
RoleClaimType = JwtBearerSettings.RoleClaimType,
- ValidateIssuer = JwtBearerSettings.Issuers?.Any() ?? false,
+ ValidateIssuer = JwtBearerSettings.Issuers?.Length > 0,
ValidIssuers = JwtBearerSettings.Issuers,
- ValidateAudience = JwtBearerSettings.Audiences?.Any() ?? false,
+ ValidateAudience = JwtBearerSettings.Audiences?.Length > 0,
ValidAudiences = JwtBearerSettings.Audiences,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtBearerSettings.SecurityKey)),
@@ -89,7 +89,7 @@ public virtual async Task ValidateTokenAsync(string token, bool
public virtual async Task RefreshTokenAsync(string token, bool validateLifetime, DateTime? absoluteExpiration = null)
{
var principal = await ValidateTokenAsync(token, validateLifetime);
- var claims = (principal.Identity as ClaimsIdentity)!.Claims.ToList();
+ var claims = (principal.Identity as ClaimsIdentity)!.Claims;
var userName = claims.First(c => c.Type == JwtBearerSettings.NameClaimType).Value;
var issuer = claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Iss)?.Value;
diff --git a/src/SimpleAuthentication/SimpleAuthentication.csproj b/src/SimpleAuthentication/SimpleAuthentication.csproj
index aef11b0..413c0c8 100644
--- a/src/SimpleAuthentication/SimpleAuthentication.csproj
+++ b/src/SimpleAuthentication/SimpleAuthentication.csproj
@@ -31,15 +31,15 @@
-
+
-
+
-
+