API Authentication Methods Compared: OAuth vs JWT vs API Keys

API Authentication Methods Compared OAuth vs JWT vs API Keys

The Problem

Choosing the wrong authentication method for your API creates security vulnerabilities, poor user experience, or unnecessary complexity. OAuth is powerful but overkill for simple integrations; JWT tokens offer flexibility but require careful implementation to avoid security flaws; API keys are simple but lack granular permissions and expire poorly. Most developers pick one method and force it into every use case—using OAuth for machine-to-machine communication (too complex), API keys for user authentication (insecure), or JWT without proper validation (vulnerable). The real challenge is understanding when each method shines: API keys for server-to-server, JWT for stateless user sessions, OAuth for third-party access. This guide provides working implementations of all three methods in a single codebase, benchmarks their performance, explains security tradeoffs, and gives decision frameworks so you can confidently choose—and implement—the right authentication for your specific use case.

Tech Stack & Prerequisites

  • Node.js v20+ and npm/pnpm
  • Express.js v4.18+ for API server
  • TypeScript 5+
  • PostgreSQL v15+ for user/key storage
  • Prisma v5+ for database ORM
  • jsonwebtoken v9+ for JWT handling
  • bcrypt v5+ for password hashing
  • crypto (built-in Node.js) for API key generation
  • dotenv for environment variables
  • jose v5+ for modern JWT operations (optional)

Step-by-Step Implementation

Step 1: Setup

Initialize your Node.js project:

bash
mkdir api-auth-comparison
cd api-auth-comparison
npm init -y
npm install express @prisma/client jsonwebtoken bcrypt dotenv cors
npm install -D typescript @types/node @types/express @types/jsonwebtoken @types/bcrypt tsx nodemon prisma

Initialize TypeScript:

bash
npx tsc --init

Update tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Initialize Prisma:

bash
npx prisma init

Create prisma/schema.prisma:

prisma
// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id            String   @id @default(cuid())
  email         String   @unique
  passwordHash  String
  name          String?
  role          Role     @default(USER)
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
  
  apiKeys       ApiKey[]
  oauthTokens   OAuthToken[]
  
  @@index([email])
}

enum Role {
  USER
  ADMIN
  SERVICE
}

model ApiKey {
  id          String   @id @default(cuid())
  userId      String
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  keyHash     String   @unique
  name        String
  prefix      String   // First 8 chars for identification
  permissions Json?    // Scoped permissions
  lastUsedAt  DateTime?
  expiresAt   DateTime?
  createdAt   DateTime @default(now())
  
  @@index([userId])
  @@index([keyHash])
}

model OAuthToken {
  id            String   @id @default(cuid())
  userId        String
  user          User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  accessToken   String   @unique
  refreshToken  String?  @unique
  scope         String?
  expiresAt     DateTime
  createdAt     DateTime @default(now())
  
  @@index([userId])
  @@index([accessToken])
}

Run migrations:

bash
npx prisma migrate dev --name init
npx prisma generate

Create project structure:

bash
mkdir -p src/middleware src/routes src/services src/utils
touch src/index.ts src/config.ts src/middleware/auth.ts src/routes/api.ts src/services/auth.service.ts

Step 2: Configuration

Create .env file:

bash
# .env
PORT=3000
NODE_ENV=development

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/api_auth_demo

# JWT Configuration
JWT_SECRET=your_super_secret_jwt_key_change_in_production
JWT_EXPIRES_IN=15m
REFRESH_TOKEN_SECRET=your_refresh_token_secret_key
REFRESH_TOKEN_EXPIRES_IN=7d

# API Key Configuration
API_KEY_PREFIX=sk_live_
API_KEY_LENGTH=32

# OAuth Configuration (if using third-party OAuth)
OAUTH_CLIENT_ID=your_oauth_client_id
OAUTH_CLIENT_SECRET=your_oauth_client_secret
OAUTH_REDIRECT_URI=http://localhost:3000/oauth/callback

Add to .gitignore:

bash
echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore
echo "dist/" >> .gitignore

Create src/config.ts:

typescript
// src/config.ts
import dotenv from 'dotenv';

dotenv.config();

