c6918cbeea
JWTs were previously fully stateless -- the token alone was the credential until its own expiry, with no way to list or kill a session server-side. Add a UserSession table alongside every JWT issued (AppSessionIssuer), embed its id as a "sid" claim, and check that claim against the DB on every "local" scheme request (Program.cs OnTokenValidated) so a session can actually be revoked before its JWT naturally expires. New /api/auth/sessions endpoints (list, revoke one, revoke-others) plus a Sessions card on the profile page. Fails closed on a missing "sid" claim: every JWT issued going forward has one, so a token without it is either pre-deploy (forces one re-login for already-signed-in users at deploy time, same additive-forward cost the 2FA/trusted-device work on this branch already paid) or forged.
31 lines
1.2 KiB
C#
31 lines
1.2 KiB
C#
using System.Security.Claims;
|
|
using JobTrackerApi.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace JobTrackerApi.Services;
|
|
|
|
// The actual revocation check behind Program.cs's "local" JwtBearer OnTokenValidated. Pulled out
|
|
// of Program.cs so it's unit-testable without standing up a full TestServer/HTTP pipeline.
|
|
public static class LocalSessionValidator
|
|
{
|
|
public static async Task<bool> IsValidAsync(JobTrackerContext db, ClaimsPrincipal? principal, DateTimeOffset now, CancellationToken cancellationToken = default)
|
|
{
|
|
var sid = principal?.FindFirst("sid")?.Value;
|
|
// Fail closed: see the comment on the OnTokenValidated wiring in Program.cs for why a
|
|
// missing sid is rejected rather than grandfathered in.
|
|
if (string.IsNullOrWhiteSpace(sid)) return false;
|
|
|
|
var session = await db.UserSessions.IgnoreQueryFilters()
|
|
.FirstOrDefaultAsync(x => x.Id == sid, cancellationToken);
|
|
if (session is null || session.RevokedAtUtc is not null || session.ExpiresAtUtc <= now) return false;
|
|
|
|
if (now - session.LastSeenAtUtc > TimeSpan.FromMinutes(5))
|
|
{
|
|
session.LastSeenAtUtc = now;
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|