using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moonlight.Api.Database; using Moonlight.Api.Database.Entities; using Moonlight.Shared; namespace Moonlight.Api.Implementations.ApiKeyScheme; public class ApiKeySchemeHandler : AuthenticationHandler { private readonly DatabaseRepository ApiKeyRepository; private readonly IMemoryCache MemoryCache; public ApiKeySchemeHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, DatabaseRepository apiKeyRepository, IMemoryCache memoryCache ) : base(options, logger, encoder) { ApiKeyRepository = apiKeyRepository; MemoryCache = memoryCache; } protected override async Task HandleAuthenticateAsync() { var authHeaderValue = Request.Headers.Authorization.FirstOrDefault() ?? null; if (string.IsNullOrWhiteSpace(authHeaderValue)) return AuthenticateResult.NoResult(); if (authHeaderValue.Length > 32) return AuthenticateResult.Fail("Invalid api key specified"); if (!MemoryCache.TryGetValue(authHeaderValue, out var apiKey)) { apiKey = await ApiKeyRepository .Query() .Where(x => x.Key == authHeaderValue) .Select(x => new ApiKeySession(x.Permissions, x.ValidUntil)) .FirstOrDefaultAsync(); if (apiKey == null) return AuthenticateResult.Fail("Invalid api key specified"); MemoryCache.Set(authHeaderValue, apiKey, Options.LookupCacheTime); } else { if (apiKey == null) return AuthenticateResult.Fail("Invalid api key specified"); } if (DateTimeOffset.UtcNow > apiKey.ValidUntil) return AuthenticateResult.Fail("Api key expired"); return AuthenticateResult.Success(new AuthenticationTicket( new ClaimsPrincipal( new ClaimsIdentity( apiKey.Permissions.Select(x => new Claim(Permissions.ClaimType, x)).ToArray() ) ), Scheme.Name )); } private record ApiKeySession(string[] Permissions, DateTimeOffset ValidUntil); }