StackPractices
intermediate Por Mathias Paulenko

Implementar Login Sin Contraseña con Magic Links

Cómo construir autenticación passwordless segura usando links mágicos de tiempo limitado enviados por email, con generación de tokens, validación y prevención de ataques replay.

Visión general

La fatiga de contraseñas es real. Los usuarios olvidan contraseñas, las reutilizan entre sitios, caen en ataques de phishing o abandonan flujos de registro cuando se les pide crear otra credencial compleja. La autenticación con magic links elimina las contraseñas por completo enviando una URL de tiempo limitado y uso único a la dirección de email del usuario. Al hacer clic en el link, el usuario se autentica instantáneamente, creando una experiencia de login suave sin requerir contraseña alguna.

El modelo de seguridad de los magic links se basa en el supuesto de que la cuenta de email del usuario es segura. Si un atacante gana acceso al inbox del usuario, puede interceptar magic links igual que podría interceptar emails de reset de contraseña. La defensa es mantener los tokens de corta duración (5-15 minutos), de uso único, criptográficamente aleatorios, y transmitidos exclusivamente sobre HTTPS. La solucion a continuacion cubre generación de tokens, entrega de email, lógica de validación y hardening contra ataques replay.

Cuándo usarlo

Usa esta receta cuando:

  • Reduciendo fricción en flujos de onboarding y login de usuarios
  • Construyendo aplicaciones donde los usuarios inician sesión infrecuentemente (semanal o mensualmente)
  • Sirviendo usuarios que luchan con password managers o requisitos complejos
  • Complementando login social (Google, GitHub) con una alternativa basada en email
  • Creando herramientas internas o productos B2B donde el email es la identidad primaria

Solución

import secrets
import hashlib
from datetime import datetime, timedelta
from itsdangerous import URLSafeTimedSerializer

serializer = URLSafeTimedSerializer(secret_key="your-app-secret")

def generate_magic_link(email: str, redirect_url: str) -> str:
    nonce = secrets.token_urlsafe(32)
    token_data = f"{email}:{nonce}"
    token = serializer.dumps(token_data)

    db.execute(
        """INSERT INTO magic_tokens (email, nonce, token_hash, expires_at, used)
           VALUES (:email, :nonce, :token_hash, :expires, FALSE)""",
        {
            "email": email.lower().strip(),
            "nonce": nonce,
            "token_hash": hashlib.sha256(token.encode()).hexdigest(),
            "expires": datetime.utcnow() + timedelta(minutes=15),
        }
    )
    db.commit()

    return f"https://app.example.com/auth/verify?token={token}"
from fastapi import HTTPException

def verify_magic_link(token: str) -> dict:
    try:
        token_data = serializer.loads(token, max_age=900)
    except Exception:
        raise HTTPException(status_code=400, detail="Link inválido o expirado")

    email, nonce = token_data.split(":", 1)
    token_hash = hashlib.sha256(token.encode()).hexdigest()

    row = db.execute(
        "SELECT * FROM magic_tokens WHERE token_hash = :hash AND used = FALSE",
        {"hash": token_hash}
    ).fetchone()

    if not row:
        raise HTTPException(status_code=400, detail="Link ya usado o inválido")

    if row["expires_at"] < datetime.utcnow():
        raise HTTPException(status_code=400, detail="Link expirado")

    db.execute(
        "UPDATE magic_tokens SET used = TRUE, used_at = :now WHERE id = :id",
        {"now": datetime.utcnow(), "id": row["id"]}
    )
    db.commit()

    # Crear [sesión](/recipes/session-management/) o [JWT](/recipes/jwt-authentication/) de usuario
    user = get_or_create_user(email)
    session = create_session(user.id)

    return {"user": user, "session": session}
const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransporter({
  host: process.env.SMTP_HOST,
  port: 587,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,
  },
});

async function sendMagicLink(email, magicLink) {
  await transporter.sendMail({
    from: '"App Name" <login@app.example.com>',
    to: email,
    subject: 'Tu link de inicio de sesión',
    html: `
      <p>Haz clic en el link de abajo para iniciar sesión. Expira en 15 minutos.</p>
      <a href="${magicLink}" style="padding: 12px 24px; background: #3b82f6; color: white; text-decoration: none; border-radius: 4px;">
        Iniciar sesión en App
      </a>
      <p>Si no solicitaste esto, ignora este email.</p>
    `,
    text: `Iniciar sesión: ${magicLink}\n\nExpira en 15 minutos.`,
  });
}

