Un correo con un código de un solo uso es el mensaje más impaciente que enviará tu producto. El usuario está mirando una pantalla que dice «Introduce el código de 6 dígitos que te hemos enviado», y cada segundo que tarda el correo es un segundo en el que se pregunta si rendirse.
Los correos de verificación también están justo en tu frontera de seguridad. Si los haces mal, o bloqueas a usuarios reales o dejas entrar a atacantes. Esta guía cubre ambos lados: hacer llegar el código a la bandeja de entrada rápido y asegurarte de que solo la persona correcta pueda usarlo.
¿Códigos o enlaces?
Hay dos formatos habituales:
- Códigos de un solo uso (OTP): un número corto como
482913, que se escribe de vuelta en tu app. Adecuados para el inicio de sesión y la verificación en dos pasos, y funcionan cuando el usuario lee el correo en otro dispositivo. - Enlaces de verificación: un token aleatorio largo en una URL. Adecuados para confirmar una dirección de correo en el registro o para restablecer una contraseña, porque un solo clic completa el proceso.
Todo lo que sigue se aplica a ambos. La diferencia es sobre todo la longitud: los enlaces pueden llevar un token largo; los códigos deben ser lo bastante cortos para escribirlos.
Genera los códigos de forma segura
Usa el generador aleatorio criptográficamente seguro de tu lenguaje. Math.random() y el módulo random de Python son predecibles y nunca deben usarse para códigos de seguridad.
// Node.js
import { randomInt, createHmac } from 'node:crypto';
const code = String(randomInt(0, 1_000_000)).padStart(6, '0'); // "048213"
// Keyed hash: OTP_SECRET lives in your secrets manager, never in the database.
const codeHash = createHmac('sha256', process.env.OTP_SECRET).update(`${user.id}:${code}`).digest('hex');
# Python
import hashlib
import hmac
import os
import secrets
code = f"{secrets.randbelow(1_000_000):06d}"
# Keyed hash: OTP_SECRET lives in your secrets manager, never in the database.
code_hash = hmac.new(os.environ["OTP_SECRET"].encode(), f"{user_id}:{code}".encode(), hashlib.sha256).hexdigest()
Guarda solo un hash con clave (HMAC), junto con el ID del usuario, una hora de caducidad y un contador de intentos. Un SHA-256 simple no basta: con solo un millón de códigos posibles, cualquiera que tenga la base de datos podría probarlos todos en menos de un segundo. El HMAC necesita un secreto guardado fuera de la base de datos, así que una filtración de la base de datos por sí sola no revela los códigos vigentes.
Caducidad rápida e intentos limitados
Un código de seis dígitos tiene un millón de posibilidades. Es suficiente solo si los atacantes no pueden seguir probando:
| Control | Valor recomendado |
|---|---|
| Validez del código | 10 minutos para códigos de inicio de sesión; hasta 24 horas para enlaces de confirmación de correo |
| Intentos fallidos por código | 5; después, el código deja de ser válido |
| Códigos enviados por dirección | Unos 3 cada 15 minutos, con una espera visible en «Reenviar» |
| Códigos enviados por dirección IP | Un tope más alto, para impedir que un atacante recorra muchas direcciones |
| Uso | Un solo uso: elimina el código o márcalo como usado en cuanto se valide |
Compara siempre los códigos en tiempo constante (crypto.timingSafeEqual en Node.js, hmac.compare_digest en Python), y emitir un código nuevo debe invalidar el anterior.
Envíalo de inmediato
Los correos de OTP deben salir en la misma solicitud que los pidió, no en un proceso por lotes que se ejecuta cada minuto. Con Inbixio es una sola llamada:
await fetch('https://backend.inbixio.com/api/send-email/', {
method: 'POST',
headers: { 'X-API-Key': process.env.INBIXIO_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
to_email: user.email,
subject: `${code} is your YourApp sign-in code`,
message: otpTemplate({ code, minutes: 10 }),
}),
signal: AbortSignal.timeout(10_000),
});
Inbixio acepta la solicitud y pone la entrega en cola al instante, así que tu endpoint puede responder rápido. Si la llamada falla, díselo al usuario enseguida («No hemos podido enviar tu código. Inténtalo de nuevo en un momento») en lugar de dejarlo esperando un correo que nunca llegará. Cómo gestionar los errores de la API de correo indica qué errores merecen un reintento automático.
Dos consejos de entrega específicos para los códigos:
- Usa un remitente dedicado, como
security@yourco.com, conectado como servidor propio y con una clave de API fijada a él (vuelve a fijar la clave si alguna vez eliminas ese servidor). Los usuarios aprenden a confiar en esa dirección, y las campañas de marketing no pueden afectar a su reputación. - Autentica tu dominio. Los códigos que acaban en spam son códigos que nunca llegan. Repasa la lista de comprobación de SPF, DKIM y DMARC.
Escribe un correo que no pueda confundirse con phishing
Tu plantilla compite con correos de phishing que copian exactamente este formato. Haz que el auténtico sea fácil de reconocer:
- Pon el código en el asunto, p. ej. «482913 es tu código de inicio de sesión de YourApp». Los usuarios pueden leerlo en la notificación sin abrir el correo, y los teclados del móvil pueden sugerirlo.
- Indica para qué sirve y cuánto dura: «Usa este código para iniciar sesión en YourApp. Caduca en 10 minutos».
- Indica qué hacer si no fue el usuario: «Si no has intentado iniciar sesión, puedes ignorar este correo. Tu cuenta está a salvo».
- No pidas nunca que te devuelvan el código. Dilo claramente: «YourApp nunca te pedirá este código por teléfono ni por correo».
- Que sea breve, sin marketing, y con los enlaces reducidos al mínimo imprescindible.
Una plantilla HTML sencilla y accesible:
<div style="font-family:system-ui,sans-serif;max-width:480px;margin:auto;color:#111">
<p>Use this code to sign in to YourApp:</p>
<p style="font-size:32px;font-weight:700;letter-spacing:6px;margin:24px 0">482913</p>
<p>It expires in 10 minutes. If you didn't try to sign in, you can ignore this email.</p>
<p style="color:#666;font-size:13px">YourApp will never ask you for this code.</p>
</div>
Los estilos en línea son intencionados. Muchos clientes de correo eliminan los bloques <style>, así que el CSS en línea es la forma más fiable de dar formato a un correo.
No reveles qué correos tienen cuenta
En una pantalla de inicio de sesión o de «envíame un código», muestra la misma respuesta tanto si la dirección está registrada como si no: «Si existe una cuenta con esa dirección, te hemos enviado un código». De lo contrario, tu formulario se convierte en una herramienta gratuita para comprobar qué direcciones de correo usan tu producto.
Lista de comprobación
- Códigos de un generador criptográficamente seguro; solo se guardan HMAC, con el secreto fuera de la base de datos
- Caducidad de 10 minutos, un solo uso, 5 intentos, comparación en tiempo constante
- Límites de envío por dirección y por IP, con espera para reenviar
- Envío inmediato, con un error claro si el envío falla
- Dirección remitente dedicada y autenticada
- Código en el asunto, finalidad y caducidad en el cuerpo, indicaciones de «¿no has sido tú?»
- Misma respuesta tanto si la cuenta existe como si no
Envíalo con Inbixio
Conecta tu propio servidor SMTP, crea una clave de API con permisos limitados y envía desde cualquier lenguaje con una sola solicitud HTTPS.