using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.RegularExpressions; using Abstractions; using HashidsNet; using JWT.Algorithms; using JWT.Builder; using JWT.Serializers; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using W542.GandalfReborn.Data.Database; namespace Security.Scheme; public class GandalfRebornJwtBody { public string? Id { get; init; } public string? Sub { get; init; } public DateTimeOffset? Iat { get; init; } public DateTimeOffset? Exp { get; init; } public string? Iss { get; init; } public string? Aud { get; init; } } public partial class GandalfRebornJwtTokenAuthSchemeHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IHashids hashIds, ApplicationContext context, IHttpContextAccessor httpContextAccessor, InvokerContext invokerContext, TimeProvider timeProvider) : AuthenticationHandler(options, logger, encoder) { [GeneratedRegex(@"Bearer\s+(?[A-Za-z0-9\-._~+\/]+=*\.[A-Za-z0-9\-._~+\/]+=*\.[A-Za-z0-9\-._~+\/]+=*)")] private static partial Regex BearerTokenRegex(); private string FailReason { get; set; } = ""; protected override async Task HandleAuthenticateAsync() { try { var jwtBody = GetJwtTokenBody(httpContextAccessor.HttpContext); if (!hashIds.TryDecodeSingleLong(jwtBody.Sub, out var subjectId)) { throw new UnauthorizedAccessException("One does not simply authenticate with invalid id."); } var subjectExists = context.Subjects.Any(x => x.Id == subjectId); if (!subjectExists) { throw new UnauthorizedAccessException("One does not simply authenticate with a not existing subject."); } var claims = new List { new(Invoker.SubType, subjectId.ToString()) }; var tenantAuthorities = await context.TenantSubjectRelations .AsNoTracking() .Where(x => x.SubjectId == subjectId) .Include(x => x.InternalAuthorities) .ToDictionaryAsync(x => x.TenantId, x => x.InternalAuthorities.Select(authority => authority.Name)); var appAuthorities = await context.AppSubjectRelations .AsNoTracking() .Where(x => x.SubjectId == subjectId) .Include(x => x.InternalAuthorities) .ToDictionaryAsync(x => x.AppId, x => x.InternalAuthorities.Select(authority => authority.Name)); var tenantClaims = tenantAuthorities .SelectMany(x => x.Value.Select(authority => new Claim($"{Invoker.TenantAuthorityPrefix}{Invoker.AuthoritySeparator}{hashIds.EncodeLong(x.Key)}", authority))); var appClaims = appAuthorities .SelectMany(x => x.Value.Select(authority => new Claim($"{Invoker.AppAuthorityPrefix}{Invoker.AuthoritySeparator}{hashIds.EncodeLong(x.Key)}", authority))); claims.AddRange(tenantClaims); claims.AddRange(appClaims); var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Tokens")); invokerContext.Invoker = principal; var ticket = new AuthenticationTicket(principal, Scheme.Name); return AuthenticateResult.Success(ticket); } catch (Exception ex) { FailReason = ex.Message; return AuthenticateResult.Fail(ex); } } protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { await base.HandleChallengeAsync(properties); if (Response.StatusCode == StatusCodes.Status401Unauthorized && !string.IsNullOrWhiteSpace(FailReason)) { Response.ContentType = "application/json"; var executor = Context.RequestServices.GetRequiredService>(); var actionContext = new ActionContext(Context, Context.GetRouteData(), new ActionDescriptor()); await executor.ExecuteAsync(actionContext, new UnauthorizedObjectResult(new { status = Response.StatusCode, error = FailReason, })); } } private GandalfRebornJwtBody GetJwtTokenBody(HttpContext? httpContext) { var authHeader = httpContext?.Request.Headers.Authorization.ToString(); if (authHeader is null) throw new UnauthorizedAccessException("One does not simply not provide an authorization header."); var tokenRegexMatch = BearerTokenRegex().Match(authHeader); if (!tokenRegexMatch.Success) throw new UnauthorizedAccessException("One does not simply provide an invalid authorization header."); var token = tokenRegexMatch.Groups["token"].Value; var decodedToken = JwtBuilder.Create() .WithAlgorithm(new HMACSHA512Algorithm()) .WithSecret(Options.JwtSecret) .MustVerifySignature() .WithJsonSerializer(new JsonNetSerializer()) .Decode(token); if (decodedToken.Aud is null || !decodedToken.Aud.StartsWith(Options.BaseUrl)) throw new UnauthorizedAccessException("One does not simply provide a token for a different audience."); if (decodedToken.Iss is null || !decodedToken.Iss.StartsWith(Options.BaseUrl)) throw new UnauthorizedAccessException("One does not simply provide a token from a unknown issuer."); if (decodedToken.Exp is null || decodedToken.Exp?.ToUniversalTime() <= timeProvider.GetUtcNow()) throw new UnauthorizedAccessException("One does not simply provide an expired token."); return decodedToken; } }