🎉 RamAPI v1.0 is now available! Read the Getting Started Guide
Documentation
Authentication
JWT Authentication

JWT Authentication

JSON Web Tokens (JWT) provide stateless authentication for your API. This guide covers the JWTService class, token generation, verification, and authentication middleware.

Table of Contents

  1. Overview
  2. JWTService Class
  3. Token Generation
  4. Token Verification
  5. Authentication Middleware
  6. Configuration
  7. Security Best Practices
  8. Troubleshooting

Overview

RamAPI uses the jsonwebtoken library for JWT operations, providing a simple and secure API for authentication.

Installation

JWT support is built-in, but you need to install the peer dependency:

npm install jsonwebtoken
npm install -D @types/jsonwebtoken

Quick Start

import { createApp, JWTService, authenticate } from 'ramapi';
 
// Create JWT service
const jwtService = new JWTService({
  secret: process.env.JWT_SECRET!,
  expiresIn: 86400, // 24 hours
});
 
const app = createApp();
 
// Protected route
app.get('/api/profile',
  authenticate(jwtService),
  async (ctx) => {
    ctx.json({ user: ctx.user });
  }
);
 
app.listen(3000);

JWTService Class

Constructor

class JWTService {
  constructor(config: JWTConfig);
}
 
interface JWTConfig {
  secret: string;              // Required: Secret key for signing
  expiresIn?: number;          // Optional: Expiration time in seconds
  algorithm?: jwt.Algorithm;   // Optional: Signing algorithm (default: 'HS256')
  issuer?: string;             // Optional: Token issuer
  audience?: string;           // Optional: Token audience
}

Create Instance

import { JWTService } from 'ramapi';
 
const jwtService = new JWTService({
  secret: process.env.JWT_SECRET!,
  expiresIn: 86400, // 24 hours in seconds
});

Methods

sign(payload: JWTPayload): string

Signs a JWT token with the provided payload.

interface JWTPayload {
  sub: string;  // Subject (user ID) - required
  [key: string]: unknown; // Additional claims
}

verify(token: string): JWTPayload

Verifies and decodes a JWT token. Throws HTTPError on invalid/expired tokens.

decode(token: string): JWTPayload | null

Decodes a token without verification. Use with caution.


Token Generation

Basic Token

const token = jwtService.sign({
  sub: 'user-123', // User ID (required)
});
 
console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Token with Custom Claims

const token = jwtService.sign({
  sub: 'user-123',
  email: 'user@example.com',
  role: 'admin',
  permissions: ['read', 'write', 'delete'],
});

Login Endpoint Example

import { validate } from 'ramapi';
import { z } from 'zod';
 
const loginSchema = {
  body: z.object({
    email: z.string().email(),
    password: z.string().min(8),
  }),
};
 
app.post('/auth/login',
  validate(loginSchema),
  async (ctx) => {
    const { email, password } = ctx.body;
 
    // Verify credentials (pseudo-code)
    const user = await db.users.findByEmail(email);
    if (!user || !(await passwordService.verify(password, user.passwordHash))) {
      throw new HTTPError(401, 'Invalid credentials');
    }
 
    // Generate token
    const token = jwtService.sign({
      sub: user.id,
      email: user.email,
      role: user.role,
    });
 
    ctx.json({ token });
  }
);

Token Verification

Manual Verification

app.get('/api/verify', async (ctx) => {
  const authHeader = ctx.headers.authorization as string;
 
  if (!authHeader) {
    throw new HTTPError(401, 'No token provided');
  }
 
  const token = authHeader.split(' ')[1]; // Extract from "Bearer \<token\>"
 
  try {
    const payload = jwtService.verify(token);
    ctx.json({
      valid: true,
      payload,
    });
  } catch (error) {
    ctx.json({
      valid: false,
      error: (error as Error).message,
    }, 401);
  }
});

Decode Without Verification

Use this to inspect token contents without validating the signature:

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const payload = jwtService.decode(token);
 
console.log(payload);
// { sub: 'user-123', email: 'user@example.com', ... }

Warning: Never use decoded tokens for authentication without verification!


Authentication Middleware

authenticate()

Requires a valid JWT token. Throws 401 if missing or invalid.

import { authenticate } from 'ramapi';
 
