using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using MoonlightServers.Api.Infrastructure.Database; using MoonlightServers.Api.Infrastructure.Database.Entities; namespace MoonlightServers.Api.Infrastructure.Implementations.NodeToken; public class NodeTokenSchemeHandler : AuthenticationHandler { public const string SchemeName = "MoonlightServers.NodeToken"; public const string CacheKeyFormat = $"MoonlightServers.{nameof(NodeTokenSchemeHandler)}.{{0}}"; private readonly DatabaseRepository DatabaseRepository; private readonly HybridCache Cache; public NodeTokenSchemeHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, DatabaseRepository databaseRepository, HybridCache cache ) : base(options, logger, encoder) { DatabaseRepository = databaseRepository; Cache = cache; } protected override async Task HandleAuthenticateAsync() { // Basic format validation if (!Context.Request.Headers.TryGetValue(HeaderNames.Authorization, out var authHeaderValues)) return AuthenticateResult.Fail("No authorization header present"); if (authHeaderValues.Count != 1) return AuthenticateResult.Fail("No authorization value present"); var authHeaderValue = authHeaderValues[0]; if (string.IsNullOrEmpty(authHeaderValue)) return AuthenticateResult.Fail("No authorization value present"); var authHeaderParts = authHeaderValue.Split( ' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); // Validate parts if (authHeaderParts.Length < 2) return AuthenticateResult.Fail("Malformed authorization header"); var tokenId = authHeaderParts[0]; var token = authHeaderParts[1]; if (tokenId.Length != 10 && token.Length != 64) return AuthenticateResult.Fail("Malformed authorization header"); // Real validation var cacheKey = string.Format(CacheKeyFormat, tokenId); var session = await Cache.GetOrCreateAsync(cacheKey, async cancellationToken => { return await DatabaseRepository .Query() .Where(x => x.TokenId == tokenId) .Select(x => new NodeTokenSession(x.Id, x.Token)) .FirstOrDefaultAsync(cancellationToken: cancellationToken); }, new HybridCacheEntryOptions() { LocalCacheExpiration = Options.LookupCacheL1Expiry, Expiration = Options.LookupCacheL2Expiry } ); if(session == null || token != session.Token) return AuthenticateResult.Fail("Invalid authorization header"); // All checks have passed, create auth ticket return AuthenticateResult.Success(new AuthenticationTicket( new ClaimsPrincipal( new ClaimsIdentity( [ new Claim("NodeId", session.Id.ToString()) ], SchemeName ) ), SchemeName )); } private record NodeTokenSession(int Id, string Token); }