export const config = {
  port: process.env.PORT || 3000,
  env: process.env.NODE_ENV || 'development',
  
  database: {
    url: process.env.DATABASE_URL || '',
  },
  
  jwt: {
    secret: process.env.JWT_SECRET || '',
    expiresIn: process.env.JWT_EXPIRES_IN || '15m',
    refreshSecret: process.env.REFRESH_TOKEN_SECRET || '',
    refreshExpiresIn: process.env.REFRESH_TOKEN_EXPIRES_IN || '7d',
  },
  
  apiKey: {
    prefix: process.env.API_KEY_PREFIX || 'sk_live_',
    length: parseInt(process.env.API_KEY_LENGTH || '32'),
  },
  
  oauth: {
    clientId: process.env.OAUTH_CLIENT_ID || '',
    clientSecret: process.env.OAUTH_CLIENT_SECRET || '',
    redirectUri: process.env.OAUTH_REDIRECT_URI || '',
  },
} as const;

// Validate required environment variables
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET', 'REFRESH_TOKEN_SECRET'];

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`);
  }
}

Step 3: Core Logic

3.1: Authentication Service

Create src/services/auth.service.ts:

typescript
// src/services/auth.service.ts
import { PrismaClient, User } from '@prisma/client';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import { config } from '../config';

const prisma = new PrismaClient();

export interface JWTPayload {
  userId: string;
  email: string;
  role: string;
}

export interface TokenPair {
  accessToken: string;
  refreshToken: string;
}

export class AuthService {
  /**
   * Register new user
   */
  async register(email: string, password: string, name?: string): Promise<User> {
    const passwordHash = await bcrypt.hash(password, 10);

    const user = await prisma.user.create({
      data: {
        email,
        passwordHash,
        name,
      },
    });

    return user;
  }

  /**
   * JWT: Generate access and refresh tokens
   */
  generateJWTTokens(user: User): TokenPair {
    const payload: JWTPayload = {
      userId: user.id,
      email: user.email,
      role: user.role,
    };

    const accessToken = jwt.sign(payload, config.jwt.secret, {
      expiresIn: config.jwt.expiresIn,
    });

    const refreshToken = jwt.sign(
      { userId: user.id },
      config.jwt.refreshSecret,
      {
        expiresIn: config.jwt.refreshExpiresIn,
      }
    );

    return { accessToken, refreshToken };
  }

  /**
   * JWT: Verify access token
   */
  verifyJWT(token: string): JWTPayload {
    try {
      return jwt.verify(token, config.jwt.secret) as JWTPayload;
    } catch (error) {
      throw new Error('Invalid or expired token');
    }
  }

  /**
   * JWT: Refresh access token
   */
  async refreshJWT(refreshToken: string): Promise<TokenPair> {
    try {
      const decoded = jwt.verify(refreshToken, config.jwt.refreshSecret) as {
        userId: string;
      };

      const user = await prisma.user.findUnique({
        where: { id: decoded.userId },
      });

      if (!user) {
        throw new Error('User not found');
      }

      return this.generateJWTTokens(user);
    } catch (error) {
      throw new Error('Invalid refresh token');
    }
  }

  /**
   * API Key: Generate new API key
   */
  async generateApiKey(
    userId: string,
    name: string,
    permissions?: Record<string, any>,
    expiresInDays?: number
  ): Promise<{ key: string; prefix: string }> {
    // Generate random key
    const randomKey = crypto.randomBytes(config.apiKey.length).toString('hex');
    const fullKey = `${config.apiKey.prefix}${randomKey}`;

    // Hash the key for storage
    const keyHash = await bcrypt.hash(fullKey, 10);

    // Get prefix for display (first 12 chars)
    const prefix = fullKey.substring(0, 12);

    // Calculate expiration
    const expiresAt = expiresInDays
      ? new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000)
      : null;

    await prisma.apiKey.create({
      data: {
        userId,
        keyHash,
        name,
        prefix,
        permissions: permissions || {},
        expiresAt,
      },
    });

    // Return full key (only time it's shown)
    return { key: fullKey, prefix };
  }

  /**
   * API Key: Validate API key
   */
  async validateApiKey(key: string): Promise<User | null> {
    try {
      // Get all API keys (we need to compare hashes)
      const apiKeys = await prisma.apiKey.findMany({
        include: { user: true },
      });

      // Find matching key
      for (const apiKey of apiKeys) {
        const isMatch = await bcrypt.compare(key, apiKey.keyHash);

        if (isMatch) {
          // Check expiration
          if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
            throw new Error('API key expired');
          }

          // Update last used timestamp
          await prisma.apiKey.update({
            where: { id: apiKey.id },
            data: { lastUsedAt: new Date() },
          });

          return apiKey.user;
        }
      }

      return null;
    } catch (error) {
      console.error('API key validation error:', error);
      return null;
    }
  }

  /**
   * OAuth: Store OAuth tokens
   */
  async storeOAuthToken(
    userId: string,
    accessToken: string,
    refreshToken: string | null,
    scope: string | null,
    expiresIn: number
  ): Promise<void> {
    const expiresAt = new Date(Date.now() + expiresIn * 1000);

    await prisma.oAuthToken.create({
      data: {
        userId,
        accessToken,
        refreshToken,
        scope,
        expiresAt,
      },
    });
  }

  /**
   * OAuth: Validate OAuth token
   */
  async validateOAuthToken(accessToken: string): Promise<User | null> {
    const token = await prisma.oAuthToken.findUnique({
      where: { accessToken },
      include: { user: true },
    });

    if (!token) {
      return null;
    }

    // Check expiration
    if (token.expiresAt < new Date()) {
      return null;
    }

    return token.user;
  }

  /**
   * Login with email/password
   */
  async login(email: string, password: string): Promise<User | null> {
    const user = await prisma.user.findUnique({ where: { email } });

    if (!user) {
      return null;
    }

    const isValid = await bcrypt.compare(password, user.passwordHash);

    if (!isValid) {
      return null;
    }

    return user;
  }
}

export const authService = new AuthService();

3.2: Authentication Middleware

Create src/middleware/auth.ts:

typescript
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import { User } from '@prisma/client';
import { authService } from '../services/auth.service';

export interface AuthRequest extends Request {
  user?: User;
  authMethod?: 'jwt' | 'api_key' | 'oauth';
}

/**
 * JWT Authentication Middleware
 */
export async function authenticateJWT(
  req: AuthRequest,
  res: Response,
  next: NextFunction
) {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({
        error: 'Missing or invalid authorization header',
        expected: 'Bearer <token>',
      });
    }

    const token = authHeader.substring(7);
    const payload = authService.verifyJWT(token);

    // Fetch user from database
    const { PrismaClient } = await import('@prisma/client');
    const prisma = new PrismaClient();

    const user = await prisma.user.findUnique({
      where: { id: payload.userId },
    });

    if (!user) {
      return res.status(401).json({ error: 'User not found' });
    }

    req.user = user;
    req.authMethod = 'jwt';
    next();
  } catch (error: any) {
    res.status(401).json({ error: error.message || 'Invalid token' });
  }
}

/**
 * API Key Authentication Middleware
 */
export async function authenticateApiKey(
  req: AuthRequest,
  res: Response,
  next: NextFunction
) {
  try {
    const apiKey = req.headers['x-api-key'] as string;

    if (!apiKey) {
      return res.status(401).json({
        error: 'Missing API key',
        expected: 'X-API-Key header',
      });
    }

    const user = await authService.validateApiKey(apiKey);

    if (!user) {
      return res.status(401).json({ error: 'Invalid or expired API key' });
    }

    req.user = user;
    req.authMethod = 'api_key';
    next();
  } catch (error: any) {
    res.status(401).json({ error: error.message || 'Authentication failed' });
  }
}

/**
 * OAuth Token Authentication Middleware
 */
export async function authenticateOAuth(
  req: AuthRequest,
  res: Response,
  next: NextFunction
) {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({
        error: 'Missing or invalid authorization header',
        expected: 'Bearer <oauth_token>',
      });
    }

    const token = authHeader.substring(7);
    const user = await authService.validateOAuthToken(token);

    if (!user) {
      return res.status(401).json({ error: 'Invalid or expired OAuth token' });
    }

    req.user = user;
    req.authMethod = 'oauth';
    next();
  } catch (error: any) {
    res.status(401).json({ error: error.message || 'Authentication failed' });
  }
}

/**
 * Multi-method authentication (tries all methods)
 */
export async function authenticateAny(
  req: AuthRequest,
  res: Response,
  next: NextFunction
) {
  // Try JWT first
  if (req.headers.authorization?.startsWith('Bearer ')) {
    try {
      const token = req.headers.authorization.substring(7);
      const payload = authService.verifyJWT(token);

      const { PrismaClient } = await import('@prisma/client');
      const prisma = new PrismaClient();

      const user = await prisma.user.findUnique({
        where: { id: payload.userId },
      });

      if (user) {
        req.user = user;
        req.authMethod = 'jwt';
        return next();
      }
    } catch (error) {
      // Continue to next method
    }

    // Try OAuth token
    try {
      const token = req.headers.authorization.substring(7);
      const user = await authService.validateOAuthToken(token);

      if (user) {
        req.user = user;
        req.authMethod = 'oauth';
        return next();
      }
    } catch (error) {
      // Continue to next method
    }
  }

  // Try API key
  if (req.headers['x-api-key']) {
    try {
      const user = await authService.validateApiKey(
        req.headers['x-api-key'] as string
      );

      if (user) {
        req.user = user;
        req.authMethod = 'api_key';
        return next();
      }
    } catch (error) {
      // Failed all methods
    }
  }

  res.status(401).json({
    error: 'Authentication required',
    supportedMethods: ['JWT (Bearer token)', 'API Key (X-API-Key header)', 'OAuth token'],
  });
}

/**
 * Role-based authorization middleware
 */
export function requireRole(...roles: string[]) {
  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        error: 'Insufficient permissions',
        required: roles,
        current: req.user.role,
      });
    }

    next();
  };
}

3.3: API Routes

Create src/routes/api.ts:

typescript
// src/routes/api.ts
import { Router } from 'express';
import { authService } from '../services/auth.service';
import {
  authenticateJWT,
  authenticateApiKey,
  authenticateOAuth,
  authenticateAny,
  requireRole,
  AuthRequest,
} from '../middleware/auth';

const router = Router();

/**
 * Register new user
 */
router.post('/auth/register', async (req, res) => {
  try {
    const { email, password, name } = req.body;

    if (!email || !password) {
      return res.status(400).json({ error: 'Email and password required' });
    }

    const user = await authService.register(email, password, name);

    res.status(201).json({
      message: 'User registered successfully',
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
      },
    });
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

/**
 * JWT: Login and get tokens
 */
router.post('/auth/login/jwt', async (req, res) => {
  try {
    const { email, password } = req.body;

    const user = await authService.login(email, password);

    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    const tokens = authService.generateJWTTokens(user);

    res.json({
      method: 'JWT',
      user: {
        id: user.id,
        email: user.email,
        role: user.role,
      },
      ...tokens,
    });
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

/**
 * JWT: Refresh access token
 */
router.post('/auth/refresh', async (req, res) => {
  try {
    const { refreshToken } = req.body;

    if (!refreshToken) {
      return res.status(400).json({ error: 'Refresh token required' });
    }

    const tokens = await authService.refreshJWT(refreshToken);

    res.json(tokens);
  } catch (error: any) {
    res.status(401).json({ error: error.message });
  }
});

/**
 * API Key: Generate new API key
 */
router.post('/auth/apikey/generate', authenticateJWT, async (req: AuthRequest, res) => {
  try {
    const { name, permissions, expiresInDays } = req.body;

    if (!name) {
      return res.status(400).json({ error: 'Key name required' });
    }

    const { key, prefix } = await authService.generateApiKey(
      req.user!.id,
      name,
      permissions,
      expiresInDays
    );

    res.json({
      message: 'API key generated successfully',
      apiKey: key,
      prefix,
      warning: 'Save this key securely. It will not be shown again.',
    });
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

/**
 * Protected route - JWT only
 */
router.get('/protected/jwt', authenticateJWT, (req: AuthRequest, res) => {
  res.json({
    message: 'Access granted via JWT',
    user: req.user,
    authMethod: req.authMethod,
  });
});

/**
 * Protected route - API Key only
 */
router.get('/protected/apikey', authenticateApiKey, (req: AuthRequest, res) => {
  res.json({
    message: 'Access granted via API Key',
    user: req.user,
    authMethod: req.authMethod,
  });
});

/**
 * Protected route - Any auth method
 */
router.get('/protected/any', authenticateAny, (req: AuthRequest, res) => {
  res.json({
    message: 'Access granted via any method',
    user: req.user,
    authMethod: req.authMethod,
  });
});

/**
 * Admin-only route
 */
router.get(
  '/admin/users',
  authenticateAny,
  requireRole('ADMIN'),
  async (req: AuthRequest, res) => {
    try {
      const { PrismaClient } = await import('@prisma/client');
      const prisma = new PrismaClient();

      const users = await prisma.user.findMany({
        select: {
          id: true,
          email: true,
          name: true,
          role: true,
          createdAt: true,
        },
      });

      res.json({ users });
    } catch (error: any) {
      res.status(500).json({ error: error.message });
    }
  }
);

/**
 * Get authentication comparison
 */
router.get('/auth/comparison', (req, res) => {
  res.json({
    methods: {
      JWT: {
        description: 'JSON Web Tokens for stateless authentication',
        useCases: ['User sessions', 'Single-page applications', 'Mobile apps'],
        pros: [
          'Stateless (no server storage)',
          'Self-contained (includes user data)',
          'Works across domains',
          'Industry standard',
        ],
        cons: [
          'Cannot invalidate before expiry',
          'Larger payload size',
          'Vulnerable if secret is compromised',
          'Requires careful expiration management',
        ],
        implementation: 'Bearer token in Authorization header',
        tokenLifetime: '15 minutes (access), 7 days (refresh)',
      },
      API_KEY: {
        description: 'Long-lived keys for server-to-server authentication',
        useCases: [
          'Server-to-server communication',
          'CLI tools',
          'Third-party integrations',
          'IoT devices',
        ],
        pros: [
          'Simple implementation',
          'Long-lived (months/years)',
          'Easy to rotate',
          'Can scope permissions',
        ],
        cons: [
          'Shared secret (must be kept secure)',
          'No automatic expiration',
          'Less granular than OAuth scopes',
          'Requires secure storage',
        ],
        implementation: 'X-API-Key header',
        tokenLifetime: 'Configurable (default: no expiration)',
      },
      OAuth: {
        description: 'Delegated authorization for third-party access',
        useCases: [
          'Third-party app access',
          'Social login',
          'Enterprise SSO',
          'Multi-tenant applications',
        ],
        pros: [
          'Delegated authorization',
          'Granular scopes',
          'Industry standard',
          'Token refresh built-in',
        ],
        cons: [
          'Complex implementation',
          'Requires OAuth provider',
          'More network requests',
          'Learning curve',
        ],
        implementation: 'Bearer token in Authorization header',
        tokenLifetime: '1 hour (access), 30 days (refresh)',
      },
    },
    recommendations: {
      'User authentication in web/mobile apps': 'JWT',
      'Server-to-server API calls': 'API Key',
      'Third-party integrations': 'OAuth',
      'CLI tools': 'API Key',
      'Microservices communication': 'JWT or API Key',
      'Social login': 'OAuth',
    },
  });
});

export default router;

3.4: Main Application

Create src/index.ts:

typescript
// src/index.ts
import express from 'express';
import cors from 'cors';
import { config } from './config';
import apiRoutes from './routes/api';

const app = express();

// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Request logging
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

// Routes
app.use('/api', apiRoutes);

// Home page
app.get('/', (req, res) => {
  res.json({
    name: 'API Authentication Methods Comparison',
    version: '1.0.0',
    endpoints: {
      comparison: '/api/auth/comparison',
      register: 'POST /api/auth/register',
      jwt: {
        login: 'POST /api/auth/login/jwt',
        refresh: 'POST /api/auth/refresh',
        protected: 'GET /api/protected/jwt',
      },
      apiKey: {
        generate: 'POST /api/auth/apikey/generate (requires JWT auth)',
        protected: 'GET /api/protected/apikey',
      },
      any: 'GET /api/protected/any (supports all methods)',
    },
  });
});

// Error handler
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
  console.error('Error:', err);
  res.status(err.status || 500).json({
    error: err.message || 'Internal server error',
  });
});

// Start server
app.listen(config.port, () => {
  console.log(`Server running on http://localhost:${config.port}`);
  console.log(`Environment: ${config.env}`);
});