app.get('/api/profile',
  authenticate(jwtService),
  async (ctx) => {
    // ctx.user is populated with token payload
    console.log(ctx.user);
    // { sub: 'user-123', email: 'user@example.com', ... }
 
    // ctx.state.userId contains the user ID
    console.log(ctx.state.userId); // 'user-123'
 
    ctx.json({ user: ctx.user });
  }
);

optionalAuthenticate()

Authenticates if token is present, but doesn't fail if missing.

import { optionalAuthenticate } from 'ramapi';
 
app.get('/api/posts',
  optionalAuthenticate(jwtService),
  async (ctx) => {
    if (ctx.user) {
      // User is authenticated - show private posts
      const posts = await db.posts.findByUserId(ctx.user.sub);
      ctx.json({ posts, authenticated: true });
    } else {
      // No authentication - show public posts only
      const posts = await db.posts.findPublic();
      ctx.json({ posts, authenticated: false });
    }
  }
);

Authorization Header Format

The middleware expects tokens in the Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Error Handling

The middleware throws HTTPError with appropriate status codes:

  • 401: Authorization header missing
  • 401: Invalid authorization header format
  • 401: Token expired
  • 401: Invalid token signature
  • 401: Other authentication failures

Configuration

Basic Configuration

const jwtService = new JWTService({
  secret: process.env.JWT_SECRET!,
  expiresIn: 86400, // 24 hours
});

Full Configuration

const jwtService = new JWTService({
  // Required: Secret key for signing tokens
  secret: process.env.JWT_SECRET!,
 
  // Optional: Token expiration in seconds
  expiresIn: 86400, // 24 hours
 
  // Optional: Signing algorithm
  algorithm: 'HS256', // Default, also: HS384, HS512, RS256, etc.
 
  // Optional: Token issuer (your API name)
  issuer: 'my-api',
 
  // Optional: Token audience (your app name)
  audience: 'my-app',
});

Algorithm Options

AlgorithmTypeSecuritySpeed
HS256HMACGoodFast
HS384HMACBetterFast
HS512HMACBestFast
RS256RSABestSlower
RS384RSABestSlower
RS512RSABestSlower

Recommendation: Use HS256 for most applications, RS256 for microservices.

Environment Variables

# .env
JWT_SECRET=your-super-secret-key-at-least-32-characters-long
JWT_EXPIRES_IN=86400
JWT_ISSUER=my-api
JWT_AUDIENCE=my-app
import { config as loadEnv } from 'dotenv';
loadEnv();
 
const jwtService = new JWTService({
  secret: process.env.JWT_SECRET!,
  expiresIn: parseInt(process.env.JWT_EXPIRES_IN || '86400'),
  issuer: process.env.JWT_ISSUER,
  audience: process.env.JWT_AUDIENCE,
});

Token Expiration

Common expiration times:

// 15 minutes
expiresIn: 900
 
// 1 hour
expiresIn: 3600
 
// 24 hours
expiresIn: 86400
 
// 7 days
expiresIn: 604800
 
// 30 days
expiresIn: 2592000

Security Best Practices

1. Strong Secret Keys

// BAD - Too short
secret: 'secret123'
 
// GOOD - Long, random, secure
secret: crypto.randomBytes(64).toString('hex')

Generate a secure secret:

node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

2. Short Expiration Times

// BAD - Token valid for 1 year
expiresIn: 31536000
 
// GOOD - Token valid for 24 hours
expiresIn: 86400
 
// BETTER - Short-lived with refresh tokens
expiresIn: 900 // 15 minutes

3. Use HTTPS

JWT tokens should only be transmitted over HTTPS to prevent interception.

4. Store Secrets Securely

// BAD - Hardcoded secret
const jwtService = new JWTService({
  secret: 'my-secret-key',
});
 
// GOOD - From environment variable
const jwtService = new JWTService({
  secret: process.env.JWT_SECRET!,
});

5. Validate Token Claims

app.get('/admin',
  authenticate(jwtService),
  async (ctx) => {
    // Verify role claim
    if (ctx.user.role !== 'admin') {
      throw new HTTPError(403, 'Forbidden: Admin access required');
    }
 
    ctx.json({ data: 'Admin data' });
  }
);

