[go: up one dir, main page]

Browse 1000+ Public APIs
API Authentication Methods Explained: API Keys, OAuth, JWT & More
a month ago

API authentication is how you verify that requests to your API come from legitimate users or applications. Choosing the right method is crucial for security and user experience. This guide explains the most common authentication methods with implementation examples.

Authentication Methods Overview

Method Complexity Best For Security Level
API Keys Low Server-to-server Medium
Basic Auth Low Simple APIs Low
Bearer Tokens Medium Web/mobile apps Medium-High
JWT Medium Stateless auth High
OAuth 2.0 High Third-party access High
API Key + Secret Medium Signed requests High

1. API Keys

The simplest authentication method. A unique string identifies the caller.

How It Works

  1. Developer registers and receives an API key
  2. Key is sent with each request (header or query param)
  3. Server validates the key and processes the request

Implementation

Server-side (Express):

const express = require('express');
const crypto = require('crypto');

const app = express();

// Store API keys (in production, use a database)
const apiKeys = new Map([
  ['ak_live_abc123', { userId: '1', plan: 'pro', rateLimit: 1000 }],
  ['ak_live_xyz789', { userId: '2', plan: 'free', rateLimit: 100 }]
]);

function authenticateApiKey(req, res, next) {
  // Accept key from header or query parameter
  const apiKey = req.headers['x-api-key'] || req.query.api_key;

  if (!apiKey) {
    return res.status(401).json({
      error: 'API key required',
      message: 'Include your API key in the X-API-Key header'
    });
  }

  const keyData = apiKeys.get(apiKey);

  if (!keyData) {
    return res.status(401).json({
      error: 'Invalid API key',
      message: 'The provided API key is not valid'
    });
  }

  // Attach user data to request
  req.apiUser = keyData;
  next();
}

// Protected route
app.get('/api/data', authenticateApiKey, (req, res) => {
  res.json({
    message: 'Authenticated successfully',
    plan: req.apiUser.plan
  });
});

Client-side usage:

// Using fetch
const response = await fetch('https://api.example.com/data', {
  headers: {
    'X-API-Key': 'ak_live_abc123'
  }
});

// Using axios
const axios = require('axios');

const client = axios.create({
  baseURL: 'https://api.example.com',
  headers: {
    'X-API-Key': 'ak_live_abc123'
  }
});

const data = await client.get('/data');

Best Practices

  • Use HTTPS only
  • Don't expose keys in client-side code
  • Implement key rotation
  • Add rate limiting per key
  • Use prefixes to identify key types (e.g., ak_live_, ak_test_)

2. Basic Authentication

Sends username and password encoded in Base64 with each request.

How It Works

  1. Combine username:password
  2. Encode in Base64
  3. Send in Authorization header

Implementation

Server-side:

function basicAuth(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Basic ')) {
    res.setHeader('WWW-Authenticate', 'Basic realm="API"');
    return res.status(401).json({ error: 'Authentication required' });
  }

  // Decode Base64 credentials
  const base64Credentials = authHeader.split(' ')[1];
  const credentials = Buffer.from(base64Credentials, 'base64').toString('utf8');
  const [username, password] = credentials.split(':');

  // Validate credentials (use constant-time comparison!)
  const validUser = 'api_user';
  const validPass = 'secure_password';

  const usernameValid = crypto.timingSafeEqual(
    Buffer.from(username),
    Buffer.from(validUser)
  );
  const passwordValid = crypto.timingSafeEqual(
    Buffer.from(password),
    Buffer.from(validPass)
  );

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

  next();
}

Client-side:

const username = 'api_user';
const password = 'secure_password';

const response = await fetch('https://api.example.com/data', {
  headers: {
    'Authorization': 'Basic ' + btoa(`${username}:${password}`)
  }
});

// Node.js
const response = await fetch('https://api.example.com/data', {
  headers: {
    'Authorization': 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64')
  }
});

When to Use

  • Internal APIs
  • Simple integrations
  • When combined with HTTPS
  • Avoid for public-facing APIs

3. Bearer Token Authentication

A token (usually from OAuth or login) sent in the Authorization header.

How It Works

  1. User authenticates (login, OAuth, etc.)
  2. Server issues a bearer token
  3. Client includes token in requests
  4. Server validates token

Implementation

Server-side:

const tokens = new Map(); // In production, use Redis or database

// Login endpoint - issues token
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;

  // Validate credentials
  const user = await validateUser(email, password);
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Generate token
  const token = crypto.randomBytes(32).toString('hex');

  // Store token with user data and expiry
  tokens.set(token, {
    userId: user.id,
    expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
  });

  res.json({
    access_token: token,
    token_type: 'Bearer',
    expires_in: 86400
  });
});

