using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using StellaOps.Auth.Abstractions; namespace StellaOps.AirGap.Controller.Auth; public sealed class HeaderScopeAuthenticationHandler : AuthenticationHandler { public const string SchemeName = "HeaderScope"; #pragma warning disable CS0618 // ISystemClock obsolete; base ctor signature still requires it on this TF. public HeaderScopeAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } #pragma warning restore CS0618 protected override Task HandleAuthenticateAsync() { var scopes = ExtractScopes(Request.Headers); if (scopes.Count == 0) { return Task.FromResult(AuthenticateResult.Fail("scope_header_missing")); } var claims = new List { new(ClaimTypes.NameIdentifier, "header-scope"), new(StellaOpsClaimTypes.Subject, "header-scope"), new(StellaOpsClaimTypes.Scope, string.Join(' ', scopes)) }; foreach (var scope in scopes) { claims.Add(new Claim(StellaOpsClaimTypes.ScopeItem, scope)); } if (TryGetTenantHeader(Request.Headers, out var tenantId)) { claims.Add(new Claim(StellaOpsClaimTypes.Tenant, tenantId)); claims.Add(new Claim("tid", tenantId)); } var identity = new ClaimsIdentity(claims, SchemeName); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, SchemeName); return Task.FromResult(AuthenticateResult.Success(ticket)); } private static HashSet ExtractScopes(IHeaderDictionary headers) { var scopes = new HashSet(StringComparer.OrdinalIgnoreCase); AddScopes(headers, "scope", scopes); AddScopes(headers, "scp", scopes); return scopes; } private static void AddScopes(IHeaderDictionary headers, string headerName, ISet scopes) { if (!headers.TryGetValue(headerName, out var values)) { return; } foreach (var value in values) { foreach (var scope in value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { scopes.Add(scope); } } } private static bool TryGetTenantHeader(IHeaderDictionary headers, out string tenantId) { tenantId = string.Empty; if (headers.TryGetValue("x-tenant-id", out var headerValue) && !string.IsNullOrWhiteSpace(headerValue)) { tenantId = headerValue.ToString().Trim(); return true; } if (headers.TryGetValue("tid", out var legacyValue) && !string.IsNullOrWhiteSpace(legacyValue)) { tenantId = legacyValue.ToString().Trim(); return true; } return false; } }