first commit
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Fitness_core_BE.Helpers;
|
||||
using Fitness_core_BE.Interfaces;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Fitness_core_BE.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants for the agent API-key authentication scheme / authorization policy.
|
||||
/// </summary>
|
||||
public static class AgentApiKeyDefaults
|
||||
{
|
||||
public const string AuthenticationScheme = "AgentApiKey";
|
||||
public const string PolicyName = "AgentApiKey";
|
||||
public const string HeaderName = "X-Api-Key";
|
||||
|
||||
// Claim types used to carry the resolved agent + client server-side.
|
||||
public const string AgentIdClaim = "AgentId";
|
||||
public const string ClientIdClaim = "FKClientId";
|
||||
}
|
||||
|
||||
public class AgentApiKeyAuthenticationOptions : AuthenticationSchemeOptions { }
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates on-prem agents using the high-entropy key sent in the X-Api-Key header.
|
||||
/// The key is hashed and matched against an active Agent. On success it exposes the
|
||||
/// agent's FKClientId and AgentId via claims + HttpContext.Items so downstream code never
|
||||
/// has to trust client/agent identifiers coming from the request body.
|
||||
/// </summary>
|
||||
public class AgentApiKeyAuthenticationHandler : AuthenticationHandler<AgentApiKeyAuthenticationOptions>
|
||||
{
|
||||
private readonly IAgentRepo _agentRepo;
|
||||
|
||||
public AgentApiKeyAuthenticationHandler(
|
||||
IOptionsMonitor<AgentApiKeyAuthenticationOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
IAgentRepo agentRepo)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
_agentRepo = agentRepo;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(AgentApiKeyDefaults.HeaderName, out var headerValues))
|
||||
return AuthenticateResult.NoResult();
|
||||
|
||||
var apiKey = headerValues.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
return AuthenticateResult.Fail("Missing API key.");
|
||||
|
||||
var hash = ApiKeyHelper.HashApiKey(apiKey);
|
||||
var agent = await _agentRepo.GetByApiKeyHashAsync(hash);
|
||||
|
||||
if (agent == null || !agent.IsActive)
|
||||
return AuthenticateResult.Fail("Invalid API key.");
|
||||
|
||||
// Expose resolved identity server-side (never trust the body for these).
|
||||
Context.Items[AgentApiKeyDefaults.AgentIdClaim] = agent.AgentId.ToString();
|
||||
Context.Items[AgentApiKeyDefaults.ClientIdClaim] = agent.FKClientId.ToString();
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(AgentApiKeyDefaults.AgentIdClaim, agent.AgentId.ToString()),
|
||||
new Claim(AgentApiKeyDefaults.ClientIdClaim, agent.FKClientId.ToString()),
|
||||
new Claim(ClaimTypes.NameIdentifier, agent.AgentId.ToString())
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
|
||||
return AuthenticateResult.Success(ticket);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user