Add ApiKey support

This commit is contained in:
honzapatCZ 2026-03-22 19:52:19 +01:00
parent bd3a0dcadf
commit a4735e0f44
4 changed files with 166 additions and 0 deletions

View File

@ -0,0 +1,50 @@
using System.Security.Claims;
using AspNetCore.Authentication.ApiKey;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
namespace NejCommon.Authorization;
public partial class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
if (context.Resource is not HttpContext http)
return;
var companyId = http.GetRouteValue("companyId")?.ToString();
if (companyId == null)
{
context.Succeed(requirement);
return;
}
var authType = context.User.Identity?.AuthenticationType;
if (authType == null)
return;
bool allowed;
if (authType == ApiKeyDefaults.AuthenticationScheme)
{
var id = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (id == null)
return;
allowed = await CheckApiKeyPermission(id, companyId, requirement.Permission);
}
else
{
var sub = context.User.FindFirst("sub")?.Value;
if (sub == null)
return;
allowed = await CheckUserPermission(sub, companyId, requirement.Permission);
}
if (allowed)
context.Succeed(requirement);
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
namespace NejCommon.Authorization;
public class PermissionRequirement : IAuthorizationRequirement
{
public string? Permission { get; }
public PermissionRequirement(string? permission)
{
Permission = permission;
}
}

60
Models/ApiKey.cs Normal file
View File

@ -0,0 +1,60 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using AspNetCore.Authentication.ApiKey;
using AutoMapPropertyHelper;
using NejCommon.Models;
namespace NejCommon.Models;
public partial class ApiKey : IApiKey
{
[AutoMapProperty("Response")]
public string Id { get; set; } = NanoidDotNet.Nanoid.Generate(size: 64);
[AutoMapProperty("Request")]
public string Name { get; set; } = null!;
public byte[] LookupHash { get; set; } = null!;
public string KeyHash { get; set; } = null!;
[AutoMapProperty("Response")]
public bool Revoked { get; set; }
[NotMapped]
public string key = "";
[NotMapped]
[AutoMapProperty("CreateResponse")]
public string Key
{
get => key;
set
{
key = value;
LookupHash = ComputeLookupHash(value);
KeyHash = ComputeStrongHash(value);
}
}
[NotMapped]
public string OwnerName => Id;
[NotMapped]
public IReadOnlyCollection<Claim> Claims => [
new Claim(ClaimTypes.NameIdentifier, Id),
new Claim(ClaimTypes.Name, Name)
];
public static byte[] ComputeLookupHash(string key)
{
using var sha256 = SHA256.Create();
return sha256.ComputeHash(Encoding.UTF8.GetBytes(key));
}
public static string ComputeStrongHash(string key)
{
return BCrypt.Net.BCrypt.EnhancedHashPassword(key);
}
public bool VerifyStrongHash(string key){
return BCrypt.Net.BCrypt.EnhancedVerify(key, KeyHash);
}
}
public partial class ApiKeyResponse : ApiKeyRequest {}
public partial class ApiKeyCreateResponse : ApiKeyResponse {}

View File

@ -0,0 +1,43 @@
using AspNetCore.Authentication.ApiKey;
using Microsoft.EntityFrameworkCore;
using NanoidDotNet;
using NejCommon.Models;
namespace NejCommon.Services;
public partial class ApiKeyProvider : IApiKeyProvider, IScopedService
{
private readonly ILogger<ApiKeyProvider> _logger;
public async Task<IApiKey> ProvideAsync(string key)
{
try
{
var lookup = ApiKey.ComputeLookupHash(key);
var apiKey = await GetApiKeySet().FirstOrDefaultAsync(x => x.LookupHash.SequenceEqual(lookup));
if (apiKey == null)
{
return null;
}
if (!apiKey.VerifyStrongHash(key))
{
return null;
}
apiKey.Key = key;
// write your validation implementation here and return an instance of a valid ApiKey or retun null for an invalid key.
// return await _apiKeyRepository.GetApiKeyAsync(key);
return apiKey;
}
catch (System.Exception exception)
{
_logger.LogError(exception, exception.Message);
throw;
}
}
public static string GenerateKey() => "nej-" + Nanoid.Generate(size: 256);
}