You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
136 lines
5.9 KiB
136 lines
5.9 KiB
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using System;
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Linq;
|
|
using System.Security.Claims;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Hasher;
|
|
|
|
/// <summary>
|
|
/// Middleware for JWT authentication and payload signature verification.
|
|
/// </summary>
|
|
public class JwtAuthenticationMiddleware
|
|
{
|
|
private readonly RequestDelegate _next;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the JwtAuthenticationMiddleware class.
|
|
/// </summary>
|
|
/// <param name="next">The next middleware in the pipeline.</param>
|
|
/// <param name="configuration">The application configuration.</param>
|
|
/// <param name="logger">The logger for this middleware.</param>
|
|
public JwtAuthenticationMiddleware(RequestDelegate next, IConfiguration configuration, ILogger<JwtAuthenticationMiddleware> logger)
|
|
{
|
|
_next = next;
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes the middleware for the specified context.
|
|
/// </summary>
|
|
/// <param name="context">The HttpContext for the current request.</param>
|
|
/// <returns>A Task representing the asynchronous operation.</returns>
|
|
public async Task InvokeAsync(HttpContext context)
|
|
{
|
|
_logger.LogDebug($"Request cookies: {context.Request.Cookies}");
|
|
// Read the Authorization and UserFingerprint cookies
|
|
context.Request.Cookies.TryGetValue("__Secure-Authorization", out string token);
|
|
context.Request.Cookies.TryGetValue("__Secure-UserFingerprint", out string fingerprint);
|
|
|
|
// Check if both token and fingerprint are present
|
|
if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(fingerprint))
|
|
{
|
|
// Validate the JWT and fingerprint
|
|
var validationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuer = true,
|
|
ValidateAudience = true,
|
|
ValidateLifetime = true,
|
|
ValidateIssuerSigningKey = true,
|
|
|
|
ValidIssuer = _configuration["JwtSettings:issuer"],
|
|
ValidAudience = _configuration["JwtSettings:audience"],
|
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSettings:SecretKey"])),
|
|
ClockSkew = TimeSpan.Zero
|
|
};
|
|
|
|
try
|
|
{
|
|
// Validate the JWT token
|
|
var tokenHandler = new JwtSecurityTokenHandler();
|
|
tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
|
|
|
|
// Extract claims from the validated token
|
|
var jwtToken = (JwtSecurityToken)validatedToken;
|
|
var claims = jwtToken.Claims;
|
|
|
|
// Validate the fingerprint
|
|
var fingerprintClaim = claims.FirstOrDefault(c => c.Type == "Fingerprint");
|
|
if (fingerprintClaim != null)
|
|
{
|
|
var hashedFingerprint = fingerprintClaim.Value;
|
|
_logger.LogDebug($"HashedFp: {hashedFingerprint} | Cookie Fp: {fingerprintClaim}");
|
|
|
|
// Verify the fingerprint here, based on how you generate the fingerprint hash
|
|
// If the fingerprint is valid, set the User property with the claims
|
|
if (FingerPrinter.ValidateFingerpint(fingerprint, hashedFingerprint))
|
|
{
|
|
// Record the claims so that they can be accessed by controllers
|
|
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.Select(c => c.Type == "Auth" ? new Claim(ClaimTypes.Role, c.Value) : c), "Jwt"));
|
|
}
|
|
}
|
|
|
|
// Validate signed headers
|
|
string payloadSig = context.Request.Headers["X-Payload-Signature"];
|
|
// Read the request body content
|
|
string payload;
|
|
using (var streamReader = new StreamReader(context.Request.Body))
|
|
{
|
|
payload = await streamReader.ReadToEndAsync();
|
|
}
|
|
string clientPublicKey = claims.FirstOrDefault(c => c.Type == "ClientPublicKey").Value;
|
|
_logger.LogDebug($"PayloadSig: {payloadSig} | payload: {payload} | Key: {clientPublicKey}");
|
|
//FIXME This could use some testing
|
|
try
|
|
{
|
|
bool validSig = KeyHelper.validateSignature(payloadSig, payload, clientPublicKey);
|
|
_logger.LogDebug($"Valid sig!");
|
|
if (!validSig)
|
|
{
|
|
_logger.LogWarning("Invalid signature!");
|
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
context.Response.ContentType = "text/plain";
|
|
await context.Response.WriteAsync("Invalid signature!");
|
|
return;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
_logger.LogError($"Failed to verify signature!");
|
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
return;
|
|
}
|
|
|
|
|
|
}
|
|
// This means the JWT is invalid
|
|
catch (SecurityTokenValidationException)
|
|
{
|
|
_logger.LogWarning("Invlaid JWT Exception!");
|
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
context.Response.ContentType = "text/plain";
|
|
await context.Response.WriteAsync("Invalid token!");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Call the next middleware in the pipeline
|
|
await _next(context);
|
|
}
|
|
} |