Update package.json scripts:

json
{
  "scripts": {
    "dev": "nodemon --exec tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "migrate": "npx prisma migrate dev",
    "studio": "npx prisma studio"
  }
}

Step 4: Testing

4.1: Start Services

Start PostgreSQL:

bash
docker run --name postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres

Run migrations:

bash
npm run migrate

Start server:

bash
npm run dev

4.2: Test Registration

bash
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123",
    "name": "Test User"
  }'

Expected response:

json
{
  "message": "User registered successfully",
  "user": {
    "id": "clxxx",
    "email": "test@example.com",
    "name": "Test User",
    "role": "USER"
  }
}

4.3: Test JWT Authentication

Step 1: Login

bash
curl -X POST http://localhost:3000/api/auth/login/jwt \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123"
  }'

Response:

json
{
  "method": "JWT",
  "user": {...},
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

Step 2: Access Protected Route

bash
export JWT_TOKEN="your_access_token_here"

curl http://localhost:3000/api/protected/jwt \
  -H "Authorization: Bearer $JWT_TOKEN"

Step 3: Refresh Token

bash
curl -X POST http://localhost:3000/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "your_refresh_token_here"
  }'

4.4: Test API Key Authentication

Step 1: Generate API Key

bash
curl -X POST http://localhost:3000/api/auth/apikey/generate \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production API Key",
    "expiresInDays": 365
  }'

