Follow-up to the "refuse login for resigned accounts" work (design: eform-backendconfiguration-plugin/docs/superpowers/specs/2026-09-17-resigned-account-login-refusal-design.md).
That change stops a resigned worker starting a session. It does not end one they already have.
Current behaviour
- Tokens are valid for 24h —
AuthService.GenerateToken, AuthService.cs:256 (DateTime.Now.AddHours(24)).
GET /api/auth/token/refresh (AuthService.RefreshToken, AuthService.cs:173-204) mints a fresh 24h token from any still-valid token, indefinitely. The Angular client calls it automatically on any 403 (http-error.interceptor.ts:78-113).
- Nothing re-reads the user row after login. The only per-request hook,
ClaimsTransformer.TransformAsync (ClaimsTransformer.cs:49-100), never rejects — on a cache miss or stale updated_at it sets an advisory response header, and the client-side handler for that header is commented out (user-claims.interceptor.ts:22-28).
LogOut (AuthService.cs:297-311) only evicts an in-memory cache entry; the JWT stays valid afterwards.
So a worker resigned at 09:00 keeps full access until they stop using the app. The same is true for any account an admin wants to cut off immediately.
Why it needs its own design
ClaimsTransformer is the natural rejection point — it already runs on every authenticated request and already has the user id. But making it reject means either a per-request user lookup or a revocation cache, on every authenticated request, so it touches:
- performance — an extra DB round trip per request unless cached;
- multi-replica correctness — the existing
AuthCacheService is a per-process IMemoryCache (AuthCacheService.cs:30-85), so a token minted on one pod has no cache entry on another;
- the gRPC surface — the host calls
app.UseAuthentication() but there is no app.UseAuthorization() and no fallback policy, and no TimePlanning/BackendConfiguration gRPC service carries [Authorize].
Refusing the two refresh paths (AuthService.RefreshToken, TimePlanningAuthGrpcService.RefreshToken:202-253) is part of the login change and caps exposure at the remaining token lifetime — this issue is about closing the rest of that window.
Related, not covered here
- The four
[AllowAnonymous] kiosk endpoints authenticate with a RegistrationDevices.Token: plaintext, generated with System.Random, no expiry and no revocation field (RegistrationDevice.cs:35-56).
AccountService.ForgotPassword (AccountService.cs:284-291) returns User with <email> not found, an account-enumeration leak of the same family as the login messages.
Follow-up to the "refuse login for resigned accounts" work (design:
eform-backendconfiguration-plugin/docs/superpowers/specs/2026-09-17-resigned-account-login-refusal-design.md).That change stops a resigned worker starting a session. It does not end one they already have.
Current behaviour
AuthService.GenerateToken,AuthService.cs:256(DateTime.Now.AddHours(24)).GET /api/auth/token/refresh(AuthService.RefreshToken,AuthService.cs:173-204) mints a fresh 24h token from any still-valid token, indefinitely. The Angular client calls it automatically on any 403 (http-error.interceptor.ts:78-113).ClaimsTransformer.TransformAsync(ClaimsTransformer.cs:49-100), never rejects — on a cache miss or staleupdated_atit sets an advisory response header, and the client-side handler for that header is commented out (user-claims.interceptor.ts:22-28).LogOut(AuthService.cs:297-311) only evicts an in-memory cache entry; the JWT stays valid afterwards.So a worker resigned at 09:00 keeps full access until they stop using the app. The same is true for any account an admin wants to cut off immediately.
Why it needs its own design
ClaimsTransformeris the natural rejection point — it already runs on every authenticated request and already has the user id. But making it reject means either a per-request user lookup or a revocation cache, on every authenticated request, so it touches:AuthCacheServiceis a per-processIMemoryCache(AuthCacheService.cs:30-85), so a token minted on one pod has no cache entry on another;app.UseAuthentication()but there is noapp.UseAuthorization()and no fallback policy, and no TimePlanning/BackendConfiguration gRPC service carries[Authorize].Refusing the two refresh paths (
AuthService.RefreshToken,TimePlanningAuthGrpcService.RefreshToken:202-253) is part of the login change and caps exposure at the remaining token lifetime — this issue is about closing the rest of that window.Related, not covered here
[AllowAnonymous]kiosk endpoints authenticate with aRegistrationDevices.Token: plaintext, generated withSystem.Random, no expiry and no revocation field (RegistrationDevice.cs:35-56).AccountService.ForgotPassword(AccountService.cs:284-291) returnsUser with <email> not found, an account-enumeration leak of the same family as the login messages.