98 lines
3.7 KiB
C#
98 lines
3.7 KiB
C#
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Security.Claims;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Encodings.Web;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.Extensions.Options;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
namespace Fitness_core_BE.Auth
|
|
{
|
|
public static class UserJwtDefaults
|
|
{
|
|
public const string AuthenticationScheme = "UserJwtCookie";
|
|
}
|
|
|
|
public sealed class UserJwtAuthenticationOptions : AuthenticationSchemeOptions { }
|
|
|
|
/// <summary>
|
|
/// Authenticates browser SignalR connections from the existing httpOnly
|
|
/// access_token cookie. MVC controllers retain their existing action filter.
|
|
/// </summary>
|
|
public sealed class UserJwtAuthenticationHandler
|
|
: AuthenticationHandler<UserJwtAuthenticationOptions>
|
|
{
|
|
private readonly IConfiguration _configuration;
|
|
|
|
public UserJwtAuthenticationHandler(
|
|
IOptionsMonitor<UserJwtAuthenticationOptions> options,
|
|
ILoggerFactory logger,
|
|
UrlEncoder encoder,
|
|
IConfiguration configuration)
|
|
: base(options, logger, encoder)
|
|
{
|
|
_configuration = configuration;
|
|
}
|
|
|
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
{
|
|
var token = Request.Cookies["access_token"];
|
|
if (string.IsNullOrWhiteSpace(token))
|
|
return Task.FromResult(AuthenticateResult.NoResult());
|
|
|
|
try
|
|
{
|
|
var xmlKey = _configuration["Jwt:PublicKey"];
|
|
if (string.IsNullOrWhiteSpace(xmlKey))
|
|
throw new SecurityTokenException("JWT public key is not configured.");
|
|
|
|
using var rsa = RSA.Create();
|
|
rsa.FromXmlString(xmlKey);
|
|
var parameters = rsa.ExportParameters(false);
|
|
var key = new RsaSecurityKey(parameters)
|
|
{
|
|
KeyId = ComputeKeyId(parameters)
|
|
};
|
|
|
|
var principal = new JwtSecurityTokenHandler().ValidateToken(
|
|
token,
|
|
new TokenValidationParameters
|
|
{
|
|
ValidateIssuerSigningKey = true,
|
|
IssuerSigningKey = key,
|
|
ValidateIssuer = true,
|
|
ValidIssuer = _configuration["Jwt:Issuer"],
|
|
ValidateAudience = true,
|
|
ValidAudience = _configuration["Jwt:Audience"],
|
|
ValidateLifetime = true,
|
|
ClockSkew = TimeSpan.FromMinutes(2)
|
|
},
|
|
out _);
|
|
|
|
var identity = new ClaimsIdentity(
|
|
principal.Claims,
|
|
UserJwtDefaults.AuthenticationScheme);
|
|
var ticket = new AuthenticationTicket(
|
|
new ClaimsPrincipal(identity),
|
|
UserJwtDefaults.AuthenticationScheme);
|
|
return Task.FromResult(AuthenticateResult.Success(ticket));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Task.FromResult(
|
|
AuthenticateResult.Fail($"Invalid access token: {ex.Message}"));
|
|
}
|
|
}
|
|
|
|
private static string ComputeKeyId(RSAParameters parameters)
|
|
{
|
|
var modulus = parameters.Modulus ?? Array.Empty<byte>();
|
|
var exponent = parameters.Exponent ?? Array.Empty<byte>();
|
|
var material =
|
|
$"{Convert.ToBase64String(modulus)}.{Convert.ToBase64String(exponent)}";
|
|
return Base64UrlEncoder.Encode(
|
|
SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(material)));
|
|
}
|
|
}
|
|
}
|