diff --git a/Authorization/PermissionHandler.cs b/Authorization/PermissionHandler.cs new file mode 100644 index 0000000..97d7522 --- /dev/null +++ b/Authorization/PermissionHandler.cs @@ -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 +{ + 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); + } +} \ No newline at end of file diff --git a/Authorization/PermissionRequirement.cs b/Authorization/PermissionRequirement.cs new file mode 100644 index 0000000..ddd73fc --- /dev/null +++ b/Authorization/PermissionRequirement.cs @@ -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; + } +} \ No newline at end of file diff --git a/Models/ApiKey.cs b/Models/ApiKey.cs new file mode 100644 index 0000000..55609b1 --- /dev/null +++ b/Models/ApiKey.cs @@ -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 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 {} \ No newline at end of file diff --git a/Services/ApiKeyProvider.cs b/Services/ApiKeyProvider.cs new file mode 100644 index 0000000..fd6049c --- /dev/null +++ b/Services/ApiKeyProvider.cs @@ -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 _logger; + + public async Task 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); +} \ No newline at end of file