shoc-backend/Api.SeaHavenIndustries/Infrastructure/JwtAuthenticationRegistration.cs
Alexandre Brandizzi b7be07411e fix(auth): end earlier sessions when a password or account status changes
Tokens now carry a keyed hash of the account's security stamp, and every
authenticated request compares it with the stored stamp (cached for 60 s,
evicted in-process on change). A password reset or change, a deactivation
and a deletion all rotate or remove the stamp, so tokens issued before them
get 401. Tokens without the claim get 401 too.
2026-09-25 19:08:33 -03:00

67 lines
3 KiB
C#

using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using SeaHaven.Services.Helpers;
using SeaHaven.Services.Interfaces;
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")))
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = RejectEndedSessionAsync
};
});
return services;
}
/// <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.");
}
}
}
}