2026-09-25 19:08:33 -03:00
|
|
|
using System.Security.Claims;
|
2026-09-25 18:54:10 -03:00
|
|
|
using System.Text;
|
|
|
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
|
|
|
using Microsoft.IdentityModel.Tokens;
|
2026-09-25 19:08:33 -03:00
|
|
|
using SeaHaven.Services.Helpers;
|
|
|
|
|
using SeaHaven.Services.Interfaces;
|
2026-09-25 18:54:10 -03:00
|
|
|
|
|
|
|
|
namespace Api.SeaHavenIndustries.Infrastructure
|
|
|
|
|
{
|
|
|
|
|
public static class JwtAuthenticationRegistration
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Registers bearer-token authentication as the default scheme. Program.cs and the
|
|
|
|
|
/// behavior tests both compose authentication through this method so the token
|
|
|
|
|
/// rules under test are the rules that run.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static IServiceCollection AddSeaHavenJwtAuthentication(this IServiceCollection services, IConfiguration configuration)
|
|
|
|
|
{
|
|
|
|
|
services.AddAuthentication(options =>
|
|
|
|
|
{
|
|
|
|
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
|
|
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
|
|
|
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
|
|
|
})
|
|
|
|
|
.AddJwtBearer(options =>
|
|
|
|
|
{
|
|
|
|
|
options.SaveToken = true;
|
|
|
|
|
options.RequireHttpsMetadata = false;
|
|
|
|
|
options.TokenValidationParameters = new TokenValidationParameters()
|
|
|
|
|
{
|
|
|
|
|
ValidateIssuer = true,
|
|
|
|
|
ValidateAudience = true,
|
|
|
|
|
ValidAudience = configuration["JWT:ValidAudience"],
|
|
|
|
|
ValidIssuer = configuration["JWT:ValidIssuer"],
|
|
|
|
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
|
|
|
|
configuration["JWT:Secret"]
|
|
|
|
|
?? throw new InvalidOperationException("JWT:Secret configuration is required")))
|
|
|
|
|
};
|
2026-09-25 19:08:33 -03:00
|
|
|
options.Events = new JwtBearerEvents
|
|
|
|
|
{
|
|
|
|
|
OnTokenValidated = RejectEndedSessionAsync
|
|
|
|
|
};
|
2026-09-25 18:54:10 -03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return services;
|
|
|
|
|
}
|
2026-09-25 19:08:33 -03:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Refuses a correctly signed token whose session stamp no longer matches the
|
|
|
|
|
/// account: the password was reset or changed, or the account was deactivated or
|
|
|
|
|
/// deleted, after the token was issued. A token without a stamp is refused too.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static async Task RejectEndedSessionAsync(TokenValidatedContext context)
|
|
|
|
|
{
|
|
|
|
|
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
|
|
|
var claimValue = context.Principal?.FindFirstValue(SeaHavenClaimTypes.SessionStamp);
|
|
|
|
|
var sessions = context.HttpContext.RequestServices.GetRequiredService<ISessionStampService>();
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(userId)
|
|
|
|
|
|| !await sessions.IsCurrentAsync(userId, claimValue, context.HttpContext.RequestAborted))
|
|
|
|
|
{
|
|
|
|
|
// The handler logs this text; it names no account, token or stamp.
|
|
|
|
|
context.Fail("The session has ended.");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-25 18:54:10 -03:00
|
|
|
}
|
|
|
|
|
}
|