Añadir autenticación de doble factor con contraseñas de un solo uso basadas en tiempo (TOTP) a tu propia aplicación es una de las funciones de seguridad de mayor valor que puedes lanzar, y es mucho más sencilla de lo que la mayoría de desarrolladores espera. TOTP es el algoritmo detrás de Google Authenticator, Authy, 1Password, Microsoft Authenticator y cualquier otra app que muestra un código rotatorio de seis dígitos. Está totalmente estandarizado en la RFC 6238 (que se apoya en el algoritmo de contraseña de un solo uso basado en HMAC de la RFC 4226), y como el estándar es universal, cualquier app autenticadora conforme funciona con cualquier servidor conforme. Esta guía recorre la implementación completa — alta, almacenamiento y verificación — de forma neutral en cuanto a lenguaje y framework, para que la mapees sobre el stack que ya uses. Si quieres la teoría de fondo sobre cómo se calculan los códigos, nuestro artículo pilar complementario sobre cómo funciona la autenticación TOTP cubre la matemática del HMAC y el paso de tiempo en profundidad; aquí nos centramos en el flujo práctico del lado del servidor.
Primera Regla: No Implementes el HMAC a Mano
Antes de escribir una sola línea, interioriza esto: existen librerías RFC 6238 maduras y bien auditadas en todos los lenguajes principales, y deberías usar una en lugar de implementar tú mismo el núcleo HMAC-SHA1. El algoritmo en sí es corto, pero sus modos de fallo son sutiles — la comparación en tiempo constante, la codificación big-endian correcta del contador y el manejo de base32 son todas oportunidades de introducir bugs silenciosos o canales laterales de temporización. Recurre a algo como otplib (JavaScript/TypeScript), pyotp (Python), rotp (Ruby), el paquete github.com/pquerna/otp (Go) o un equivalente contrastado. Tu trabajo es cablear correctamente el flujo de alta y verificación alrededor de esa librería y almacenar los secretos con seguridad. Ahí es donde las aplicaciones reales fallan de verdad, no en el HMAC.
El Flujo de Alta de un Vistazo
El alta es el apretón de manos único donde el servidor y la app autenticadora del usuario acuerdan un secreto compartido. El flujo completo es: generar un secreto aleatorio, codificarlo, mostrárselo al usuario como código QR (y como texto para entrada manual), pedirle que confirme tecleando un código que su app produce, y solo entonces marcar el doble factor como activo en la cuenta. Ese paso final de confirmación importa — nunca actives la 2FA hasta que el usuario haya demostrado que su app produce códigos correctos, o arriesgas dejarle fuera de su propia cuenta.
Paso 1 — Generar un Secreto Criptográficamente Aleatorio
El secreto compartido es la raíz de todo el esquema, así que debe provenir de una fuente aleatoria criptográficamente segura, nunca de Math.random(), un timestamp o un hash del nombre de usuario. La RFC 4226 recomienda un secreto compartido de al menos 128 bits y sugiere 160 bits (20 bytes), que es el estándar de facto que esperan las apps autenticadoras. Genera 20 bytes aleatorios del CSPRNG de tu plataforma — crypto.getRandomValues() en el navegador, crypto.randomBytes() en Node, secrets.token_bytes() en Python, crypto/rand en Go.
Esos bytes crudos se codifican en base32 según la RFC 4648, porque es el alfabeto que usan el formato Key URI y las apps autenticadoras. Base32 usa las 26 letras mayúsculas más los dígitos 2–7, lo que lo hace seguro para mostrar, imprimir y teclear a mano sin los problemas de sensibilidad a mayúsculas y ambigüedad de base64. Un secreto de 20 bytes se convierte en una cadena base32 de 32 caracteres. Muchas apps autenticadoras ignoran el relleno final =, así que la mayoría de librerías emiten el secreto sin relleno. Puedes ver exactamente qué aspecto tienen un secreto válido y sus códigos resultantes usando nuestro generador TOTP, que se ejecuta enteramente en tu navegador y es una forma cómoda de contrastar tu implementación de servidor con una referencia conocida como buena.
Paso 2 — Construir la URI otpauth://
El código QR que escanea un autenticador no contiene una imagen ni una cuenta — contiene una única línea de texto llamada Key URI, una cadena que empieza por otpauth://totp/. Este formato de facto, definido originalmente por Google Authenticator y hoy soportado universalmente, empaqueta todo lo que la app necesita en una sola URI. Su forma es:
- Tipo:
totp(basado en tiempo) en lugar dehotp(basado en contador). - Etiqueta:
Emisor:cuenta, por ejemploAcme:[email protected]. La cuenta suele ser el email o nombre de usuario para que pueda distinguir entradas. - secret: el secreto base32 del Paso 1 (obligatorio).
- issuer: el nombre de tu servicio, repetido como parámetro de consulta. Inclúyelo siempre — las apps lo usan como nombre a mostrar y para evitar entradas duplicadas.
- algorithm, digits, period: parámetros opcionales que por defecto valen
SHA1,6y30respectivamente.
Un detalle crítico para la interoperabilidad: incluye algorithm, digits y period solo cuando difieran de los valores por defecto. Algunas apps autenticadoras antiguas ignoran silenciosamente estos parámetros y siempre asumen SHA1/6/30; si emites un valor no predeterminado que la app ignora, sus códigos nunca coincidirán con tu servidor. A menos que tengas una razón concreta para desviarte, quédate con los valores por defecto y omite los parámetros por completo, produciendo una URI como otpauth://totp/Acme:[email protected]?secret=SECRETOBASE32&issuer=Acme. Para un desglose campo por campo de esta cadena, consulta nuestro artículo pilar complementario sobre la anatomía de un código QR de 2FA. Si tienes una razón para usar SHA-256 u ocho dígitos, lee antes nuestra guía hermana sobre cuándo los servicios usan SHA-256 o TOTP de 8 dígitos, porque los compromisos de compatibilidad son reales.
Paso 3 — Renderizar la URI como Código QR
Codifica esa cadena otpauth:// en un código QR y muéstralo durante el alta. Vale cualquier librería de QR — el código QR es solo un contenedor visual para el texto de la URI, nada más. Muestra también siempre el secreto base32 como texto plano junto a la imagen QR, para que los usuarios de autenticadores de escritorio, o cualquiera cuya cámara falle, puedan introducir el secreto manualmente. Genera la imagen QR en el servidor o el cliente a partir de la URI que construiste; no envíes secretos a un servicio web de imágenes QR de terceros, porque eso filtraría el secreto compartido a una parte externa.

