import pkceChallenge from 'pkce-challenge' import { randomBytes } from 'node:crypto' import type { H3Event } from 'h3' export interface PkceArtifacts { codeVerifier: string codeChallenge: string state: string } export async function generatePkce(): Promise { const { code_verifier, code_challenge } = await pkceChallenge() const state = randomBytes(16).toString('base64url') return { codeVerifier: code_verifier, codeChallenge: code_challenge, state, } } // ─── PKCE state em Redis (single-use, TTL curto) ───────────────────────────── export interface PkceStateData { codeVerifier: string returnTo: string createdAt: number } export async function savePkceState(state: string, data: PkceStateData): Promise { const ttl = useRuntimeConfig().pkceTtlSeconds await useRedis().set(`pkce:${state}`, JSON.stringify(data), 'EX', ttl) } /** * Lê e remove o state atomicamente — single-use, previne replay. */ export async function consumePkceState(state: string): Promise { if (!state) return null const key = `pkce:${state}` const pipe = useRedis().multi() pipe.get(key) pipe.del(key) const results = await pipe.exec() const raw = results?.[0]?.[1] as string | null | undefined if (!raw) return null try { return JSON.parse(raw) as PkceStateData } catch { return null } } /** * Computa o redirect_uri do callback a partir do request — coerente com o que foi enviado ao Keycloak. */ export function callbackUrlFromEvent(event: H3Event): string { const host = getRequestHost(event, { xForwardedHost: true }) const proto = getRequestProtocol(event, { xForwardedProto: true }) return `${proto}://${host}/api/auth/callback` }