Skip to content

Commit

Permalink
make ISecurityKeyValidator async
Browse files Browse the repository at this point in the history
  • Loading branch information
pwelter34 committed Dec 12, 2024
1 parent e2d1960 commit 3f97600
Show file tree
Hide file tree
Showing 15 changed files with 104 additions and 53 deletions.
2 changes: 1 addition & 1 deletion samples/Sample.Controllers/Controllers.http
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@HostAddress = http://localhost:5154
@HostAddress = https://localhost:7236

###

Expand Down
8 changes: 7 additions & 1 deletion samples/Sample.Middleware/Middleware.http
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@HostAddress = http://localhost:5284
@HostAddress = https://localhost:7006

###

Expand All @@ -17,3 +17,9 @@ x-api-key: 01HSGVAH2M5WVQYG4YPT7FNK4K8
GET {{HostAddress}}/addresses/
Accept: application/json
X-API-KEY: 01hsgvah2m5wvqyg4ypt7fnk4k8

###

GET {{HostAddress}}/addresses/
Accept: application/json
X-API-KEY: badkey
2 changes: 1 addition & 1 deletion samples/Sample.MinimalApi/MinimalApi.http
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@HostAddress = http://localhost:5009
@HostAddress = https://localhost:7216

###

Expand Down
7 changes: 5 additions & 2 deletions samples/Sample.MinimalApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ public static void Main(string[] args)

app.MapGet("/current", (ClaimsPrincipal? principal) =>
{
var p = principal;
return WeatherFaker.Instance.Generate(5);
return new
{
Name = principal?.Identity?.Name,
Data = WeatherFaker.Instance.Generate(5)
};
})
.WithName("GetCurrentUser")
.WithOpenApi();
Expand Down
2 changes: 1 addition & 1 deletion src/AspNetCore.SecurityKey/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static class ApplicationBuilderExtensions
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder" /> instance this method extends</param>
/// <returns>
/// The <see cref="IApplicationBuilder" /> for requirirng security API keys
/// The <see cref="IApplicationBuilder" /> for requiring security API keys
/// </returns>
/// <exception cref="System.ArgumentNullException">builder is null</exception>
public static IApplicationBuilder UseSecurityKey(this IApplicationBuilder builder)
Expand Down
4 changes: 2 additions & 2 deletions src/AspNetCore.SecurityKey/AuthenticationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static AuthenticationBuilder AddSecurityKey(this AuthenticationBuilder bu
=> builder.AddSecurityKey(SecurityKeyAuthenticationDefaults.AuthenticationScheme, configureOptions);

/// <summary>
/// Adds security api key authentication to <see cref="AuthenticationBuilder"/> using the specified scheme.
/// Adds security API key authentication to <see cref="AuthenticationBuilder"/> using the specified scheme.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
/// <param name="authenticationScheme">The authentication scheme.</param>
Expand All @@ -47,7 +47,7 @@ public static AuthenticationBuilder AddSecurityKey(this AuthenticationBuilder bu
=> builder.AddSecurityKey(authenticationScheme, displayName: null, configureOptions: configureOptions);

/// <summary>
/// Adds security api key authentication to <see cref="AuthenticationBuilder"/> using the specified scheme.
/// Adds security API key authentication to <see cref="AuthenticationBuilder"/> using the specified scheme.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
/// <param name="authenticationScheme">The authentication scheme.</param>
Expand Down
9 changes: 4 additions & 5 deletions src/AspNetCore.SecurityKey/ISecurityKeyValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ public interface ISecurityKeyValidator
/// </summary>
/// <param name="value">The security API key to validate.</param>
/// <returns>true if security API key is valid; otherwise false</returns>
bool Validate(string? value);
ValueTask<bool> Validate(string? value);

/// <summary>
/// Validates the specified security API key.
/// Authenticate the specified security API key.
/// </summary>
/// <param name="value">The security API key to validate.</param>
/// <param name="claims">The claims associated with the security API key.</param>
/// <returns>
/// true if security API key is valid; otherwise false
/// <see cref="ClaimsIdentity"/> result of the authentication
/// </returns>
bool Validate(string? value, out Claim[] claims);
ValueTask<ClaimsIdentity> Authenticate(string? value);
}
11 changes: 5 additions & 6 deletions src/AspNetCore.SecurityKey/SecurityKeyAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,21 @@ public SecurityKeyAuthenticationHandler(
#pragma warning restore CS0618

/// <inheritdoc />
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var securityKey = _securityKeyExtractor.GetKey(Context);
var identity = await _securityKeyValidator.Authenticate(securityKey);

if (!_securityKeyValidator.Validate(securityKey, out var claims))
if (!identity.IsAuthenticated)
{
SecurityKeyLogger.InvalidSecurityKey(Logger, securityKey);
return Task.FromResult(AuthenticateResult.Fail("Invalid Security Key"));
return AuthenticateResult.Fail("Invalid Security Key");
}

// create a user claim for the security key
var identity = new ClaimsIdentity(claims, SecurityKeyAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);

var ticket = new AuthenticationTicket(principal, Scheme.Name);

return Task.FromResult(AuthenticateResult.Success(ticket));
return AuthenticateResult.Success(ticket);
}
}
12 changes: 6 additions & 6 deletions src/AspNetCore.SecurityKey/SecurityKeyAuthorizationFilter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