6. Don't Store Sensitive Data in Tokens

// BAD - Storing sensitive data
const token = jwtService.sign({
  sub: user.id,
  password: user.password, // NEVER!
  ssn: user.ssn, // NEVER!
});
 
// GOOD - Only non-sensitive identifiers
const token = jwtService.sign({
  sub: user.id,
  email: user.email,
  role: user.role,
});

7. Implement Token Revocation

JWTs are stateless, so implement a blacklist for revoked tokens:

const revokedTokens = new Set\<string\>();
 
function revokeToken(token: string) {
  revokedTokens.add(token);
}
 
function customAuthenticate(jwtService: JWTService): Middleware {
  return async (ctx, next) => {
    const authHeader = ctx.headers.authorization as string;
    if (!authHeader) {
      throw new HTTPError(401, 'Authorization header missing');
    }
 
    const token = authHeader.split(' ')[1];
 
    // Check if token is revoked
    if (revokedTokens.has(token)) {
      throw new HTTPError(401, 'Token has been revoked');
    }
 
    const payload = jwtService.verify(token);
    ctx.user = payload;
    await next();
  };
}

8. Use Refresh Tokens

Implement refresh tokens for long-lived sessions:

// Short-lived access token
const accessToken = jwtService.sign({
  sub: user.id,
  type: 'access',
}, { expiresIn: 900 }); // 15 minutes
 
// Long-lived refresh token (stored securely)
const refreshToken = jwtService.sign({
  sub: user.id,
  type: 'refresh',
}, { expiresIn: 604800 }); // 7 days

Troubleshooting

Problem: "Authorization header missing"

Cause: No Authorization header in request.

Solution: Include Authorization header with Bearer token:

fetch('/api/profile', {
  headers: {
    'Authorization': `Bearer ${token}`,
  },
});

Problem: "Invalid authorization header format"

Cause: Authorization header not in "Bearer <token>" format.

Solution: Ensure correct format:

// WRONG
Authorization: eyJhbGci...
 
// CORRECT
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Problem: "Token expired"

Cause: Token's expiration time has passed.

Solution: Generate a new token or implement refresh token flow.

Problem: "Invalid token"

Cause: Token signature doesn't match or token is malformed.

Solution: Ensure the same secret is used for signing and verification.

Problem: Token works locally but not in production

Cause: Different JWT_SECRET in environments.

Solution: Ensure JWT_SECRET is set correctly in production environment.


Complete Example

import { createApp, JWTService, authenticate, optionalAuthenticate } from 'ramapi';
import { passwordService } from 'ramapi';
 
// Setup JWT
const jwtService = new JWTService({
  secret: process.env.JWT_SECRET!,
  expiresIn: 86400, // 24 hours
  issuer: 'my-api',
  audience: 'my-app',
});
 
const app = createApp();
 
// Login endpoint
app.post('/auth/login', async (ctx) => {
  const { email, password } = ctx.body;
 
  // Verify credentials
  const user = await db.users.findByEmail(email);
  if (!user || !(await passwordService.verify(password, user.passwordHash))) {
    throw new HTTPError(401, 'Invalid credentials');
  }
 
  // Generate token
  const token = jwtService.sign({
    sub: user.id,
    email: user.email,
    role: user.role,
  });
 
  ctx.json({ token, user: { id: user.id, email: user.email } });
});
 
// Public endpoint (optional auth)
app.get('/api/posts',
  optionalAuthenticate(jwtService),
  async (ctx) => {
    const posts = ctx.user
      ? await db.posts.findAll()
      : await db.posts.findPublic();
 
    ctx.json({ posts });
  }
);
 
// Protected endpoint
app.get('/api/profile',
  authenticate(jwtService),
  async (ctx) => {
    const user = await db.users.findById(ctx.user.sub);
    ctx.json({ user });
  }
);
 
// Admin-only endpoint
app.get('/api/admin',
  authenticate(jwtService),
  async (ctx) => {
    if (ctx.user.role !== 'admin') {
      throw new HTTPError(403, 'Admin access required');
    }
 
    ctx.json({ data: 'Admin data' });
  }
);
 
app.listen(3000);

Next Steps


Need help? Check the Troubleshooting Guide or GitHub Issues (opens in a new tab).