Back to flin
flin

Autenticacion JWT en 3 lineas de FLIN

Como las funciones JWT integradas de FLIN -- create_token, verify_token, refresh_token -- reducen la autenticacion basada en tokens a tres lineas de codigo con valores seguros por defecto.

Juste Thales Gnimavo & Claude | March 26, 2026 8 min flin
EN/ FR/ ES
flinrust

Los JSON Web Tokens se han convertido en el estandar para la autenticacion sin estado en APIs web. El concepto es simple: crear un token firmado que contenga la identidad del usuario, enviarlo al cliente y verificarlo en cada solicitud. La implementacion, en la mayoria de los frameworks, es cualquier cosa menos simple.

En una aplicacion tipica de Node.js, la autenticacion JWT requiere instalar jsonwebtoken, express-jwt y jwks-rsa. Se genera una clave secreta, se configuran las opciones de firma, se escribe middleware para extraer y verificar tokens, se manejan tokens expirados, se implementa la logica de renovacion y se gestiona la revocacion de tokens. El concepto "simple" se convierte en mas de 200 lineas de configuracion y codigo repetitivo repartidas en multiples archivos.

FLIN reduce esto a tres funciones integradas:

flin// Crear un token
token = create_token(user, { expires: "7d" })

// Verificar un token
claims = verify_token(token)

// Renovar un token
new_token = refresh_token(old_token)

Tres lineas. Tres funciones. El algoritmo de firma, la gestion de claves secretas, el manejo de expiracion y la estructura de claims son manejados por el runtime.

Creacion de tokens

La funcion create_token() genera un JWT firmado:

flin// Minimo: solo la entidad de usuario
token = create_token(user)
// Expiracion por defecto: 24 horas

// Con expiracion personalizada
token = create_token(user, { expires: "7d" })

// Con claims personalizados
token = create_token(user, {
    expires: "30d",
    claims: {
        role: user.role,
        org_id: user.org_id,
        plan: user.plan
    }
})

El token generado contiene claims JWT estandar:

json{
    "sub": "42",
    "email": "[email protected]",
    "name": "Thales Gnimavo",
    "role": "admin",
    "iat": 1711411200,
    "exp": 1712016000,
    "iss": "flin"
}

El claim sub (sujeto) es siempre el ID del usuario. Los claims iat (emitido en) y exp (expiracion) se establecen automaticamente. Los claims personalizados de la opcion claims se fusionan en el payload.

Verificacion de tokens

La funcion verify_token() decodifica y valida un JWT:

flinclaims = verify_token(token)
// Retorna los claims decodificados si es valido, none si es invalido

if claims == none {
    return error(401, "Invalid or expired token")
}

user_id = claims.sub
user_role = claims.role

Verificaciones realizadas:

  1. La firma es valida (el token no fue manipulado).
  2. El token no ha expirado (exp esta en el futuro).
  3. El token fue emitido por esta aplicacion (iss coincide).
  4. La estructura del token es valida (tres segmentos codificados en Base64).

Si alguna verificacion falla, verify_token retorna none. No hay excepcion que capturar, no hay codigo de error que decodificar. O el token es valido y se obtienen los claims, o no lo es y se obtiene none.

El patron de middleware de autenticacion

El uso mas comun de JWT en FLIN es un middleware de autenticacion que extrae el token de la cabecera Authorization:

flin// app/api/_middleware.flin

middleware {
    auth_header = headers["Authorization"]

    if auth_header != "" && auth_header.starts_with("Bearer ") {
        token = auth_header.slice(7)
        claims = verify_token(token)

        if claims != none {
            request.user = claims
            request.user_id = to_int(claims.sub)
        }
    }

    next()
}

Este middleware no rechaza solicitudes no autenticadas -- simplemente adjunta la informacion del usuario si hay un token valido presente. Los guards a nivel de ruta manejan el rechazo:

flin// app/api/profile.flin

guard auth    // Rechaza si request.user no esta establecido

route GET {
    User.find(request.user_id)
}

Esta separacion significa que los endpoints publicos (como listar productos) funcionan sin autenticacion, mientras que los endpoints protegidos (como ver un perfil) estan protegidos explicitamente.

El flujo completo de inicio de sesion

Aqui hay un endpoint completo de inicio de sesion basado en JWT:

