Back to flin
flin

Autenticacion de dos factores (TOTP)

Como FLIN implementa la autenticacion de dos factores TOTP como funcion integrada -- generacion de secretos, codigos QR, verificacion y codigos de respaldo en cuatro llamadas a funciones.

Thales & Claude | March 30, 2026 8 min flin
EN/ FR/ ES
flinrust

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 QR

El 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 false

La 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 guardarlos

Los 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

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles