Flujos de cuenta (restablecimiento, verificación, enlace mágico)
Los flujos que toda aplicación necesita en los márgenes del inicio de sesión: restablecimiento de contraseña, verificación de correo electrónico e inicio de sesión por enlace mágico (sin contraseña). Los tres tienen la misma forma — enviar por correo al usuario un enlace a prueba de manipulaciones y con tiempo limitado, y luego actuar cuando hace clic en él — y Rustango los construye sobre un mismo sustrato: las URL firmadas. Una URL firmada es una URL normal con una firma HMAC añadida, de modo que el servidor puede confiar en sus parámetros sin almacenar nada.
¿Algún término aquí es nuevo para ti? HMAC, token, caducidad — consulta el glosario.
Fuente:
rustango::signed_url(sign,verify,SignedUrlError) yrustango::auth_flows(PasswordReset,EmailVerification,MagicLink,confirm_password_reset_pool_into) — tras las funcionalidadessigned_url/auth_flows(activadas por defecto; la confirmación de restablecimiento también necesitapasswords+ un backend de BD).Versión ejecutable: cada fragmento está copiado de
auth_flows_doc.rs(cargo test -p rustango --features sqlite --test auth_flows_doc).
Tabla de contenidos
- URL firmadas: el sustrato
- Restablecimiento de contraseña
- Verificación de correo electrónico
- Inicio de sesión por enlace mágico
- Tokens de un solo uso
- Lo que aportas tú
- Véase también
URL firmadas: el sustrato
sign añade una firma HMAC-SHA256 (y una caducidad opcional) sobre la ruta + la query de la URL.
verify la vuelve a calcular: manipula cualquier parámetro, usa el secreto equivocado o deja que
caduque, y falla.
use rustango::signed_url::{sign, verify, SignedUrlError};
let url = "https://app.example.com/files/42?user_id=7";
let signed = sign(url, secret, None); // None = never expires
assert!(verify(&signed, secret).is_ok());
// Flip any signed byte → InvalidSignature.
let tampered = signed.replace("user_id=7", "user_id=8");
assert_eq!(verify(&tampered, secret), Err(SignedUrlError::InvalidSignature));
Añade un TTL y un enlace caducado se rechaza (sign_at / verify_at toman segundos unix
explícitos para pruebas deterministas):
use rustango::signed_url::{sign_at, verify_at, SignedUrlError};
let signed = sign_at(url, secret, Some(100)); // expires at t=100
assert!(verify_at(&signed, secret, 50).is_ok()); // before → ok
assert_eq!(verify_at(&signed, secret, 1000), Err(SignedUrlError::Expired));
La query se ordena antes de firmar, así que el orden de los parámetros no importa. Los errores son
MissingSignature, MalformedSignature, InvalidSignature, Expired.
Restablecimiento de contraseña
Los asistentes de auth_flows envuelven las URL firmadas con una etiqueta de propósito (para
que un token de restablecimiento no pueda reproducirse como un enlace mágico) y codifican el
identificador del usuario. PasswordReset también incluye un asistente de confirmación que
verifica el token y rota el hash almacenado en una sola llamada.
use std::time::Duration;
use rustango::auth_flows::{PasswordReset, confirm_password_reset_pool_into};
// 1. User asks to reset → look them up → issue a link → email it.
let url = PasswordReset::issue(
"https://app.example.com/auth/reset", // your callback route
user_id, // encoded in the token
secret,
Duration::from_secs(3600), // 1-hour TTL
);
mailer.send(&Email::new().to(addr).subject("Reset your password").body(&url)).await?;
// 2. User clicks + submits a new password → verify + rotate the hash.
let user_id = confirm_password_reset_pool(
&pool, &url, "a-brand-new-strong-password", secret,
).await?;
Usa esta forma para
rustango_users. También marcapassword_changed_at, que es lo que termina las sesiones emitidas antes del restablecimiento (#1449)._intoacepta una tabla arbitraria y no puede suponer que exista una columna de rotación, así que solo escribe la contraseña — un restablecimiento por ahí deja válida toda sesión existente, incluida la de un atacante. Eso importa precisamente porque un restablecimiento es lo que alguien hace cuando cree que su cuenta está comprometida.
El asistente de confirmación aplica la política de contraseñas, aplica argon2id al nuevo password y lo escribe — rechazando entradas débiles, caducadas, manipuladas o con el secreto equivocado sin tocar la fila:
// valid token + strong pw → hash rotated (starts "$argon2…")
// "12345678" → Err(WeakPassword), nothing written
// user_id tampered → Err(InvalidSignature), nothing written
Es el mismo passwords::strength_score que usa el resto del framework, así que una contraseña
rechazada en el registro no puede establecerse restableciéndola (#1399).
_intoapunta a tu propia tabla/columnas — por ejemplo unapp_usersde inquilino. Si tiene un equivalente depassword_changed_at, márcalo tú mismo en la misma transacción, o el restablecimiento no terminará las sesiones existentes.
Haz que el enlace sea de un solo uso
Los asistentes anteriores solo verifican firma y caducidad, así que el enlace sigue siendo utilizable durante todo su TTL — incluso después de cambiar la contraseña. Una copia del correo en un buzón compartido, un mensaje reenviado o un ticket de soporte con el correo pegado es una toma de control de la cuenta en funcionamiento hasta que el token caduque, en un momento en que el usuario legítimo ya ha terminado y no tiene motivo para sospechar.
Pasa una caché y el token se consume en el primer uso:
use rustango::auth_flows::confirm_password_reset_single_use;
let user_id = confirm_password_reset_single_use(
&pool, &url, "a-brand-new-strong-password", secret, &cache,
).await?; // una repetición → Err(AuthFlowError::AlreadyUsed)
_single_use_into toma la misma tabla/columnas que _into. La política de contraseñas se
comprueba antes de consumir el token, de modo que una contraseña rechazada no le cuesta el
enlace al usuario.
La marca de usado vive en la caché, no en el token, así que todas las réplicas deben compartir una caché — dos instancias significan dos listas negras y ninguna aplicación.
Verificación de correo electrónico
EmailVerification codifica tanto el identificador del usuario como el correo electrónico, de
modo que al verificar recuperas ambos y puedes confirmar que la dirección sigue coincidiendo
(atrapando enlaces enviados antes de un cambio de correo). Aquí no hay ninguna escritura en BD
integrada — tú defines tu propia columna «verificado»:
use rustango::auth_flows::EmailVerification;
// On signup:
let url = EmailVerification::issue(callback, user_id, &email, secret, Duration::from_secs(86_400));
mailer.send(&Email::new().to(&email).subject("Confirm your email").body(&url)).await?;
// On click:
let (user_id, email) = EmailVerification::verify(&url, secret)?;
// → if email still matches the user's current address, mark them verified
Inicio de sesión por enlace mágico
MagicLink codifica solo el correo electrónico — el usuario hace clic, tú buscas la cuenta y
acuñas una sesión. Mantén el TTL corto (10–30 min) y hazlo de un solo uso
(siguiente sección), ya que el enlace es la credencial:
use rustango::auth_flows::MagicLink;
let url = MagicLink::issue(callback, &email, secret, Duration::from_secs(900));
mailer.send(&Email::new().to(&email).subject("Your sign-in link").body(&url)).await?;
// On click:
let email = MagicLink::verify_single_use(&url, secret, &cache).await?;
// → look up the user by email, create a session
Tokens de un solo uso
verify por sí solo únicamente comprueba firma + caducidad, así que un enlace filtrado es
reproducible hasta que caduca. Para el inicio de sesión y el restablecimiento, prefiere
verify_single_use(url, secret, &cache) — registra la firma del token en un Cache y rechaza un
segundo uso:
// first click → Ok(email)
// same link reused → Err(AuthFlowError::AlreadyUsed)
Respáldalo con un caché compartido (Redis) en producción para que un token no pueda reproducirse contra una réplica distinta. La comprobación falla en modo cerrado (un error de caché rechaza en lugar de arriesgar una reproducción).
Lo que aportas tú
El framework emite/verifica los tokens y (para el restablecimiento) escribe el hash; tu aplicación aporta el resto:
- Un secreto (una clave de aplicación estable; 32 bytes por convención).
- Un mailer para enviar los enlaces —
rustango::emailincluyeConsoleMailer,SmtpMailereInMemoryMailer(útil en pruebas). - Una tabla de usuarios con las columnas que cada flujo necesita (correo para la búsqueda de verificación/enlace mágico; una columna de hash de contraseña para el restablecimiento; una columna «verificado» que es tuya).
- Las rutas de callback que reciben el clic y la acuñación de sesión para el inicio de sesión por enlace mágico.
Véase también
- Contraseñas — el hashing que rota el restablecimiento.
- Sesiones — lo que el inicio de sesión por enlace mágico crea al tener éxito.
- Firma de peticiones HMAC — la misma primitiva HMAC, aplicada a peticiones de API en lugar de a URL.
- Guía de seguridad — la lista de endurecimiento más amplia.
