Capítulo 3 de 5 · 12 min de lectura
Estados del ciclo de vida
Modela la verificación como una máquina de estados y elimina los bugs de booleanos sueltos.
2FA en producción · 0/5 completados
- Qué verifica un OTP
- Tu primera verificación
- Estados del ciclo de vida
- Expiración y reintentos
- Métricas que importan
En este capítulo
La mayoría de los incidentes con OTP que hemos revisado no vienen de criptografía débil, sino de estado mal modelado: un código que sigue vigente después de un acierto, un reenvío que no invalida el anterior, un contador que se reinicia con cada solicitud. La solución es tratar cada verificación como una entidad con estados explícitos.
Los estados
pending: emitida y enviada, esperando al usuario.approved: el usuario acertó; estado terminal.expired: venció el tiempo; terminal.locked: se agotaron los intentos; terminal.canceled: el sistema la anuló, por ejemplo al emitir un reenvío; terminal.
Solo pending acepta intentos. Todo lo demás es terminal y cualquier verify sobre un estado terminal devuelve falso sin revelar por qué al cliente; el motivo queda en tus logs.
De booleanos a estados
type Verification = {code: string;used: boolean;expired: boolean;attempts: number;};function canVerify(v: Verification) {return !v.used && !v.expired;}
Con dos booleanos tienes cuatro combinaciones, una de ellas absurda (used y expired a la vez). Con un enum, las combinaciones imposibles dejan de ser representables. Además, calcular la expiración con expiresAt en lugar de una bandera evita depender de un job que la actualice a tiempo.
Este modelo también simplifica la observabilidad: cada transición queda como un evento con timestamp, y un panel de soporte puede mostrarle al agente «emitida 10:03, reenviada 10:04 (cancela la anterior), aprobada 10:05» en vez de inferirlo desde columnas sueltas en una tabla.
En el playground de abajo simulamos justamente ese flujo: emitimos un código, lo cancelamos lógicamente reemitiendo uno nuevo (reenvío), y verificamos que solo el código vigente funcione.