using AspNetCore.Extensions.Authentication;

using Microsoft.AspNetCore.Mvc;
Expand All @@ -9,8 +10,8 @@ namespace AspNetCore.SecurityKey;
/// <summary>
/// A filter that requiring security API key authorization.
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAuthorizationFilter" />
public class SecurityKeyAuthorizationFilter : IAuthorizationFilter
/// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncAuthorizationFilter" />
public class SecurityKeyAuthorizationFilter : IAsyncAuthorizationFilter
{
private readonly ISecurityKeyExtractor _securityKeyExtractor;
private readonly ISecurityKeyValidator _securityKeyValidator;
Expand All @@ -33,14 +34,13 @@ public SecurityKeyAuthorizationFilter(
}

/// <inheritdoc />
public void OnAuthorization(AuthorizationFilterContext context)
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (context is null)
throw new ArgumentNullException(nameof(context));
ArgumentNullException.ThrowIfNull(context);

var securityKey = _securityKeyExtractor.GetKey(context.HttpContext);

if (_securityKeyValidator.Validate(securityKey))
if (await _securityKeyValidator.Validate(securityKey))
return;

SecurityKeyLogger.InvalidSecurityKey(_logger, securityKey);
Expand Down
9 changes: 4 additions & 5 deletions src/AspNetCore.SecurityKey/SecurityKeyEndpointFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class SecurityKeyEndpointFilter : IEndpointFilter
{
private readonly ISecurityKeyExtractor _securityKeyExtractor;
private readonly ISecurityKeyValidator _securityKeyValidator;
private readonly ILogger<SecurityKeyAuthorizationFilter> _logger;
private readonly ILogger<SecurityKeyEndpointFilter> _logger;

/// <summary>
/// Initializes a new instance of the <see cref="SecurityKeyEndpointFilter"/> class.
Expand All @@ -24,7 +24,7 @@ public class SecurityKeyEndpointFilter : IEndpointFilter
public SecurityKeyEndpointFilter(
ISecurityKeyExtractor securityKeyExtractor,
ISecurityKeyValidator securityKeyValidator,
ILogger<SecurityKeyAuthorizationFilter> logger)
ILogger<SecurityKeyEndpointFilter> logger)
{
_securityKeyExtractor = securityKeyExtractor ?? throw new ArgumentNullException(nameof(securityKeyExtractor));
_securityKeyValidator = securityKeyValidator ?? throw new ArgumentNullException(nameof(securityKeyValidator));
Expand All @@ -34,12 +34,11 @@ public SecurityKeyEndpointFilter(
/// <inheritdoc />
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
if (context is null)
throw new ArgumentNullException(nameof(context));
ArgumentNullException.ThrowIfNull(context);

var securityKey = _securityKeyExtractor.GetKey(context.HttpContext);

if (_securityKeyValidator.Validate(securityKey))
if (await _securityKeyValidator.Validate(securityKey))
return await next(context);

SecurityKeyLogger.InvalidSecurityKey(_logger, securityKey);
Expand Down
2 changes: 1 addition & 1 deletion src/AspNetCore.SecurityKey/SecurityKeyMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public async Task InvokeAsync(HttpContext context)

var securityKey = _securityKeyExtractor.GetKey(context);

if (_securityKeyValidator.Validate(securityKey))
if (await _securityKeyValidator.Validate(securityKey))
{
await _next(context).ConfigureAwait(false);
return;
Expand Down
26 changes: 26 additions & 0 deletions src/AspNetCore.SecurityKey/SecurityKeyOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Security.Claims;

namespace AspNetCore.SecurityKey;

/// <summary>
Expand Down Expand Up @@ -44,4 +46,28 @@ public class SecurityKeyOptions
/// The key comparer for validating the security key
/// </value>
public IEqualityComparer<string> KeyComparer { get; set; } = StringComparer.OrdinalIgnoreCase;

/// <summary>
/// Gets or sets the authentication scheme used.
/// </summary>
/// <value>
/// The authentication scheme.
/// </value>
public string AuthenticationScheme { get; set; } = SecurityKeyAuthenticationDefaults.AuthenticationScheme;

/// <summary>
/// Gets or sets the <see cref="Claim.Type"/> used when obtaining the value of <see cref="ClaimsIdentity.Name"/>
/// </summary>
/// <value>
/// The <see cref="Claim.Type"/> used when obtaining the value of <see cref="ClaimsIdentity.Name"/>
/// </value>
public string ClaimNameType { get; set; } = ClaimTypes.Name;

/// <summary>
/// Gets or sets the <see cref="Claim.Type"/> used when performing logic for <see cref="ClaimsPrincipal.IsInRole"/>
/// </summary>
/// <value>
/// The <see cref="Claim.Type"/> used when performing logic for <see cref="ClaimsPrincipal.IsInRole"/>
/// </value>
public string ClaimRoleType { get; set; } = ClaimTypes.Role;
}
26 changes: 16 additions & 10 deletions src/AspNetCore.SecurityKey/SecurityKeyValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ namespace AspNetCore.SecurityKey;
/// <seealso cref="AspNetCore.SecurityKey.ISecurityKeyValidator" />
public class SecurityKeyValidator : ISecurityKeyValidator
{
private static readonly Claim[] _defaultClaims = [new Claim(ClaimTypes.Name, "Security Key")];

private readonly IConfiguration _configuration;
private readonly SecurityKeyOptions _securityKeyOptions;
private readonly ILogger<SecurityKeyValidator> _logger;

private readonly Lazy<HashSet<string>> _validKeys;
private readonly Claim[] _claims;

/// <summary>
/// Initializes a new instance of the <see cref="SecurityKeyValidator"/> class.
Expand All @@ -39,24 +38,31 @@ public SecurityKeyValidator(

// one-time extract keys
_validKeys = new Lazy<HashSet<string>>(ExractKeys);
_claims = [new Claim(_securityKeyOptions.ClaimNameType, "SecurityKey")];
}

/// <inheritdoc />
public bool Validate(string? value)
public async ValueTask<ClaimsIdentity> Authenticate(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
var isValid = await Validate(value);

return _validKeys.Value.Contains(value);
if (!isValid)
return new ClaimsIdentity();

return new ClaimsIdentity(
claims: _claims,
authenticationType: _securityKeyOptions.AuthenticationScheme,
nameType: _securityKeyOptions.ClaimNameType,
roleType: _securityKeyOptions.ClaimRoleType);
}

/// <inheritdoc />
public bool Validate(string? value, out Claim[] claims)
public ValueTask<bool> Validate(string? value)
{
var isValid = Validate(value);
if (string.IsNullOrWhiteSpace(value))
return ValueTask.FromResult(false);

claims = isValid ? _defaultClaims : [];
return isValid;
return ValueTask.FromResult(_validKeys.Value.Contains(value));
}

private HashSet<string> ExractKeys()
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup Label="Package">
<Description>ASP.NET Core Extension Projects</Description>
<Description>API Key Authentication Implementation for ASP.NET Core</Description>
<Copyright>Copyright © $([System.DateTime]::Now.ToString(yyyy)) LoreSoft</Copyright>
<Authors>LoreSoft</Authors>
<NeutralLanguage>en-US</NeutralLanguage>
Expand Down
35 changes: 24 additions & 11 deletions test/AspNetCore.SecurityKey.Tests/SecurityKeyValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace AspNetCore.Extensions.Authentication.Tests;
public class SecurityKeyValidatorTests
{
[Fact]
public void ValidateSecurityKey()
public async Task ValidateSecurityKey()
{
var securityKeyOptions = new SecurityKeyOptions();

Expand All @@ -29,13 +29,18 @@ public void ValidateSecurityKey()

var validator = new SecurityKeyValidator(configuration, options, logger);

validator.Validate("test").Should().BeFalse();
validator.Validate("this-is-test").Should().BeTrue();
validator.Validate("another-test").Should().BeTrue();
var result = await validator.Validate("test");
result.Should().BeFalse();

result = await validator.Validate("this-is-test");
result.Should().BeTrue();

result = await validator.Validate("another-test");
result.Should().BeTrue();
}

[Fact]
public void ValidateSecurityKeyCase()
public async Task ValidateSecurityKeyCaseAsync()
{
var securityKeyOptions = new SecurityKeyOptions()
{
Expand All @@ -57,12 +62,15 @@ public void ValidateSecurityKeyCase()

var validator = new SecurityKeyValidator(configuration, options, logger);

validator.Validate("this-is-test").Should().BeTrue();
validator.Validate("THIS-IS-TEST").Should().BeFalse();
var result = await validator.Validate("this-is-test");
result.Should().BeTrue();

result = await validator.Validate("THIS-IS-TEST");
result.Should().BeFalse();
}

[Fact]
public void ValidateNoKeyFound()
public async Task ValidateNoKeyFound()
{
var securityKeyOptions = new SecurityKeyOptions();

Expand All @@ -78,8 +86,13 @@ public void ValidateNoKeyFound()

var validator = new SecurityKeyValidator(configuration, options, logger);

validator.Validate("test").Should().BeFalse();
validator.Validate("this-is-test").Should().BeFalse();
validator.Validate("another-test").Should().BeFalse();
var result = await validator.Validate("test");
result.Should().BeFalse();

result = await validator.Validate("this-is-test");
result.Should().BeFalse();

result = await validator.Validate("another-test");
result.Should().BeFalse();
}
}

0 comments on commit 3f97600

Please sign in to comment.