fix(auth): port server-side session revocation and sign-in hardening

Ports the upstream auth audit fixes onto the kept auth feature:

- revoke sessions server-side via an in-memory jti denylist (B5):
  createSession embeds the session id as the JWT jti, invalidateSession
  denylists it for the max token lifetime, validateSession rejects
  denylisted and jti-less (fail-closed) tokens; constant-time signature
  comparison (B4). Adds session-denylist.ts + test.
- cover signToken/verifyToken/validateSession crypto paths without a
  running Payload by stubbing the payload module (B8).
- derive clientIp server-side from trusted proxy headers and drop it from
  the public sign-in input schema; thread it as a server-only request
  context argument so a client can no longer spoof its rate-limit bucket
  (B2).
- declare the auth-injected email (and displayName) in the users
  collection-level DSR pii map so Art. 15 export and Art. 17 soft delete
  cover them (A5). Adapted to our collection set (no username field).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 23:53:43 +02:00
parent 8b114351a8
commit ac0bf80eca
10 changed files with 437 additions and 32 deletions

View File

@@ -3,6 +3,7 @@ import {
signInInputSchema,
type ISignInUseCase,
type SignInOutput,
type SignInRequestContext,
} from "../../application/use-cases/sign-in.use-case";
function presenter(value: SignInOutput) {
@@ -13,11 +14,21 @@ export type ISignInController = ReturnType<typeof signInController>;
export const signInController =
(signInUseCase: ISignInUseCase) =>
async (input: unknown): Promise<ReturnType<typeof presenter>> => {
async (
input: unknown,
// Server-derived, never part of the client-facing input schema (B2):
// the tRPC adapter builds it from trusted proxy headers.
requestContext?: SignInRequestContext,
): Promise<ReturnType<typeof presenter>> => {
const parsed = signInInputSchema.safeParse(input);
if (!parsed.success) {
throw new InputParseError("Invalid sign-in input", { cause: parsed.error });
throw new InputParseError("Invalid sign-in input", {
cause: parsed.error,
});
}
const result = await signInUseCase(parsed.data);
const result = await signInUseCase({
...parsed.data,
clientIp: requestContext?.clientIp,
});
return presenter(result);
};