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
{
}