flin// app/api/auth/login.flin

guard rate_limit(5, 60)    // 5 intentos por minuto

route POST {
    validate {
        email: text @required @email
        password: text @required
    }

    user = User.where(email == body.email).first

    // Mismo error para "no encontrado" y "contrasena incorrecta"
    if user == none || !verify_password(body.password, user.password) {
        return error(401, "Invalid credentials")
    }

    // Verificar bloqueo de cuenta
    if user.locked_until != none && user.locked_until > now {
        return error(423, "Account locked. Try again later.")
    }

    // Reiniciar intentos de inicio de sesion en caso de exito
    user.login_attempts = 0
    save user

    // Crear tokens
    access_token = create_token(user, {
        expires: "15m",
        claims: { role: user.role }
    })

    refresh_token_val = create_token(user, {
        expires: "7d",
        claims: { type: "refresh" }
    })

    {
        access_token: access_token,
        refresh_token: refresh_token_val,
        expires_in: 900,
        user: {
            id: user.id,
            name: user.name,
            email: user.email,
            role: user.role
        }
    }
}

La respuesta contiene tanto un token de acceso (de corta duracion, 15 minutos) como un token de renovacion (de larga duracion, 7 dias). El token de acceso se envia con cada solicitud API. Cuando expira, el cliente usa el token de renovacion para obtener un nuevo token de acceso.

Renovacion de tokens

La funcion refresh_token() valida un token existente y emite uno nuevo con una expiracion fresca:

flin// app/api/auth/refresh.flin

route POST {
    validate {
        refresh_token: text @required
    }

    claims = verify_token(body.refresh_token)
    if claims == none {
        return error(401, "Invalid refresh token")
    }

    if claims.type != "refresh" {
        return error(401, "Not a refresh token")
    }

    user = User.find(to_int(claims.sub))
    if user == none {
        return error(401, "User not found")
    }

    new_access = create_token(user, {
        expires: "15m",
        claims: { role: user.role }
    })

    new_refresh = create_token(user, {
        expires: "7d",
        claims: { type: "refresh" }
    })

    {
        access_token: new_access,
        refresh_token: new_refresh,
        expires_in: 900
    }
}

El endpoint de renovacion valida el token de renovacion, verifica que sea efectivamente un token de renovacion (no un token de acceso reutilizado), busca al usuario y emite tokens frescos. Si el usuario ha sido eliminado o desactivado desde que se emitio el token de renovacion, la renovacion falla.

Firma y gestion de secretos

FLIN usa HMAC-SHA256 para la firma de tokens por defecto. El secreto de firma se deriva de la variable de entorno JWT_SECRET de la aplicacion:

flin// .env
JWT_SECRET=a-very-long-random-string-at-least-256-bits

Si JWT_SECRET no esta establecido, FLIN genera un secreto aleatorio al inicio y registra una advertencia. Esto significa que el desarrollo funciona sin configuracion, pero la produccion requiere un secreto explicito.

La implementacion en Rust:

rustuse hmac::{Hmac, Mac};
use sha2::Sha256;

type HmacSha256 = Hmac<Sha256>;

fn sign_token(header: &str, payload: &str, secret: &[u8]) -> String {
    let message = format!("{}.{}", header, payload);
    let mut mac = HmacSha256::new_from_slice(secret)
        .expect("HMAC can take key of any size");
    mac.update(message.as_bytes());
    let signature = mac.finalize().into_bytes();
    base64url_encode(&signature)
}

fn verify_signature(token: &str, secret: &[u8]) -> bool {
    let parts: Vec<&str> = token.splitn(3, '.').collect();
    if parts.len() != 3 { return false; }

    let expected = sign_token(parts[0], parts[1], secret);
    constant_time_eq(parts[2].as_bytes(), expected.as_bytes())
}

La funcion constant_time_eq previene ataques de temporizado en la verificacion de firmas. Un atacante no puede determinar cuantos bytes de su firma falsificada son correctos midiendo el tiempo de respuesta.

Autenticacion basada en sesion vs. basada en tokens

FLIN soporta tanto autenticacion basada en sesion como basada en tokens. La eleccion depende de la aplicacion:

Basada en sesion (basada en cookies) es mejor para aplicaciones web tradicionales donde el frontend y el backend estan en el mismo dominio. El sistema de sesiones de FLIN maneja esto automaticamente a traves del objeto session.

