using System.Security.Claims; using System.Text.Encodings.Web; using Infrastructure.Jwt; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Shared.Contracts; namespace API.Core.Authentication; /// /// Custom ASP.NET Core authentication handler that validates bearer JWT access tokens /// supplied in the Authorization header against the JWT authentication scheme. /// /// The monitored options for the JWT authentication scheme. /// The logger factory used by the base . /// The URL encoder used by the base . /// The infrastructure service used to validate JWT access tokens. /// The application configuration, used as a fallback source for the JWT secret key. public class JwtAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ITokenInfrastructure tokenInfrastructure, IConfiguration configuration ) : AuthenticationHandler(options, logger, encoder) { /// /// Validates the incoming request's bearer JWT access token and produces an authentication result. /// /// /// The signing secret is resolved first from the ACCESS_TOKEN_SECRET environment variable, falling /// back to the Jwt:SecretKey configuration value, to stay consistent with the secret source used /// when tokens are issued. Authentication fails if the secret is not configured, the /// Authorization header is missing, the header does not use the Bearer scheme, or the /// token fails validation. /// /// /// A successful containing the validated /// /// on success, or a failure result describing why authentication could not be completed. /// protected override async Task HandleAuthenticateAsync() { // Use the same access-token secret source as TokenService to avoid mismatched validation. string? secret = Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET"); if (string.IsNullOrWhiteSpace(secret)) secret = configuration["Jwt:SecretKey"]; if (string.IsNullOrWhiteSpace(secret)) return AuthenticateResult.Fail("JWT secret is not configured"); // Check if Authorization header exists if (!Request.Headers.TryGetValue("Authorization", out StringValues authHeaderValue)) return AuthenticateResult.Fail("Authorization header is missing"); string authHeader = authHeaderValue.ToString(); if (!authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) return AuthenticateResult.Fail("Invalid authorization header format"); string token = authHeader.Substring("Bearer ".Length).Trim(); try { ClaimsPrincipal claimsPrincipal = await tokenInfrastructure.ValidateJwtAsync( token, secret ); AuthenticationTicket ticket = new(claimsPrincipal, Scheme.Name); return AuthenticateResult.Success(ticket); } catch (Exception ex) { return AuthenticateResult.Fail($"Token validation failed: {ex.Message}"); } } /// /// Writes a JSON 401 Unauthorized response when authentication fails or is required but not supplied. /// /// The authentication properties associated with the challenge. /// A task that completes once the response body has been written. protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { Response.ContentType = "application/json"; Response.StatusCode = 401; ResponseBody response = new() { Message = "Unauthorized: Invalid or missing authentication token", }; await Response.WriteAsJsonAsync(response); } } /// /// Options for the JWT authentication scheme handled by . /// /// No additional options are defined beyond those provided by . public class JwtAuthenticationOptions : AuthenticationSchemeOptions { }