using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using Application.Abstractions.Data; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Infrastructure.Authentication; /// /// Personal Access Token (PAT) Bearer scheme. /// Header: Authorization: Bearer dpot_pat_xxxxx /// public sealed class ApiKeyAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IAppDbContext db ) : AuthenticationHandler(options, logger, encoder) { public const string SchemeName = "ApiKey"; public const string TokenPrefix = "dpot_pat_"; private readonly IAppDbContext _db = db; protected override async Task HandleAuthenticateAsync() { if (!Request.Headers.TryGetValue("Authorization", out var authHeader)) { return AuthenticateResult.NoResult(); } var auth = authHeader.ToString(); if (string.IsNullOrEmpty(auth) || !auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { return AuthenticateResult.NoResult(); } var token = auth.Substring("Bearer ".Length).Trim(); if (!token.StartsWith(TokenPrefix, StringComparison.Ordinal)) { // Not a PAT — let other schemes try. return AuthenticateResult.NoResult(); } var hash = HashToken(token); var pat = await _db.ApiPersonalAccessToken .AsTracking() .FirstOrDefaultAsync(c => c.TokenHash == hash, Context.RequestAborted); if (pat is null) { return AuthenticateResult.Fail("Invalid API key."); } if (!pat.IsActive) { return AuthenticateResult.Fail("API key is revoked or expired."); } pat.TouchLastUsed(); await _db.SaveChangesAsync(Context.RequestAborted); var claims = new List { new(ClaimTypes.NameIdentifier, pat.OwnerMemberID.ToString()), new("token_id", pat.ID.ToString()), new("token_type", "pat"), new("scope", pat.ScopesCsv) }; foreach (var s in pat.ScopesCsv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { claims.Add(new Claim("scp", s)); } var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return AuthenticateResult.Success(ticket); } public static string HashToken(string token) { var bytes = Encoding.UTF8.GetBytes(token); var hash = SHA256.HashData(bytes); return Convert.ToHexString(hash).ToLowerInvariant(); } }