using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.Json; using API.Core.Contracts.Common; using Infrastructure.Jwt; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; 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. var 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 var authHeaderValue ) ) { return AuthenticateResult.Fail("Authorization header is missing"); } var authHeader = authHeaderValue.ToString(); if ( !authHeader.StartsWith( "Bearer ", StringComparison.OrdinalIgnoreCase ) ) { return AuthenticateResult.Fail( "Invalid authorization header format" ); } var token = authHeader.Substring("Bearer ".Length).Trim(); try { var claimsPrincipal = await tokenInfrastructure.ValidateJwtAsync( token, secret ); var ticket = new AuthenticationTicket(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; var response = new ResponseBody { 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 { }