JWT Authentication for Frontend Developers

This guide explains how to generate and use JWTs for authentication with the backend API. The system uses RSA-based JWT tokens with RS256 signing. The steps outlined here will be done on the server, not the frontend. When you see references to any frontend JWTs, this is the expected flow:
  1. Your frontend requests a token from your JWT service
  2. Your JWT service performs the steps outlined below to generate a working JWT
  3. The JWT token is taken and attached to a request and sent to Ampersand
DO NOT DO JWT SIGNING LOGIC ON THE FRONTEND. IT’S INSECURE AND INVALIDATES THE SECURITY MODEL.

Overview

The JWT system consists of:
  1. RSA Key Pair Generation - Creating public/private key pairs
  2. Key Registration - Registering the public key with the backend
  3. JWT Token Generation - Creating signed JWT tokens using the private key
  4. API Authentication - Using JWTs to authenticate API requests

1. Generate RSA Key Pair

First, generate an RSA key pair for signing JWTs:
const crypto = require('crypto');
const fs = require('fs');

// Generate RSA key pair (2048-bit recommended)
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs1',
    format: 'pem'
  }
});

// Save keys to files
fs.writeFileSync('private_key.pem', privateKey);
fs.writeFileSync('public_key.pem', publicKey);

console.log('RSA key pair generated successfully!');

2. Register Public Key with Backend

Once you have the public key, register it with the backend API:
const fs = require('fs');

async function registerJWTKey(projectId, label) {
  const publicKeyPem = fs.readFileSync('public_key.pem', 'utf8');
  
  const requestBody = {
    label: label,                    // Descriptive label for the key
    algorithm: 'RS256',              // Only RS256 is supported
    publicKeyPem: publicKeyPem       // PEM-formatted public key
  };

  const response = await fetch(`https://api.withampersand.com/v1/projects/${projectId}/jwt-keys`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer YOUR_API_TOKEN'  // Your regular API token
    },
    body: JSON.stringify(requestBody)
  });

  if (!response.ok) {
    throw new Error(`Failed to register key: ${response.status}`);
  }

  const result = await response.json();
  console.log('Key registered with ID:', result.kid);
  return result.kid; // This is your Key ID for JWT signing
}

// Usage
const keyId = await registerJWTKey('your-project-id', 'My Frontend App Public Key');

3. Generate JWT Tokens

Create JWT tokens using the private key and registered key ID:
const jwt = require('jsonwebtoken');
const fs = require('fs');

function generateJWT(keyId, claims, ttlSeconds = 600) {
  const privateKey = fs.readFileSync('private_key.pem', 'utf8');
  
  // JWT payload with required claims
  const payload = {
    sub: claims.subject,           // Required: Subject (user/service ID)
    groupRef: claims.groupRef,     // Required: Group reference
    consumerRef: claims.consumerRef, // Required: Consumer reference
    scope: claims.scopes || [],    // Optional: Array of scopes
    iat: Math.floor(Date.now() / 1000),           // Issued at time
    exp: Math.floor(Date.now() / 1000) + ttlSeconds // Expiration time (max 600 seconds)
  };

  // Sign the token
  const token = jwt.sign(payload, privateKey, {
    algorithm: 'RS256',
    header: {
      kid: keyId  // Key ID from registration
    }
  });

  return token;
}

// Usage example
const jwtToken = generateJWT('your-key-id', {
  subject: 'user@example.com',
  groupRef: 'group-123',
  consumerRef: 'consumer-456',
  scopes: ['read', 'write']
}, 300); // 5 minute expiration

console.log('Generated JWT:', jwtToken);

4. Using JWTs for API Authentication

