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();
}
}