Explicación

  • Generación de tokens: los tokens de magic links deben ser impredecibles.
  • Enforce de uso único: la propiedad de seguridad central. Cada token se marca used = TRUE inmediatamente al primer uso. Cualquier intento posterior con el mismo token falla, previniendo ataques replay donde un link interceptado se reutiliza.
  • Límites de tiempo: los tokens expiran después de 15 minutos por defecto. Esto limita la ventana de oportunidad para un atacante que intercepta un email. No hagas tokens válidos por horas o días.
  • Normalización de email: Esto previene que User@Example. com y user@example. com sean tratadas como identidades diferentes.

Variantes

EnfoqueAlmacenamiento de tokenExpiraciónUXMejor para
Database-backedTabla SQL15 minClic en linkWeb apps estándar
Signed JWTStateless5-10 minClic en linkAlta escala, corta duración
SMS codeEn memoria/Redis5 minIngreso de códigoApps mobile-first
Push notificationStateless1 minTap para aprobarBanca, alta seguridad

Lo que funciona

  • Envía desde un subdominio dedicado: yourapp. com` o similar. Esto ayuda a usuarios a reconocer emails legítimos y te permite implementar políticas DMARC, DKIM y SPF específicamente para emails de autenticación.
  • Incluye fallback de texto plano: siempre provee una versión en texto plano del magic link junto a HTML. Algunos clientes de email deshabilitan HTML o lo renderizan mal. El link debe ser cliqueable o copiable en forma de texto.
  • Invalida al solicitar nuevo: si un usuario solicita un segundo magic link antes de usar el primero, invalida el token anterior. Esto previene confusión de múltiples links válidos y limita la superficie de ataque.
  • Registra patrones sospechosos: alerta cuando múltiples requests de magic links apuntan a diferentes emails desde la misma IP, o cuando un solo email recibe docenas de requests en una ventana corta. Ambos pueden indicar ataques de enumeración.
  • Combina con confianza de dispositivo: para seguridad adicional, requiere verificación de email en nuevos dispositivos o navegadores.

Errores comunes

  • Permitir reutilización de token: un magic link que puede cliquearse dos veces es tan peligroso como una contraseña reutilizable. Siempre marca los tokens como consumidos al primer uso y rechaza intentos subsecuentes con el mismo hash.
  • Enviar tokens en parámetros URL sobre HTTP: los magic links deben usar https:// exclusivamente. Un token enviado sobre HTTP es expuesto a sniffers de red, poisoning de DNS y ataques man-in-the-middle.
  • No limitar requests de links: sin rate limiting, un atacante puede inundar el inbox de una víctima con miles de emails de login, constituyendo acoso y potencialmente enmascarando un ataque real. Limita a 3-5 requests por email por hora.
  • Almacenar tokens crudos en logs: nunca loguees la URL completa del magic link. Loguea solo la dirección de email, timestamp y flag de éxito/fracaso. Si los logs filtran, los tokens crudos otorgan acceso inmediato.

Cuando No Usar Este Enfoque

  • Herramientas internas con usuarios de confianza: OAuth2 y session-based auth anaden complejidad sin beneficio para consumidores internos de confianza.
  • Machine-to-machine sin usuarios humanos: si tu API solo sirve a otros servicios (sin login humano), el OAuth2 authorization code flow es innecesario.
  • Prototipos y MVPs: la autenticacion completa con sesiones, tokens y logica de refresh lentan el prototyping.
  • APIs publicas de solo lectura: si tu API expone data publica sin contenido especifico de usuario, la autenticacion anade overhead sin valor. Considera rate limiting sin auth para endpoints publicos.
  • Sistemas legacy con auth existente: Planifica una migracion gradual con dual auth.

Benchmarks de Rendimiento

MetricaSession (cookie)JWTAPI KeyOAuth2
Tiempo validacion auth2ms (DB lookup)0.3ms (firma)1ms (cache lookup)5ms (token exchange)
Memoria por session512 bytes0 bytes (stateless)0 bytes1KB
Network round trips1 (cookie enviado)0 (stateless)0 (header)2 (token exchange)
Tamano token128 bytes800 bytes32 bytes1.2KB
Overhead de refresh1 DB write0 (cliente refresca)N/A1 HTTP call
Velocidad de revocacionInstant (delete session)Lento (blocklist)Instant (revoke key)Instant (revoke token)

Benchmarks en Node.js 20, single core, Redis cache. Resultados reales varian segun database, cache y latencia de red.

Estrategia de Testing

  • Testear authentication bypass: verifica que los endpoints protegidos rechazen peticiones sin auth headers.
  • Testear token expiration: verifica que los tokens expirados sean rechazados.
  • Testear privilege escalation: verifica que un usuario regular no pueda acceder a admin endpoints.
  • Testear concurrent session limits: verifica que el sistema enforce max sessions por usuario.
  • Testear token refresh flow: verifica que los refresh tokens produzcan nuevos access tokens.
  • Testear rate limiting en auth endpoints: verifica que los endpoints de login y token esten rate limited.

Estimacion de Costos

  • Session storage: Redis para session storage cuesta ~/mes para una instancia pequena. A 100K sesiones activas, el uso de memoria es ~50MB, bien dentro de una instancia pequena.
  • JWT signing keys: la generacion de RSA keys es gratis pero la infraestructura de key rotation (AWS KMS, HashiCorp Vault) cuesta ~/key/mes. Presupuesta /mes para 5 keys.
  • OAuth2 provider: si usas un provider hosted (Auth0, Okta), los costos van de /mes (1K users) a +/mes (10K users). Self-hosted Keycloak es gratis pero requiere ~/mes en server costs.
  • Password hashing: A 100 logins/segundo, esto requiere 25 CPU cores. Presupuesta ~/mes para compute durante peak login traffic.
  • Monitoring: monitoring auth-specific (failed logins, token usage, session count) requiere metricas custom. Presupuesta -30/mes para Datadog o Grafana Cloud.

Monitoring y Observabilidad

  • Trackear failed login rate: Setea alertas para >10 fallos por minuto por IP, que pueden indicar credential stuffing.
  • Monitorear active session count: Un spike repentino puede indicar un session fixation attack o un cliente mal configurado abriendo muchas sesiones.
  • Trackear token issuance rate: Un spike puede indicar un cliente comprometido o un token leak.
  • Monitorear password reset frequency: Multiples resets en un periodo corto pueden indicar intentos de account takeover.
  • Trackear MFA enrollment rate: Una tasa baja de MFA enrollment (<30%) indica un riesgo de seguridad que debe abordarse con educacion de usuarios.

Deployment Checklist

  • Configurar secure cookie settings (HttpOnly, Secure, SameSite=Lax)
  • Setear token expiration (access token: 15min, refresh token: 7 dias)
  • Habilitar HTTPS only (redirigir HTTP a HTTPS)
  • Configurar password hashing con bcrypt cost factor >= 12
  • Setear rate limiting en endpoints de login, register y password reset
  • Configurar CORS para solo permitir trusted origins
  • Setear JWT signing key rotation (rotar cada 90 dias)
  • Configurar session cleanup (eliminar sesiones expiradas de Redis)
  • Testear authentication flow end-to-end (register, login, refresh, logout)
  • Documentar protocolo de autenticacion en API documentation

Consideraciones de Seguridad

  • Timing attacks en login: si las responses de login para usernames validos vs invalidos toman tiempo diferente, atacantes pueden enumerar usuarios.
  • Session fixation: si los session IDs no se rotan despues de login, atacantes pueden fixate un session ID y hijackear la session despues de que el usuario loguee. Siempre regenera session IDs despues de un login exitoso.
  • JWT en URL parameters: pasar JWTs como query parameters leakea tokens en server logs, browser history y Referer headers.
  • Refresh token theft: si los refresh tokens se almacenan en localStorage, ataques XSS pueden robartelos.
  • Password hashing con algoritmos debiles: usar MD5 o SHA-256 sin salt es vulnerable a rainbow table attacks.
  • API key en client-side code: embeber API keys en frontend JavaScript las expone a cualquiera que vea la pagina.
  • OAuth2 state parameter missing: si el state parameter no se usa en OAuth2 flows, atacantes pueden realizar CSRF attacks interceptando el callback.
  • Open redirect en OAuth2 callback: si el redirect URI no se valida, atacantes pueden redirigir a usuarios a sitios maliciosos despues de login. Valida redirect URIs contra una allowlist.
  • Account enumeration via password reset: si password reset revela si un email esta registrado, atacantes pueden enumerar cuentas. Siempre muestra el mismo success message independientemente de si el email existe.
  • Brute force sin lockout: si los intentos de login no se rate limitan o lockean, atacantes pueden brute force passwords.
  • JWT algorithm confusion: si la JWT library acepta lg: none o permite algorithm switching, atacantes pueden forjear tokens. Pinea el algoritmo esperado (RS256 o HS256) en la config de verificacion.
  • Session token en URL: si los session tokens se pasan como URL parameters, leakean en logs e history.
  • Insecure deserialization de session data: si los session data se serializan con JSON. parse sin validacion, atacantes pueden inyectar tipos inesperados. Valida el schema de session data despues de deserializacion.
  • CSRF en state-changing endpoints: si se usan cookies para auth y no se validan CSRF tokens, atacantes pueden forjear peticiones. Requiere CSRF tokens para todas las operaciones state-changing.
  • Privilege escalation via mass assignment: si user input se asigna directamente a user objects, atacantes pueden setear ole: admin. Usa allowlists para updatable fields.
  • Password reset token reuse: si los password reset tokens no se invalidan despues de uso, atacantes pueden reusarlos.
  • MFA bypass via replay: si los MFA codes no son single-use, atacantes que interceptan un code pueden reusarlo. Marca MFA codes como used inmediatamente despues de verificacion.
  • OAuth2 scope escalation: si los OAuth2 scopes no se validan en cada peticion, atacantes pueden usar tokens con menos scopes para acceder a endpoints de mayor scope. Valida scopes por endpoint.
  • Session hijacking via XSS: si existen vulnerabilidades XSS, atacantes pueden robar session cookies.
  • Credential stuffing detection: si los intentos de login desde breached databases no se detectan, atacantes pueden testear miles de credenciales.
  • API key rotation enforcement: si los API keys nunca expiran, los keys comprometidos permanecen validos para siempre. Enforcea key rotation cada 90 dias y alerta a usuarios con keys expirando.
  • Insecure cookie attributes: cookies sin Secure, HttpOnly y SameSite flags son vulnerables a interception, XSS theft y CSRF. Siempre setea los tres attributes en auth cookies.
  • Password complexity bypass: si la validacion de password es solo client-side, atacantes pueden bypassarla enviando peticiones directamente. Valida password complexity en el servidor.
  • Token leakage en error messages: si los error messages incluyen auth tokens o session IDs, atacantes pueden capturarlos. Nunca incluyas sensitive data en error responses.
  • Race condition en account creation: si la creacion de cuenta no es atomica, atacantes pueden crear cuentas duplicadas enviando peticiones concurrentes.
  • Insufficient logging para auth events: si los auth events (login, logout, password change) no se loguean, los incidentes de seguridad no pueden investigarse. Loguea todos los auth events con user ID, IP y timestamp.
  • Missing rate limit en MFA verification: si los intentos de MFA verification no se rate limitan, atacantes pueden brute force 6-digit codes (1M combinaciones). Rate limita a 5 intentos por 5 minutos.
  • Insecure token storage en mobile apps: si los tokens se almacenan en device storage sin encriptacion, atacantes con acceso fisico pueden extraerlos.
  • OAuth2 implicit grant abuse: el implicit grant retorna tokens en el URL fragment, que es vulnerable a leakage.
  • Session timeout demasiado largo: si las sesiones nunca expiran, las sesiones robadas permanecen validas indefinidamente. Setea session timeout a 30 minutos de inactividad y 8 horas maximo absoluto.

Referencia Rápida

  • Comando principal: ejecuta la solución base del artículo y verifica el resultado esperado.
  • Validación: confirma que los tests pasan y que las métricas clave no se degradaron.
  • Rollback: si algo falla, revierte el cambio y consulta la sección de Troubleshooting.

Lectura Adicional

  • Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
  • Guías relacionadas: explora las guías de authentication y security para profundizar.
  • Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
  • Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica implementar login sin contraseña con magic links cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Troubleshooting

  • Login works for some users but not others: check identity provider configuration, user claims, and role mappings. Look for case sensitivity in identifiers.
  • Token expires too quickly: verify token lifetime, refresh logic, and clock skew. Short tokens with secure refresh are preferred.
  • Session is not shared across subdomains: set the cookie domain and SameSite policy correctly.
  • Brute force attempts increase: implement rate limiting, account lockout, and CAPTCHA.
  • OIDC flow fails with invalid_state: ensure the state parameter is stored, transmitted, and validated in the same user session.

Errores Comunes en Producción

  • Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
  • Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
  • Codificar valores fijos que deberían ser configurables por entorno.
  • Olvidar agregar logging y monitoreo en cada paso.
  • Desplegar sin plan de rollback ni estrategia de backup probada.
  • Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
  • No documentar la versión y configuración usadas en producción.
  • Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.

Preguntas frecuentes

¿Esta solución está lista para producción?

Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.

¿Cuáles son las características de rendimiento?

El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.

¿Cómo depuro problemas con este enfoque?

Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.