Response:

json
{
  "message": "API key generated successfully",
  "apiKey": "sk_live_abc123...",
  "prefix": "sk_live_abc1",
  "warning": "Save this key securely. It will not be shown again."
}

Step 2: Use API Key

bash
export API_KEY="sk_live_abc123..."

curl http://localhost:3000/api/protected/apikey \
  -H "X-API-Key: $API_KEY"

4.5: Test Multi-Method Endpoint

bash
# Using JWT
curl http://localhost:3000/api/protected/any \
  -H "Authorization: Bearer $JWT_TOKEN"

# Using API Key
curl http://localhost:3000/api/protected/any \
  -H "X-API-Key: $API_KEY"

Both should work and return different authMethod values.

4.6: Compare Performance

Create benchmark.js:

javascript
// benchmark.js
const axios = require('axios');

const API_URL = 'http://localhost:3000';
let jwtToken, apiKey;

async function setup() {
  // Register and login
  await axios.post(`${API_URL}/api/auth/register`, {
    email: 'bench@example.com',
    password: 'password123',
  }).catch(() => {}); // Ignore if exists

  const loginRes = await axios.post(`${API_URL}/api/auth/login/jwt`, {
    email: 'bench@example.com',
    password: 'password123',
  });

  jwtToken = loginRes.data.accessToken;

  // Generate API key
  const keyRes = await axios.post(
    `${API_URL}/api/auth/apikey/generate`,
    { name: 'Benchmark Key' },
    { headers: { Authorization: `Bearer ${jwtToken}` } }
  );

  apiKey = keyRes.data.apiKey;
}