// Middleware to validate bearer token
function bearerAuth(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Bearer token required' });
  }

  const token = authHeader.split(' ')[1];
  const tokenData = tokens.get(token);

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

  if (Date.now() > tokenData.expiresAt) {
    tokens.delete(token);
    return res.status(401).json({ error: 'Token expired' });
  }

  req.userId = tokenData.userId;
  next();
}

// Protected route
app.get('/api/profile', bearerAuth, async (req, res) => {
  const user = await getUser(req.userId);
  res.json(user);
});

Client-side:

// After login, store the token
const loginResponse = await fetch('/api/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password })
});

const { access_token } = await loginResponse.json();

// Use token for subsequent requests
const response = await fetch('/api/profile', {
  headers: {
    'Authorization': `Bearer ${access_token}`
  }
});

4. JWT (JSON Web Tokens)

Self-contained tokens that include user data, signed by the server.

How It Works

  1. User authenticates
  2. Server creates JWT with user data and signs it
  3. Client stores and sends JWT with requests
  4. Server verifies signature and extracts user data

Structure

header.payload.signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.  // Header (Base64)
eyJ1c2VySWQiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ.  // Payload (Base64)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  // Signature

Implementation

const jwt = require('jsonwebtoken');

const JWT_SECRET = process.env.JWT_SECRET; // Use strong secret!
const JWT_EXPIRES_IN = '1h';
const REFRESH_EXPIRES_IN = '7d';

// Generate tokens
function generateTokens(user) {
  const accessToken = jwt.sign(
    {
      userId: user.id,
      email: user.email,
      role: user.role
    },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES_IN }
  );

  const refreshToken = jwt.sign(
    { userId: user.id, type: 'refresh' },
    JWT_SECRET,
    { expiresIn: REFRESH_EXPIRES_IN }
  );

  return { accessToken, refreshToken };
}

// Login endpoint
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;

  const user = await validateUser(email, password);
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const tokens = generateTokens(user);

  // Set refresh token as HTTP-only cookie
  res.cookie('refreshToken', tokens.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
  });

  res.json({
    accessToken: tokens.accessToken,
    expiresIn: 3600
  });
});

// JWT verification middleware
function verifyJWT(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({
        error: 'Token expired',
        code: 'TOKEN_EXPIRED'
      });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Refresh token endpoint
app.post('/api/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  if (!refreshToken) {
    return res.status(401).json({ error: 'No refresh token' });
  }

  try {
    const decoded = jwt.verify(refreshToken, JWT_SECRET);

    if (decoded.type !== 'refresh') {
      return res.status(401).json({ error: 'Invalid token type' });
    }

    const user = await getUser(decoded.userId);
    const tokens = generateTokens(user);

    res.cookie('refreshToken', tokens.refreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000
    });

    res.json({
      accessToken: tokens.accessToken,
      expiresIn: 3600
    });
  } catch (error) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }
});

// Protected route
app.get('/api/profile', verifyJWT, (req, res) => {
  res.json({
    userId: req.user.userId,
    email: req.user.email,
    role: req.user.role
  });
});

Client-side Token Management

class AuthClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.accessToken = null;
  }

  async login(email, password) {
    const response = await fetch(`${this.baseURL}/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include', // Include cookies
      body: JSON.stringify({ email, password })
    });

    const data = await response.json();
    this.accessToken = data.accessToken;
    return data;
  }

  async refreshToken() {
    const response = await fetch(`${this.baseURL}/auth/refresh`, {
      method: 'POST',
      credentials: 'include'
    });

    if (!response.ok) {
      throw new Error('Refresh failed');
    }

    const data = await response.json();
    this.accessToken = data.accessToken;
    return data;
  }

  async request(path, options = {}) {
    const response = await fetch(`${this.baseURL}${path}`, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${this.accessToken}`
      }
    });

    // If token expired, try to refresh
    if (response.status === 401) {
      const data = await response.json();
      if (data.code === 'TOKEN_EXPIRED') {
        await this.refreshToken();
        // Retry the request
        return this.request(path, options);
      }
    }

    return response;
  }
}

5. OAuth 2.0

Industry standard for delegated authorization. Allows users to grant limited access to third-party apps.

Flows

Flow Use Case
Authorization Code Web apps (server-side)
Authorization Code + PKCE Mobile/SPA apps
Client Credentials Server-to-server
Device Code Smart TVs, CLIs

Authorization Code Flow (with PKCE)

const crypto = require('crypto');

// Generate PKCE values
function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('base64url');
  const challenge = crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');

  return { verifier, challenge };
}