Basada en tokens (JWT) es mejor para APIs consumidas por aplicaciones moviles, SPAs en diferentes dominios o integraciones de terceros. Las funciones JWT de FLIN manejan esto.

Muchas aplicaciones FLIN usan ambas: basada en sesion para la interfaz web y JWT para la API. El middleware puede verificar ambas:

flinmiddleware {
    // Verificar sesion primero
    if session.user != "" {
        user = User.where(email == session.user).first
        if user != none {
            request.user = user
        }
    }

    // Verificar JWT si no hay sesion
    if request.user == none {
        auth = headers["Authorization"]
        if auth != "" && auth.starts_with("Bearer ") {
            claims = verify_token(auth.slice(7))
            if claims != none {
                request.user = claims
                request.user_id = to_int(claims.sub)
            }
        }
    }

    next()
}

Garantias de seguridad

La implementacion JWT de FLIN proporciona varias garantias que no son opcionales:

Sin tokens sin firmar. No existe una opcion algorithm: "none". Cada token esta firmado.

Sin algoritmos debiles. El algoritmo minimo de firma es HMAC-SHA256. No hay MD5, no hay SHA-1, no hay RSA con menos de 2048 bits.

Expiracion obligatoria. Cada token tiene un tiempo de expiracion. Si no se especifica uno, se usa el predeterminado (24 horas). No hay forma de crear un token que nunca expire.

Verificacion de tiempo constante. La comparacion de firmas siempre toma la misma cantidad de tiempo, independientemente de cuantos bytes coincidan.

Validacion automatica de claims. La funcion verify_token verifica exp, iss y la validez de la firma. No se puede olvidar verificar la expiracion.

Estas garantias significan que un desarrollador FLIN que use create_token y verify_token obtiene autenticacion JWT segura sin necesitar entender las implicaciones de seguridad de la seleccion de algoritmos, ataques de temporizado o validacion de tokens.

En el proximo articulo, cubrimos la limitacion de tasa y las cabeceras de seguridad automaticas -- las protecciones invisibles que endurecen cada aplicacion FLIN contra el abuso y los ataques web comunes.


Esta es la Parte 108 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: - [107] Hashing de contrasenas con Argon2 integrado en FLIN - [108] Autenticacion JWT en 3 lineas de FLIN (estas aqui) - [109] Limitacion de tasa y cabeceras de seguridad - [110] Autenticacion de dos factores (TOTP)

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude deblo

El prompt decía que lo anunciara. El modelo no lo hizo. Así que lo hizo el worker: cerrar un silencio de 15 segundos en Déblo Voice

Déblo Voice se quedaba en silencio unos 15 segundos cada vez que una pregunta difícil activaba una búsqueda profunda. El prompt ya exigía un anuncio, pero el modelo de audio nativo emite la llamada a la función como su primer token. Además, el silencio tenía dos partes, la ejecución de la herramienta y la latencia de la respuesta, así que ningún prompt podía cerrarlo. Llevamos la garantía al worker de LiveKit: un anuncio pregrabado con la voz y el idioma de la sesión, y un suave sonido de tecleo mientras el agente piensa. Probado en producción el 16 de octubre de 2026: 14 y 17,5 segundos de silencio del lado del modelo, unos 0,4 segundos percibidos por el usuario.

13 min Oct 16, 2026
deblodeblo-voicevoice-aigemini-live +17
Thales & Claude zerosuite

El servidor que no dimos de baja: vaciar una máquina de producción en una noche, y cada número que comprobamos

Una noche, seis aplicaciones migradas fila por fila, tres copias de seguridad vacías, una sin destino y un servidor cuyo futuro cambió tres veces.

19 min Oct 2, 2026
caspclaude-codeclaude-opus-5.5infrastructure +10
Thales & Claude zerosuite

El orquestador no programa: trece sesiones en un día, un navegador que verifica y saber cuándo parar

Un día, una plataforma privada de un cliente, trece sesiones headless de Claude encadenadas por un orquestador que nunca escribe código. Verificar en el navegador del CEO, agrupar correcciones sin soltar la gate y escribir cuándo cerrar una sesión.

19 min Sep 30, 2026
caspclaude-codeclaude-opus-5.5multi-session +12