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:
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:
npx tsc --init
Update tsconfig.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:
npx prisma init
Create prisma/schema.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:
npx prisma migrate dev --name init
npx prisma generate
Create project structure:
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:
# .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:
echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore
echo "dist/" >> .gitignore
Create src/config.ts:
// 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:
// 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:
// 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:
// 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:
// 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:
{
"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:
docker run --name postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres
Run migrations:
npm run migrate
Start server:
npm run dev
4.2: Test Registration
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:
{
"message": "User registered successfully",
"user": {
"id": "clxxx",
"email": "test@example.com",
"name": "Test User",
"role": "USER"
}
}
4.3: Test JWT Authentication
Step 1: Login
curl -X POST http://localhost:3000/api/auth/login/jwt \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123"
}'
Response:
{
"method": "JWT",
"user": {...},
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
Step 2: Access Protected Route
export JWT_TOKEN="your_access_token_here"
curl http://localhost:3000/api/protected/jwt \
-H "Authorization: Bearer $JWT_TOKEN"
Step 3: Refresh Token
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
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:
{
"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
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
# 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:
// 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:
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:
// 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:
// 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:
// 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:
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:
// 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:
// 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:
// 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.

Finly Insights Team is a group of software developers, cloud engineers, and technical writers with real hands-on experience in the tech industry. We specialize in cloud computing, cybersecurity, SaaS tools, AI automation, and API development. Every article we publish is thoroughly researched, written, and reviewed by people who have actually worked in these fields.




