| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192 |
- 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;
- /// <summary>
- /// Personal Access Token (PAT) Bearer scheme.
- /// Header: Authorization: Bearer dpot_pat_xxxxx
- /// </summary>
- public sealed class ApiKeyAuthenticationHandler(
- IOptionsMonitor<AuthenticationSchemeOptions> options,
- ILoggerFactory logger,
- UrlEncoder encoder,
- IAppDbContext db
- ) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
- {
- public const string SchemeName = "ApiKey";
- public const string TokenPrefix = "dpot_pat_";
- private readonly IAppDbContext _db = db;
- protected override async Task<AuthenticateResult> 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<Claim>
- {
- 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();
- }
- }
|