async function benchmark(method, iterations = 1000) {
  const start = Date.now();
  const headers = method === 'jwt'
    ? { Authorization: `Bearer ${jwtToken}` }
    : { 'X-API-Key': apiKey };

  for (let i = 0; i < iterations; i++) {
    await axios.get(`${API_URL}/api/protected/any`, { headers });
  }

  const duration = Date.now() - start;
  console.log(`${method.toUpperCase()}: ${iterations} requests in ${duration}ms (${(iterations / (duration / 1000)).toFixed(2)} req/s)`);
}

async function run() {
  await setup();
  console.log('Running benchmarks...\n');
  await benchmark('jwt', 100);
  await benchmark('apikey', 100);
}

run();

Run:

bash
npm install axios
node benchmark.js

Common Errors & Troubleshooting

1. Error: “Invalid or expired token” with valid JWT

Cause: JWT secret changed, token expired, or clock skew between systems.

Fix: Implement proper token validation and clock tolerance:

typescript
// In auth.service.ts
verifyJWT(token: string): JWTPayload {
  try {
    return jwt.verify(token, config.jwt.secret, {
      clockTolerance: 60, // Allow 60 seconds clock skew
    }) as JWTPayload;
  } catch (error: any) {
    // Provide specific error messages
    if (error.name === 'TokenExpiredError') {
      throw new Error('Token expired - please refresh');
    } else if (error.name === 'JsonWebTokenError') {
      throw new Error('Invalid token signature');
    } else if (error.name === 'NotBeforeError') {
      throw new Error('Token not yet valid');
    }
    throw error;
  }
}