Use the generated JWT token to authenticate API requests:
async function makeAuthenticatedRequest(jwtToken, endpoint) {
  const response = await fetch(endpoint, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${jwtToken}`,
      'Content-Type': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`API request failed: ${response.status}`);
  }

  return response.json();
}

// Usage
const data = await makeAuthenticatedRequest(jwtToken, 'https://api.withampersand.com/v1/projects');

Complete Example

Here’s a complete example that ties everything together:
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const fs = require('fs');

class JWTManager {
  constructor(projectId, keyLabel) {
    this.projectId = projectId;
    this.keyLabel = keyLabel;
    this.keyId = null;
    this.privateKey = null;
    this.publicKey = null;
  }

  // Step 1: Generate RSA key pair
  generateKeyPair() {
    const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
      modulusLength: 2048,
      publicKeyEncoding: { type: 'spki', format: 'pem' },
      privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
    });

    this.publicKey = publicKey;
    this.privateKey = privateKey;

    // Save to files
    fs.writeFileSync('private_key.pem', privateKey);
    fs.writeFileSync('public_key.pem', publicKey);
  }

  // Step 2: Register public key with backend
  async registerKey(apiToken) {
    const response = await fetch(`https://api.withampersand.com/v1/projects/${this.projectId}/jwt-keys`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiToken}`
      },
      body: JSON.stringify({
        label: this.keyLabel,
        algorithm: 'RS256',
        publicKeyPem: this.publicKey
      })
    });

    if (!response.ok) {
      throw new Error(`Failed to register key: ${response.status}`);
    }

    const result = await response.json();
    this.keyId = result.kid;
    return this.keyId;
  }

  // Step 3: Generate JWT token
  generateToken(claims, ttlSeconds = 600) {
    if (!this.keyId || !this.privateKey) {
      throw new Error('Key not registered or private key not loaded');
    }

    // Validate TTL (max 10 minutes per backend requirements)
    if (ttlSeconds > 600) {
      throw new Error('TTL cannot exceed 600 seconds (10 minutes)');
    }

    const now = Math.floor(Date.now() / 1000);
    const payload = {
      sub: claims.subject,
      groupRef: claims.groupRef,
      consumerRef: claims.consumerRef,
      scope: claims.scopes || [],
      iat: now,
      exp: now + ttlSeconds
    };

    return jwt.sign(payload, this.privateKey, {
      algorithm: 'RS256',
      header: { kid: this.keyId }
    });
  }

  // Load existing keys from files
  loadKeys() {
    if (fs.existsSync('private_key.pem')) {
      this.privateKey = fs.readFileSync('private_key.pem', 'utf8');
    }
    if (fs.existsSync('public_key.pem')) {
      this.publicKey = fs.readFileSync('public_key.pem', 'utf8');
    }
  }
}

// Usage example
async function main() {
  const manager = new JWTManager('your-project-id', 'Frontend App');
  
  // Generate new key pair (do this once)
  manager.generateKeyPair();
  
  // Register with backend (do this once per key)
  const keyId = await manager.registerKey('your-api-token');
  console.log('Registered key ID:', keyId);

  // Generate JWT tokens (do this for each request)
  const token = manager.generateToken({
    subject: 'user@example.com',
    groupRef: 'group-123',
    consumerRef: 'consumer-456',
    scopes: ['read', 'write']
  }, 300);
  
  console.log('Generated JWT:', token);
  
  // Use token for API requests
  const response = await fetch('https://api.withampersand.com/v1/projects', {
    headers: { 'Authorization': `Bearer ${token}` }
  });
}

Key Management API Endpoints

The backend provides these endpoints for managing JWT keys:
  • POST /projects/:projectId/jwt-keys - Register a new public key
  • GET /projects/:projectId/jwt-keys - List all keys for a project
  • GET /projects/:projectId/jwt-keys/:keyId - Get a specific key
  • PATCH /projects/:projectId/jwt-keys/:keyId - Update key (label/active status)
  • DELETE /projects/:projectId/jwt-keys/:keyId - Delete a key

JWT Token Requirements

Your JWT tokens must include these claims:
  • sub (subject) - Required: User or service identifier
  • groupRef - Required: Group reference for authorization
  • consumerRef - Required: Consumer reference
  • iat (issued at) - Required: Token issue time (Unix timestamp)
  • exp (expires at) - Required: Token expiration (max 600 seconds from iat)
  • scope - Optional: Array of permission scopes
  • kid (key ID) - Required in header: The key ID from registration (will be a UUID)

Important Security Notes

  1. Private Key Security: Never expose private keys in client-side code. Store them securely on your backend servers.
  2. Token Expiration: Tokens have a maximum TTL of 10 minutes (600 seconds). Plan for token refresh in your application.
  3. Key Rotation: Regularly rotate your RSA key pairs for better security. Right now this is a manual process which will require you to invoke the API.
  4. Scope Management: Use the scope claim to limit token permissions appropriately.
  5. Algorithm Restriction: Only RS256 algorithm is supported by the backend.