Paso 4 — Almacenar el Secreto Cifrado en Reposo
Este es el paso que las aplicaciones fallan con más frecuencia. El secreto compartido es una credencial de larga vida, equivalente en sensibilidad a una contraseña: cualquiera que lo lea puede generar códigos válidos para siempre. Nunca almacenes secretos TOTP en texto plano. Cifra cada secreto en reposo con una clave simétrica (por ejemplo AES-GCM) guardada fuera de la base de datos — en un servicio de gestión de claves, un secreto de entorno o un módulo de seguridad hardware — y descífralo solo en memoria en el momento de verificar. A diferencia de una contraseña, no puedes hashear el secreto, porque la verificación requiere el valor original para recalcular los códigos; el cifrado con una clave gestionada aparte es el patrón correcto. Asocia el secreto cifrado al registro del usuario y guárdalo solo tras la confirmación del alta en el Paso 5.
Paso 5 — Verificar un Código y Tolerar la Deriva de Reloj
Para verificar un código que el usuario envía, el servidor calcula el TOTP esperado para el paso de tiempo actual y lo compara con lo que el usuario tecleó. Un paso de tiempo es el número de periodos de 30 segundos desde la época Unix: floor(tiempoUnixActual / 30). Alimentas ese contador y el secreto descifrado a la función TOTP de tu librería y obtienes los dígitos esperados.
La complicación es la deriva de reloj. El teléfono del usuario y tu servidor nunca están perfectamente sincronizados, y un código que el usuario lee un segundo antes de que rote puede llegar a tu servidor después del límite del paso. La RFC 6238 anticipa esto explícitamente y recomienda validar los códigos a lo largo de una pequeña ventana de pasos de tiempo adyacentes. La práctica estándar es aceptar el paso actual más o menos un paso (±1), lo que tolera hasta unos 30 segundos de deriva en cada dirección manteniendo minúscula la ventana de adivinación del atacante. No amplíes la ventana a varios minutos por ser indulgente — cada paso extra que aceptas multiplica linealmente el número de códigos válidos a la vez, debilitando el segundo factor. Usa además una comparación en tiempo constante al comprobar el código, cosa que cualquier librería buena hace por ti, para no filtrar información por la temporización de la respuesta.
Paso 6 — Limitar el Ritmo y Prevenir el Replay
Un código de seis dígitos solo tiene un millón de valores posibles, así que los endpoints de verificación deben defenderse contra la fuerza bruta. Limita el ritmo de intentos de verificación de forma agresiva — un número pequeño de fallos por cuenta y minuto, con backoff exponencial o bloqueo temporal — para que un atacante no pueda simplemente rociar adivinanzas. Sin límite de ritmo, un atacante decidido con una ventana ±1 válida podría plausiblemente forzar un código dentro de su vida útil.
Segundo, previene el replay. Como un código permanece válido durante todo su paso de tiempo (y tu ventana de aceptación), un código interceptado por encima del hombro o mediante un proxy de phishing podría enviarse dos veces. La RFC 6238 recomienda que una contraseña de un solo uso ya aceptada no vuelva a aceptarse. Impleméntalo almacenando el último paso de tiempo usado con éxito para cada usuario, y rechazando cualquier verificación cuyo paso sea menor o igual al valor guardado. Una vez consumido el paso N, los pasos N y anteriores están muertos — el usuario simplemente espera al siguiente código. Esto cierra la ventana de replay a cambio de una pequeña columna extra en tu base de datos.
Paso 7 — Emitir Códigos de Recuperación de un Solo Uso
Los teléfonos se pierden, se borran y se reemplazan. Si el secreto del autenticador es la única vía de entrada, un teléfono perdido significa una cuenta bloqueada permanentemente y una carga de soporte. En el alta, genera un conjunto de códigos de recuperación de un solo uso — típicamente entre ocho y diez cadenas aleatorias de alta entropía — muéstraselos al usuario una vez e indícale que los guarde en un lugar seguro. Almacena estos códigos hasheados (como contraseñas), marca cada uno como consumido en el momento en que se usa, y deja que un código de recuperación válido sustituya a un código TOTP durante el inicio de sesión. Los códigos de recuperación convierten un incidente de dispositivo perdido, de una crisis de recuperación de cuenta, en un inconveniente menor.
Un Esquema Conceptual
Juntando las piezas, la lógica del servidor es pequeña. En pseudocódigo neutral respecto al lenguaje, el alta es:
bytesSecreto = csprng(20)secretoB32 = base32Encode(bytesSecreto)uri = "otpauth://totp/" + etiqueta + "?secret=" + secretoB32 + "&issuer=" + emisor- muestra
uricomo QR mássecretoB32como texto; manténsecretoB32en estado pendiente hasta la confirmación.
Y la verificación es:
paso = floor(ahora / 30)- para
sen[paso-1, paso, paso+1]: siconstantTimeEqual(totp(secreto, s), codigoEnviado)ys > ultimoPasoUsadoentonces acepta, fijaultimoPasoUsado = s, listo. - en caso contrario rechaza y cuenta el intento contra el límite de ritmo.
Cada llamada a totp() de arriba es una única función de la librería que elijas. Nunca tocas el HMAC directamente.
Probar Tu Implementación
La RFC 6238 publica un apéndice oficial de vectores de prueba — secretos, marcas de tiempo específicas y los códigos exactos que deben producir para SHA1, SHA256 y SHA512. Ejecuta tu librería contra esos vectores primero; si coincide, tu núcleo es correcto. Luego haz una prueba de extremo a extremo: date de alta con una app autenticadora real, y contrasta de forma independiente el mismo secreto en nuestro generador TOTP basado en navegador para confirmar que los tres — la app, la herramienta y tu servidor — coinciden en el código actual. Cuando cuadren, sabes que tus pasos de tiempo, tu manejo de base32 y tu URI son todos correctos.
Errores Comunes a Evitar
Unos pocos bugs recurrentes explican la mayoría de implementaciones TOTP rotas. Emitir un parámetro algorithm o digits no predeterminado que la app ignora produce códigos que nunca coinciden. Almacenar el secreto antes de que el usuario confirme el alta deja a la gente fuera cuando teclea mal. Olvidar convertir el paso de tiempo a un contador big-endian de 8 bytes (si bajas por debajo de una librería) rompe la entrada del HMAC. Usar una variante de base32 sensible a mayúsculas o no conforme a la RFC 4648 corrompe el secreto. Y aceptar una ventana de verificación demasiado amplia, o saltarse la protección de replay, debilita silenciosamente todo el factor. Sigue los siete pasos anteriores, apóyate en una librería contrastada, y TOTP se convierte en una de las funciones de seguridad más robustas y de bajo mantenimiento de tu aplicación.