Implement automatic refresh:

typescript
// In client code
async function makeAuthenticatedRequest(url: string) {
  try {
    return await fetch(url, {
      headers: { Authorization: `Bearer ${accessToken}` },
    });
  } catch (error: any) {
    if (error.message.includes('expired')) {
      // Refresh token
      const newTokens = await refreshTokens();
      accessToken = newTokens.accessToken;
      
      // Retry request
      return await fetch(url, {
        headers: { Authorization: `Bearer ${accessToken}` },
      });
    }
    throw error;
  }
}

2. Error: API key validation extremely slow

Cause: Iterating through all API keys and comparing bcrypt hashes is O(n) and computationally expensive.

Fix: Implement indexed lookup with prefix:

typescript
// Optimized API key validation
async validateApiKey(key: string): Promise<User | null> {
  try {
    // Extract prefix (first 12 chars)
    const prefix = key.substring(0, 12);
    
    // Query only keys with matching prefix
    const apiKeys = await prisma.apiKey.findMany({
      where: { prefix },
      include: { user: true },
    });
    
    // Should only be 1-2 keys to check now instead of all keys
    for (const apiKey of apiKeys) {
      const isMatch = await bcrypt.compare(key, apiKey.keyHash);
      
      if (isMatch) {
        // Check expiration
        if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
          throw new Error('API key expired');
        }
        
        // Update last used (async, don't wait)
        prisma.apiKey.update({
          where: { id: apiKey.id },
          data: { lastUsedAt: new Date() },
        }).catch(console.error);
        
        return apiKey.user;
      }
    }
    
    return null;
  } catch (error) {
    console.error('API key validation error:', error);
    return null;
  }
}

