Secure Data Exchange with JWTs and the @cross/jwt Library

by Pinta

3 min read

JSON Web Tokens (JWTs) provide a secure and standardized way to transmit information between parties. They are widely used for authentication, but their ability to carry arbitrary data makes them valuable for a variety of secure data exchange scenarios. If you work with Deno, Bun, or Node.js, managing JWT workflows across these different runtimes can introduce inconsistencies and potential security risks. Lets take a look at this runtime agnostic library to enable one dependency across all major runtimes.

@cross/jwt

Introducing @cross/jwt, a cross-platform JWT library designed to bring secure, consistent, and easy-to-use JWT handling to your projects. Source code available at GitHub.

Key Features for Robust Security

  • Strong Cryptography: @cross/jwt supports a range of industry-standard signing algorithms, including HMAC, RSA, RSA-PSS, and ECDSA, ensuring the integrity and authenticity of your tokens.
  • Cross-Platform Confidence: Your security practices don't need to change between Deno, Bun, and Node.js environments. @cross/jwt provides the same robust functionality everywhere.
  • Intuitive API: Security shouldn't be complex. The library's clear functions for signing, verifying, and managing keys make it easy to integrate JWTs correctly.
  • Flexible Options: Customize token behavior, such as expiration and validation rules, using JWTOptions.

Installation

# For Deno
deno add @cross/jwt

# For Bun
bunx jsr add @cross/jwt

# For Node.js
npx jsr add @cross/jwt

API at a glance

See detailed documentation on https://jsr.io for the complete API, but here's a quick look with a simple HMAC Example:

import { signJWT, validateJWT } from "@cross/jwt";

// A base64-encoded secret.
const secret = "y69uNvF9lbHE2disEqeYCBYOUmzJvr75txhxbUL5W0k=";
// Generate and sign the JWT.
const token = await signJWT({ userId: 123 }, secret);
// Verify and validate it.
const data = await validateJWT(token, secret);

Real-World Use Cases

  1. Secure API Authentication: Implement bearer token authentication in your REST APIs, ensuring that only requests with valid JWTs are authorized.
  2. Microservice Communication: Use JWTs to securely pass context and authorization data between microservices, especially across different runtimes.
  3. Distributed Data Sharing: Transmit sensitive data (e.g., configuration, limited-access resources) between applications in a secure and verifiable format.

Oak example (Simplified)

import { Application } from "jsr:@oak/oak/application";
import { signJWT, validateJWT } from "@cross/jwt";
import type { JWTPayload } from "@cross/jwt";

const app = new Application();
// Replace with a secure secret
const secret = "y69uNvF9lbHE2disEqeYCBYOUmzJvr75txhxbUL5W0k=";

// ... Your routes and other logic ...

1. Login Route

app.use(async (ctx, next) => {
  if (ctx.request.url.pathname === '/login' && ctx.request.method === 'POST') {
     // ... your login validation logic ...
     
     const payload: JWTPayload = {
       userId: 123, 
       role: "user" 
     };
     const jwt = await signJWT(payload, secret);
     ctx.response.body = { token: jwt };
   } else {
     await next();
   }
});

2. Authentication Middleware

const authMiddleware = async (ctx: any, next: any) => {
  const authHeader = ctx.request.headers.get('Authorization');
  if (!authHeader) {
    ctx.response.status = 401;  // Unauthorized
    return; 
  }

  const token = authHeader.split(' ')[1];  // Assuming 'Bearer token' format
  try {
    const payload = await validateJWT(token, secret) as JWTPayload; 

    // Attach user data to the context for downstream routes/logic
    ctx.state.user = { 
      id: payload.userId,
      role: payload.role
    }; 

    await next();
  } catch (error) {
    ctx.response.status = 401; // Unauthorized
  }
};

3. Protected Route

app.use(authMiddleware);

app.use((ctx) => {
    if (ctx.request.url.pathname === "/protected") {
        // Access user info
        const userId = ctx.state.user.id;
        const userRole = ctx.state.user.role;

        // ... logic using the authenticated user data ...
    }
});

console.log("Server running on port 8000");
await app.listen({ port: 8000 });

Explanation

  1. Login: Simulates a login process. Upon success, a JWT containing user information is generated.
  2. Middleware: Intercepts requests, extracts the JWT from the 'Authorization' header, and validates it. Unauthorized requests get a 401 response.
  3. Protected Route: Routes using the authMiddleware now require a valid JWT to access.

Important Notes:

  • Store JWTs securely on the client: Typically in cookies (with HttpOnly flag).
  • Secret Management: Use environment variables and proper secret handling.
  • Error Handling: Implement more detailed error responses in production.