// Step 1: Redirect user to authorization server
app.get('/auth/google', (req, res) => {
  const { verifier, challenge } = generatePKCE();

  // Store verifier in session
  req.session.codeVerifier = verifier;

  const params = new URLSearchParams({
    client_id: process.env.GOOGLE_CLIENT_ID,
    redirect_uri: 'https://yourapp.com/auth/google/callback',
    response_type: 'code',
    scope: 'openid email profile',
    code_challenge: challenge,
    code_challenge_method: 'S256',
    state: crypto.randomBytes(16).toString('hex')
  });

  res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// Step 2: Handle callback and exchange code for tokens
app.get('/auth/google/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state to prevent CSRF
  // (compare with stored state)

  // Exchange code for tokens
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      code,
      code_verifier: req.session.codeVerifier,
      grant_type: 'authorization_code',
      redirect_uri: 'https://yourapp.com/auth/google/callback'
    })
  });

  const tokens = await tokenResponse.json();

  // tokens contains: access_token, refresh_token, id_token, expires_in

  // Get user info
  const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
    headers: { 'Authorization': `Bearer ${tokens.access_token}` }
  });

  const user = await userResponse.json();

  // Create session or JWT for your app
  const appToken = generateTokens({ id: user.id, email: user.email });

  res.redirect(`/dashboard?token=${appToken.accessToken}`);
});

Client Credentials Flow (Server-to-Server)

async function getM2MToken() {
  const response = await fetch('https://auth.example.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: process.env.CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET,
      audience: 'https://api.example.com',
      grant_type: 'client_credentials'
    })
  });

  const { access_token, expires_in } = await response.json();
  return access_token;
}

6. HMAC Signature Authentication

Signs requests with a shared secret for high-security APIs.

Implementation

const crypto = require('crypto');

// Client-side: Sign requests
function signRequest(method, path, body, apiKey, apiSecret) {
  const timestamp = Date.now().toString();
  const bodyString = body ? JSON.stringify(body) : '';

  // Create signature string
  const signatureString = [
    method.toUpperCase(),
    path,
    timestamp,
    bodyString
  ].join('\n');

  // Generate HMAC signature
  const signature = crypto
    .createHmac('sha256', apiSecret)
    .update(signatureString)
    .digest('hex');

  return {
    'X-API-Key': apiKey,
    'X-Timestamp': timestamp,
    'X-Signature': signature
  };
}

// Usage
const headers = signRequest('POST', '/api/orders', { amount: 100 }, apiKey, apiSecret);

// Server-side: Verify signature
function verifySignature(req, res, next) {
  const apiKey = req.headers['x-api-key'];
  const timestamp = req.headers['x-timestamp'];
  const signature = req.headers['x-signature'];

  // Check timestamp freshness (prevent replay attacks)
  const now = Date.now();
  const requestTime = parseInt(timestamp);
  if (Math.abs(now - requestTime) > 300000) { // 5 minutes
    return res.status(401).json({ error: 'Request expired' });
  }

  // Get API secret for this key
  const apiSecret = getApiSecret(apiKey);
  if (!apiSecret) {
    return res.status(401).json({ error: 'Invalid API key' });
  }

  // Recreate signature
  const bodyString = req.body ? JSON.stringify(req.body) : '';
  const signatureString = [
    req.method,
    req.path,
    timestamp,
    bodyString
  ].join('\n');

  const expectedSignature = crypto
    .createHmac('sha256', apiSecret)
    .update(signatureString)
    .digest('hex');

  // Constant-time comparison
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  next();
}

Choosing the Right Method

Scenario Recommended Method
Public API for developers API Keys
Web app with users JWT + Refresh Tokens
Mobile app OAuth 2.0 + PKCE
Third-party integrations OAuth 2.0
Server-to-server API Keys or Client Credentials
High-security financial API HMAC Signatures
Simple internal API Basic Auth (with HTTPS)

Security Checklist

## Authentication Security

- [ ] Always use HTTPS
- [ ] Store secrets securely (env vars, secrets manager)
- [ ] Use constant-time comparison for tokens
- [ ] Implement token expiration
- [ ] Add rate limiting
- [ ] Log authentication failures
- [ ] Rotate secrets periodically
- [ ] Use secure session storage (HTTP-only cookies)
- [ ] Implement CSRF protection
- [ ] Validate redirect URIs (OAuth)

Conclusion

Choose your authentication method based on your use case. API Keys work well for developer APIs, JWT is great for modern web/mobile apps, and OAuth 2.0 is essential when third-party access is needed.

Remember: authentication verifies who the user is, while authorization determines what they can do. Implement both for a secure API.

Related Resources: