Las contrasenas solas no son suficientes. Los ataques de phishing, el credential stuffing y las brechas de bases de datos significan que incluso contrasenas fuertes y unicas pueden verse comprometidas. La autenticacion de dos factores agrega una segunda capa: algo que tiene (su telefono) ademas de algo que sabe (su contrasena).
TOTP (contrasena de un solo uso basada en tiempo) es el estandar 2FA mas ampliamente desplegado. Genera un codigo de 6 digitos que cambia cada 30 segundos, basado en un secreto compartido y la hora actual. Google Authenticator, Authy, 1Password y Bitwarden lo soportan.
FLIN implementa TOTP como cuatro funciones integradas: generate_totp_secret(), totp_qr_url(), verify_totp() y generate_backup_codes(). Sin biblioteca. Sin servicio externo. Sin configuracion.
Habilitacion de 2FA para un usuario
El flujo de configuracion es directo: generar un secreto, mostrar el codigo QR, verificar el primer codigo y almacenar el secreto.
flin// app/settings/two-factor.flin
guard auth
secret = ""
qr_url = ""
setup_complete = false
fn begin_setup() {
secret = generate_totp_secret()
session.pending_totp_secret = secret
qr_url = totp_qr_url(secret, session.user, "MyApp")
}
fn verify_setup(code) {
pending = session.pending_totp_secret
if pending != "" && verify_totp(pending, code) {
user = User.find(to_int(session.userId))
user.totp_secret = pending
user.totp_enabled = true
save user
session.pending_totp_secret = none
setup_complete = true
}
}
<main>
{if setup_complete}
<h2>Two-factor authentication enabled</h2>
<p>Your account is now protected with 2FA.</p>
{else if qr_url != ""}
<h2>Scan this QR code with your authenticator app</h2>
<img src={qr_url} alt="TOTP QR Code">
<p>Or enter this secret manually: <code>{secret}</code></p>
<input type="text" placeholder="Enter the 6-digit code" bind={code}>
<button click={verify_setup(code)}>Verify and Enable</button>
{else}
<button click={begin_setup()}>Set Up Two-Factor Authentication</button>
{/if}
</main>El codigo QR contiene una URL otpauth:// que las aplicaciones de autenticacion reconocen. Escanearlo agrega la cuenta automaticamente. La entrada manual del secreto es un respaldo para usuarios que no pueden escanear codigos QR.
Las cuatro funciones TOTP
generate_totp_secret()
Genera un secreto aleatorio de 20 bytes (160 bits) codificado en Base32:
flinsecret = generate_totp_secret()
// Retorna: "JBSWY3DPEHPK3PXP4WBQGZLSMY"El secreto se genera usando el generador de numeros aleatorios criptograficos del sistema operativo. Nunca es predecible, nunca se reutiliza y nunca se deriva de datos del usuario.
totp_qr_url(secret, account, issuer)
Genera una URL para una imagen de codigo QR que contiene la informacion de configuracion TOTP:
flinqr_url = totp_qr_url(secret, "[email protected]", "MyApp")
// Retorna una URL de datos o una URL a la imagen del codigo QREl codigo QR codifica una URI como: otpauth://totp/MyApp:[email protected]?secret=JBSWY3DPEHPK3PXP4WBQGZLSMY&issuer=MyApp&algorithm=SHA1&digits=6&period=30
verify_totp(secret, code)
Verifica un codigo TOTP de 6 digitos contra el secreto:
flinis_valid = verify_totp(secret, "483927")
// Retorna: true o falseLa verificacion acepta codigos del paso de tiempo actual, el paso anterior y el siguiente (una ventana de 90 segundos en total). Esto compensa la deriva del reloj entre el servidor y el telefono del usuario.
generate_backup_codes(count)
Genera codigos de recuperacion de un solo uso para usuarios que pierdan acceso a su aplicacion de autenticacion:
flincodes = generate_backup_codes(10)
// Retorna: ["A1B2-C3D4", "E5F6-G7H8", "I9J0-K1L2", ...]Cada codigo es una cadena unica y aleatoria de 8 caracteres que puede usarse una vez en lugar de un codigo TOTP. Despues del uso, el codigo se invalida.
El flujo de inicio de sesion con 2FA
Cuando 2FA esta habilitado, el proceso de inicio de sesion agrega un paso de verificacion:
flin// app/api/auth/login.flin
guard rate_limit(5, 60)
route POST {
validate {
email: text @required @email
password: text @required
}
user = User.where(email == body.email).first
if user == none || !verify_password(body.password, user.password) {
return error(401, "Invalid credentials")
}
// Verificar si 2FA esta habilitado
if user.totp_enabled {
// Emitir un token temporal que requiere completar 2FA
temp_token = create_token(user, {
expires: "5m",
claims: { type: "2fa_pending", requires_2fa: true }
})
return {
requires_2fa: true,
temp_token: temp_token
}
}
// Sin 2FA -- emitir token de acceso completo
token = create_token(user, { expires: "7d" })
{ access_token: token, user: user }
}flin// app/api/auth/verify-2fa.flin
guard rate_limit(5, 60)
route POST {
validate {
temp_token: text @required
code: text @required
}
claims = verify_token(body.temp_token)
if claims == none || claims.type != "2fa_pending" {
return error(401, "Invalid or expired token")
}
user = User.find(to_int(claims.sub))
if user == none {
return error(401, "User not found")
}
// Intentar codigo TOTP
if verify_totp(user.totp_secret, body.code) {
token = create_token(user, { expires: "7d" })
return { access_token: token, user: user }
}
// Intentar codigo de respaldo
backup = BackupCode.where(user_id == user.id && code == body.code && used == false).first
if backup != none {
backup.used = true
save backup
token = create_token(user, { expires: "7d" })
return { access_token: token, user: user }
}
error(401, "Invalid 2FA code")
}El flujo es: 1. El usuario envia correo electronico y contrasena. 2. Si la contrasena es correcta y 2FA esta habilitado, el servidor retorna un token temporal de corta duracion. 3. El cliente muestra la pantalla de entrada de 2FA. 4. El usuario ingresa su codigo TOTP (o codigo de respaldo). 5. El servidor verifica el codigo y emite el token de acceso completo.
El token temporal expira en 5 minutos, previniendo ataques de repeticion.
Detalles de implementacion TOTP
El algoritmo TOTP (RFC 6238) se construye sobre HOTP (contrasena de un solo uso basada en HMAC, RFC 4226):
rustuse hmac::{Hmac, Mac};
use sha1::Sha1;
type HmacSha1 = Hmac<Sha1>;
const TOTP_PERIOD: u64 = 30; // Paso de tiempo de 30 segundos
const TOTP_DIGITS: u32 = 6; // Codigo de 6 digitos
const TOTP_WINDOW: i64 = 1; // Aceptar +/- 1 paso de tiempo
pub fn generate_totp(secret: &[u8], time: u64) -> String {
let counter = time / TOTP_PERIOD;
let counter_bytes = counter.to_be_bytes();
let mut mac = HmacSha1::new_from_slice(secret).unwrap();
mac.update(&counter_bytes);
let hmac_result = mac.finalize().into_bytes();
// Truncamiento dinamico
let offset = (hmac_result[19] & 0x0f) as usize;
let code = ((hmac_result[offset] as u32 & 0x7f) << 24)
| ((hmac_result[offset + 1] as u32) << 16)
| ((hmac_result[offset + 2] as u32) << 8)
| (hmac_result[offset + 3] as u32);
let otp = code % 10u32.pow(TOTP_DIGITS);
format!("{:0>width$}", otp, width = TOTP_DIGITS as usize)
}
pub fn verify_totp_code(secret: &[u8], code: &str) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
for offset in -TOTP_WINDOW..=TOTP_WINDOW {
let time = (now as i64 + offset * TOTP_PERIOD as i64) as u64;
if constant_time_eq(code.as_bytes(), generate_totp(secret, time).as_bytes()) {
return true;
}
}
false
}Detalles clave de seguridad:
Ventana de tiempo. La verificacion comprueba el paso de tiempo actual mas uno antes y uno despues (90 segundos en total). Esto maneja la deriva del reloj sin hacer la ventana tan amplia que los codigos sean reutilizables.
Comparacion de tiempo constante. Incluso para codigos TOTP, la comparacion es de tiempo constante. Un atacante no puede determinar que digitos son correctos midiendo el tiempo de respuesta.
Prevencion de repeticion. Un codigo TOTP usado deberia idealmente ser rechazado durante el resto de su paso de tiempo. FLIN rastrea el ultimo contador usado por usuario para prevenir repeticion inmediata.
Codigos de respaldo
Los codigos de respaldo se generan como cadenas aleatorias de 8 caracteres y se almacenan como valores hasheados en la base de datos:
flin// Generar y mostrar al usuario (una sola vez)
codes = generate_backup_codes(10)
for code in codes {
save BackupCode {
user_id: user.id,
code_hash: hash_password(code),
used: false
}
}
// Mostrar codigos al usuario -- deben guardarlosLos codigos de respaldo se hashean antes del almacenamiento porque son equivalentes a una contrasena. Si la base de datos se ve comprometida, el atacante no puede usar los codigos de respaldo directamente.
Cuando un usuario ingresa un codigo de respaldo, la verificacion comprueba todos los codigos de respaldo no usados para ese usuario:
flinfn verify_backup(user_id, code) {
backups = BackupCode.where(user_id == user_id && used == false)
for backup in backups {
if verify_password(code, backup.code_hash) {
backup.used = true
save backup
return true
}
}
return false
}Deshabilitacion de 2FA
Los usuarios pueden deshabilitar 2FA despues de proporcionar su codigo TOTP actual (para probar que aun tienen acceso a su aplicacion de autenticacion):
flinroute POST "/disable-2fa" {
guard auth
validate {
code: text @required
}
user = User.find(to_int(session.userId))
if !verify_totp(user.totp_secret, body.code) {
return error(401, "Invalid 2FA code")
}
user.totp_enabled = false
user.totp_secret = ""
save user
// Eliminar codigos de respaldo restantes
BackupCode.where(user_id == user.id).delete_all
{ success: true, message: "Two-factor authentication disabled" }
}El requisito de proporcionar un codigo TOTP valido antes de deshabilitar 2FA previene que un atacante que haya robado una sesion deshabilite el segundo factor.
La implementacion 2FA de FLIN son cuatro funciones, cero dependencias y un protocolo estandar que funciona con cada aplicacion de autenticacion existente. El desarrollador no necesita entender HMAC, SHA-1, truncamiento dinamico o codificacion Base32. Llama a las funciones y la seguridad esta ahi.
En el proximo articulo, cubrimos OAuth2 y autenticacion social -- como FLIN se conecta a Google, GitHub, Discord, Apple, LinkedIn y Telegram con funciones de autenticacion integradas.
Esta es la Parte 110 de la serie "Como construimos FLIN", que documenta como un CEO en Abidjan y un CTO de IA disenaron y construyeron un lenguaje de programacion desde cero.
Navegacion de la serie: - [109] Limitacion de tasa y cabeceras de seguridad - [110] Autenticacion de dos factores (TOTP) (estas aqui) - [111] OAuth2 y autenticacion social - [112] Autenticacion OTP por WhatsApp para Africa