Blog

Luis Majano

May 22, 2026

Spread the word


Share your thoughts

JWT authentication is everywhere. But rolling it correctly — with proper algorithm enforcement, key management, clock skew handling, JWE encryption, and zero security footguns — is anything but trivial. Today, we're shipping bx-jwt, a production-ready JWT/JWE module for BoxLang that handles all of it out of the box, so you can focus on building, not fighting cryptography.

bx-jwt is part of the BoxLang+ and BoxLang++ subscription tiers — our enterprise-grade module collection built for teams that take security seriously.


What is bx-jwt?

bx-jwt is a full implementation of the JWT/JWE specification stack for BoxLang:

  • JWS (JSON Web Signature) — HMAC, RSA, and Elliptic Curve signing
  • JWE (JSON Web Encryption) — RSA and symmetric encryption
  • RFC 7518 — JSON Web Algorithms
  • RFC 7519 — JSON Web Token

It ships with two APIs that serve different tastes: a fluent builder for expressive, chainable token construction, and a suite of BIF functions for direct, functional-style usage. Both share the same engine, key registry, and security model.


Two APIs, One Module

The Fluent Builder — jwtNew()

When readability matters, the fluent builder gives you a clean, chainable surface for token construction. Call jwtNew() and chain your claims. Terminate with .sign() or .encrypt().

token = jwtNew()
    .subject( "user-123" )
    .issuer( "auth-service" )
    .audience( "mobile-client" )
    .claim( "roles", [ "admin", "user" ] )
    .expireIn( 3600 )
    .header( "kid", "v1" )
    .sign( secret, "HS256" );

Every standard claim has a named method. Custom claims go through .claim( key, val ). Headers via .header( key, val ). Swap .sign() for .encrypt() and you have a JWE. It reads like what it does. 🎯

The BIF Functions

For teams that prefer a direct, functional style, all operations are available as first-class BoxLang BIFs:

BIFPurpose
jwtCreate()Sign a payload struct into a compact JWS token
jwtVerify()Verify signature and validate claims — throws on failure
jwtValidate()Like jwtVerify() but returns true/false
jwtDecode()Inspect header/payload without signature verification
jwtRefresh()Re-issue a token with fresh iat, jti, and optional new exp
jwtEncrypt()Encrypt a payload as a compact JWE token
jwtDecrypt()Decrypt a JWE token and return claims
jwtGenerateSecret()Cryptographically random HMAC secret (Base64-encoded)
jwtGenerateKeyPair()RSA or EC key pair as PEM strings

Get Started in Seconds

HMAC Sign and Verify

secret  = jwtGenerateSecret( 256 );
token   = jwtCreate( { sub: "user-123", iss: "my-api", roles: [ "admin" ] }, secret, "HS256" );
payload = jwtVerify( token, secret, "HS256" );
writeOutput( payload.sub ); // user-123

RSA Sign and Verify

keys    = jwtGenerateKeyPair( "RS256" );
token   = jwtCreate( { sub: "user-123" }, keys.privateKey, "RS256" );
payload = jwtVerify( token, keys.publicKey, "RS256" );

JWE Encryption

Sensitive payloads — PII, PHI, internal claims that must stay opaque — belong in a JWE, not a JWS. bx-jwt handles both:

token   = jwtEncrypt(
    { sub: "patient-456", phi: { dob: "1990-01-15" } },
    secret32bytes,
    { keyAlgorithm: "dir", encAlgorithm: "A256GCM" }
);
payload = jwtDecrypt( token, secret32bytes, { keyAlgorithm: "dir", encAlgorithm: "A256GCM" } );

Or nest them — sign first, encrypt the signed token — for the full sign-then-encrypt pattern:

// Inner signed JWT
signedToken = jwtCreate( { sub: "u1", role: "admin" }, innerPrivKey, "RS256", {
    headers: { cty: "JWT" }
} );

// Outer encrypted JWE
encryptedToken = jwtEncrypt( signedToken, outerPubKey, {
    keyAlgorithm : "RSA-OAEP-256",
    encAlgorithm : "A256GCM"
} );

Enterprise Key Management with the Key Registry

This is where bx-jwt separates from basic JWT libraries. The Key Registry lets you define named keys once in configuration and reference them by name throughout your entire application. Keys never appear in application logic. Rotation is a config change, not a code change.

// ModuleConfig.bx
settings = {
    keys: {
        "api-signing": {
            algorithm : "HS256",
            secret    : "${Setting: env.JWT_HMAC_SECRET not found}"   // env var substitution built-in
        },
        "api-rsa": {
            algorithm  : "RS256",
            privateKey : "/etc/keys/api-private.pem",
            publicKey  : "/etc/keys/api-public.pem"
        },
        "partner-public": {
            algorithm : "RS256",
            publicKey : "/etc/keys/partner-public.pem"  // verify-only key
        }
    },
    defaultSigningKey : "api-signing",
    defaultVerifyKey  : "api-signing",
    defaultAlgorithm  : "HS256",
    defaultIssuer     : "my-api",
    defaultAudience   : "web",
    defaultExpiration : 3600,
    generateIat       : true,
    generateJti       : true
}

With defaults fully configured, the key and algorithm arguments become optional everywhere:

// No key argument, no algorithm argument — resolved from registry
token   = jwtCreate( { sub: "user-123" } );
payload = jwtVerify( token );

Keys can also be registered at runtime via the JWTService:

jwtService = getBoxContext().getRuntime().getGlobalService( "JWTService" );
jwtService.registerKey( "session-key", { algorithm: "HS256", secret: generateSecureKey() } );

Security by Default — Not by Configuration 🛡️

bx-jwt is built with the attack surface in mind. Security properties are unconditional — they cannot be turned off:

alg:none Rejection

The classic JWT attack. bx-jwt unconditionally rejects tokens with alg:none. Passing an unsigned token to jwtVerify() or jwtRefresh() always throws JWTVerificationException. No configuration switch, no override. It simply doesn't work.

HMAC Minimum Key Lengths (RFC 7518 §3.2)

Short HMAC secrets are a real-world vulnerability. bx-jwt enforces RFC 7518 minimums:

AlgorithmMinimum Key Length
HS25632 bytes (256 bits)
HS38448 bytes (384 bits)
HS51264 bytes (512 bits)

Use jwtGenerateSecret( bits ) and you're always compliant.

Algorithm Allowlist

Algorithm-confusion attacks exploit servers that accept any algorithm the token header declares. Lock your application to a known set:

// Only HS256 and RS256 are accepted — anything else throws
allowedAlgorithms: [ "HS256", "RS256" ]

Clock Skew Tolerance

Distributed systems have clock drift. bx-jwt ships with a configurable clockSkew (default: 60 seconds) that prevents legitimate tokens from failing exp/nbf validation due to minor time differences between services. Tune it per environment:

// Strict environment
payload = jwtVerify( token, secret, "HS256", { clockSkew: 0 } );

// Distributed system with known drift
payload = jwtVerify( token, secret, "HS256", { clockSkew: 120 } );

Real-World Patterns

Authentication Middleware

function requireAuth() {
    var authHeader = getHttpRequestData().headers[ "Authorization" ] ?: ""
    if ( !authHeader.startsWith( "Bearer " ) ) {
	    bx:header statusCode=401;
        abort;
    }

    var token = authHeader.removeFirst( "Bearer " )

    if ( !jwtValidate( token, application.jwtSecret, "HS256" ) ) {
        bx:header statusCode=401;
        abort;
    }

    request.currentUser = jwtVerify( token, application.jwtSecret, "HS256", {
        claims: { iss: "auth-service", aud: "api" }
    } );
}

Token Refresh with Grace Period

function refreshToken( token ) {
    try {
        return jwtRefresh( token, application.jwtSecret, "HS256", {
            allowExpired : true,   // honor recently expired tokens
            expireIn     : 3600,
            claims       : { iss: "auth-service" }
        } );
    } catch ( "bxjwt.JWTVerificationException" e ) {
        // Bad signature — not refreshable
        return "";
    }
}

Kid-Based Key Rotation

function verifyWithKeyRotation( token ) {
    var decoded = jwtDecode( token );
    var kid     = decoded.header.kid ?: "default";
    var key     = getKeyForKid( kid );
    return jwtVerify( token, key, decoded.header.alg );
}

Full Algorithm Support

Signing (JWS)

AlgorithmTypeNotes
HS256, HS384, HS512HMACSymmetric
RS256, RS384, RS512RSAAsymmetric — private signs, public verifies
ES256, ES384, ES512Elliptic CurveSmaller keys than RSA, equivalent security

Encryption (JWE)

Key AlgorithmContent EncryptionKey Type
RSA-OAEP-256A256GCMRSA key pair
dirA256GCM256-bit symmetric secret

Installation

# CommandBox
box install bx-jwt

# BoxLang CLI
install-bx-module bx-jwt

bx-jwt requires a BoxLang+ or BoxLang++ subscription. 🔑

This module ships as part of our enterprise module collection — a growing library of production-ready, security-focused, professionally maintained modules available exclusively to BoxLang+ subscribers.


BoxLang+/++/Starter

bx-jwt is one of many enterprise modules available under BoxLang+/++/Starter. When you subscribe, you get:

  • 🔐 bx-jwt and the full enterprise module library
  • ⚡ Priority support from the Ortus team
  • 🏗️ Access to upcoming enterprise modules as they ship
  • ❤️ You fund the continued development of BoxLang as a community-supported open source project

View Plans & Subscribe → boxlang.io/plans


Resources


JSON Web Tokens are a solved problem. Now BoxLang has the enterprise solution to prove it. Install bx-jwt, protect your applications, and ship with confidence. 🚀

Add Your Comment

Recent Entries

What “Modernize or Die” Really Means in 2026

What “Modernize or Die” Really Means in 2026

“Modernize or Die” is not about forcing teams into MVC, chasing trends, or rewriting every CFML application from scratch. It means making sure your applications, teams, and processes can survive the future: easier to maintain, test, secure, deploy, document, hire for, and evolve. In 2026, modernization is less about adopting the newest pattern and more about reducing business risk, protecting the value already built into your systems, and ensuring CFML applications remain credible, sustai...

Cristobal Escobar
Cristobal Escobar
May 22, 2026
Into the Box 2026: Day 2 Keynote Highlights 🎉

Into the Box 2026: Day 2 Keynote Highlights 🎉

🎤 ITB 2026: Keynote Day 2 Recap!

Day 2 of Into the Box 2026 shifted the spotlight from platform innovation to real-world impact showing how organizations are using the Ortus Solutions ecosystem to modernize massive systems, scale businesses, and embrace AI-driven development.

From enterprise mig...

Victor Campos
Victor Campos
May 14, 2026