Use caching for frequently used keys:

typescript
import NodeCache from 'node-cache';

const keyCache = new NodeCache({ stdTTL: 600 }); // 10 minutes

async validateApiKey(key: string): Promise<User | null> {
  // Check cache first
  const cached = keyCache.get<User>(key);
  if (cached) return cached;
  
  // ... validation logic ...
  
  if (user) {
    keyCache.set(key, user);
  }
  
  return user;
}

3. Error: OAuth tokens not invalidating on sign-out

Cause: OAuth tokens stored in database aren’t being deleted or marked invalid on logout.

Fix: Implement proper token revocation:

typescript
// In auth.service.ts
async revokeOAuthToken(accessToken: string): Promise<void> {
  await prisma.oAuthToken.delete({
    where: { accessToken },
  });
}

async revokeAllUserTokens(userId: string): Promise<void> {
  await prisma.oAuthToken.deleteMany({
    where: { userId },
  });
}

Add logout endpoint:

typescript
// In routes/api.ts
router.post('/auth/logout', authenticateAny, async (req: AuthRequest, res) => {
  try {
    if (req.authMethod === 'oauth' && req.headers.authorization) {
      const token = req.headers.authorization.substring(7);
      await authService.revokeOAuthToken(token);
    }
    
    res.json({ message: 'Logged out successfully' });
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

Implement token cleanup cron:

typescript
// Clean up expired tokens daily
import cron from 'node-cron';

cron.schedule('0 0 * * *', async () => {
  const deleted = await prisma.oAuthToken.deleteMany({
    where: {
      expiresAt: { lt: new Date() },
    },
  });
  
  console.log(`Cleaned up ${deleted.count} expired tokens`);
});

Security Checklist

  • Use HTTPS in production for all authentication endpoints
  • Never log tokens or API keys in application logs
  • Implement rate limiting on authentication endpoints
  • Use strong JWT secrets (minimum 256 bits, cryptographically random)
  • Set appropriate token expiration – short for access tokens (15min), longer for refresh (7 days)
  • Rotate secrets regularly and have a rotation strategy
  • Hash API keys before storage using bcrypt or argon2
  • Implement API key rotation mechanism for zero-downtime updates
  • Use PKCE flow for OAuth when available
  • Validate all user inputs before processing
  • Implement CORS properly – whitelist specific origins
  • Store refresh tokens securely – httpOnly cookies in browsers
  • Revoke tokens on suspicious activity – failed attempts, location changes
  • Monitor authentication metrics – failed logins, token refreshes
  • Implement account lockout after multiple failed attempts
  • Use different secrets for access and refresh tokens
  • Validate token audience and issuer in JWT verification
  • Implement token blacklisting for compromised tokens
  • Audit API key usage – track when and where keys are used
  • Encrypt sensitive data at rest including OAuth tokens

Related Resources

For implementing OAuth with Supabase, see our guide on OAuth login with Supabase in a single hook covering simplified integration patterns.

When building complete authentication systems with MFA, check our tutorial on implementing multi-factor authentication with Auth0 for enterprise-grade security.

For designing comprehensive API architectures, explore our article on designing scalable REST APIs for SaaS covering versioning and documentation.

When implementing usage-based pricing with authenticated APIs, read our guide on API rate limiting strategies for production systems.

For teams evaluating API protocols, our GraphQL vs REST comparison helps choose the right approach for your authentication requirements.

Leave a Comment

Your email address will not be published. Required fields are marked *

banner
Scroll to Top