import type { Redis } from "ioredis"; /** * TASK-0129 — the Redis key layout for OAuth codes / refresh tokens / * families / the access-token JTI denylist. ONE source of truth shared by * `oauth.service.ts` (issue/rotate/revoke) and the global `auth.guard.ts` * (JTI denylist check on every `type:"oauth"` bearer). * * All OAuth credentials are REDIS-ONLY (no DB tables) — codes, refresh * tokens, and the JTI denylist. Redis is the system of record here because * these are short-lived, high-churn, and need atomic single-use (GETDEL) + * TTL; PostgreSQL is not the right fit (see C-13.7 / task "Redis only"). */ export const OAUTH_CODE_PREFIX = "oauth:code:"; export const OAUTH_RT_PREFIX = "oauth:rt:"; export const OAUTH_FAMILY_PREFIX = "oauth:family:"; export const OAUTH_JTI_PREFIX = "oauth:jti:"; /** Single-use authorization code (SETEX 600, consumed via GETDEL). */ export const codeKey = (code: string): string => `${OAUTH_CODE_PREFIX}${code}`; /** Rotating refresh token, keyed by sha256 (the raw token never lands in Redis). */ export const rtKey = (tokenSha256: string): string => `${OAUTH_RT_PREFIX}${tokenSha256}`; /** The reuse-kill switch for a token family ("revoked"). */ export const familyKey = (familyId: string): string => `${OAUTH_FAMILY_PREFIX}${familyId}`; /** The access-token JTI denylist ("1"). */ export const jtiKey = (jti: string): string => `${OAUTH_JTI_PREFIX}${jti}`; /** All single-use authorization codes issued to a client (drained on app delete). */ export const clientCodesKey = (clientId: string): string => `oauth:client-codes:${clientId}`; /** All refresh-token hashes currently known for a client (drained on app delete). */ export const clientRtSetKey = (clientId: string): string => `oauth:client-rtset:${clientId}`; /** * Is this access-token JTI on the denylist? Fail-OPEN on a Redis error * (mirrors the API-key auth-failure throttle and the rate-limit guard): a * Redis blip must not lock every OAuth token out — the access token is * short-lived (15 min) so a worst case is a briefly-live revoked token. */ export async function isJtiRevoked(redis: Redis, jti: string | undefined): Promise { if (!jti) return false; try { return (await redis.get(jtiKey(jti))) === "1"; } catch { return false; } }