Saltar al contenido
verificaOTP

Capítulo 4 de 5 · 13 min de lectura

Expiración y reintentos

Ventanas de vigencia, límites de intentos y backoff de reenvío calibrados para usuarios reales.

Dos perillas definen el balance entre seguridad y conversión: cuánto dura un código y cuántas veces se puede intentar o reenviar. Muy estrictas y castigas al usuario con mala cobertura en el metro; muy laxas y regalas tiempo a quien está adivinando.

Ventanas de vigencia

Para SMS, cinco minutos es un buen punto de partida: cubre la gran mayoría de las entregas tardías sin dejar el código expuesto demasiado tiempo. Para canales de chat, que entregan casi instantáneo, puedes bajar a tres. Para voz, sube a diez, porque el usuario debe escuchar y anotar.

Intentos y reenvíos

Con cinco intentos sobre seis dígitos, la probabilidad de acertar al azar es 5 en 1.000.000 por verificación. El riesgo real aparece cuando el atacante puede pedir verificaciones nuevas sin límite: por eso el contador de intentos debe acompañarse de un límite de emisiones por número y por ventana de tiempo.

Entel, Movistar, WOM y Claro cobran de forma distinta el tráfico transaccional a sus propias bases frente a numeración fuera de red, así que un atacante que automatiza solicitudes de código hacia un rango de números ajeno a Chile puede generarte un gasto notable en pesos chilenos en pocas horas si no limitas por IP, por dispositivo y por número de destino a la vez.

Un patrón simple y efectivo es exigir un pequeño desafío (un captcha invisible o una validación de dispositivo) a partir de la tercera solicitud de código en diez minutos desde el mismo origen, sin bloquear por completo al usuario legítimo que simplemente no vio llegar el primer SMS.

// Resend backoff: 30s, 60s, 120s, then block for 1h
const RESEND_DELAYS_S = [30, 60, 120];

export function nextResendAt(sentAt: number[], now: number): number | "blocked" {
  const recent = sentAt.filter((t) => now - t < 60 * 60 * 1000);
  if (recent.length > RESEND_DELAYS_S.length) return "blocked";
  const last = recent.at(-1) ?? 0;
  const delay = RESEND_DELAYS_S[recent.length - 1] ?? 0;
  return last + delay * 1000;
}

Mensajes de error honestos, pero no informativos

Al usuario dile «El código no es válido o ya venció» para cualquier fallo. Distinguir «código incorrecto» de «número no registrado» ayuda a un atacante a enumerar cuentas. La precisión va a tus logs y a tu panel de métricas, no a la pantalla.

Se marca completado al leer el 80%.

Comentarios (2)

Se guardan solo en tu navegador.

  • Rodrigo Alarcón13 oct

    Subimos la vigencia de 3 a 5 minutos en SMS y la tasa de éxito en regiones subió notoriamente. En Santiago casi no se notó.

  • Josefa Castillo14 oct

    Buen punto el de no distinguir errores. Nuestro QA lo marcó como bug de UX y era justamente lo correcto.