Intermediate
30 min
Full Guide

API Authentication

Implement secure API authentication with API keys, JWT tokens, and OAuth 2.0

Why API Authentication?

API Authentication verifies the identity of clients making requests to your API. It ensures that only authorized users and applications can access protected resources, preventing unauthorized access and data breaches.

Different authentication methods suit different use cases—from simple API keys for server-to-server communication to OAuth 2.0 for third-party app authorization.

🔐 Authentication vs Authorization

Authentication (AuthN)

"Who are you?"

Verifying identity with credentials like passwords, tokens, or certificates.

Authorization (AuthZ)

"What can you do?"

Determining what resources/actions a verified user can access.

1. API Keys

Simplest form of authentication—a secret key sent with each request:

// API Key Authentication
// Usually sent in headers or query parameters

// Method 1: Header (preferred)
const response = await fetch('https://api.example.com/data', {
  headers: {
    'X-API-Key': 'your-api-key-here'
  }
});

// Method 2: Query parameter (less secure - visible in logs)
const response = await fetch(
  'https://api.example.com/data?api_key=your-api-key-here'
);

// Method 3: Basic Auth with API key as username
const credentials = btoa('your-api-key:'); // Base64 encode
const response = await fetch('https://api.example.com/data', {
  headers: {
    'Authorization': `Basic ${credentials}`
  }
});

// Reusable API client with API key
class ApiKeyClient {
  constructor(baseUrl, apiKey) {
    this.baseUrl = baseUrl;
    this.apiKey = apiKey;
  }

  async request(endpoint, options = {}) {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        'X-API-Key': this.apiKey,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });
    return response.json();
  }
}

// Usage
const api = new ApiKeyClient('https://api.weather.com', 'abc123');
const weather = await api.request('/current?city=London');

⚠️ API Key Security

  • • Never expose API keys in client-side code
  • • Use environment variables for storage
  • • Rotate keys regularly
  • • Use different keys for different environments

2. JWT (JSON Web Tokens)

Self-contained tokens that encode user information and are cryptographically signed:

// JWT Structure: header.payload.signature
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
// eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.
// SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

// JWT Authentication Flow
class AuthClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.accessToken = null;
    this.refreshToken = null;
  }

  // Step 1: Login to get tokens
  async login(email, password) {
    const response = await fetch(`${this.baseUrl}/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    });

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

    const data = await response.json();
    this.accessToken = data.accessToken;
    this.refreshToken = data.refreshToken;
    
    // Store refresh token securely
    localStorage.setItem('refreshToken', this.refreshToken);
    
    return data.user;
  }

  // Step 2: Use access token for API requests
  async request(endpoint, options = {}) {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        'Authorization': `Bearer ${this.accessToken}`,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });

    // If token expired, refresh and retry
    if (response.status === 401) {
      await this.refreshAccessToken();
      return this.request(endpoint, options);
    }

    return response.json();
  }

  // Step 3: Refresh expired access token
  async refreshAccessToken() {
    const response = await fetch(`${this.baseUrl}/auth/refresh`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken: this.refreshToken })
    });

    if (!response.ok) {
      // Refresh token also expired - redirect to login
      this.logout();
      throw new Error('Session expired');
    }

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

  // Step 4: Logout
  logout() {
    this.accessToken = null;
    this.refreshToken = null;
    localStorage.removeItem('refreshToken');
    window.location.href = '/login';
  }
}

// Usage
const auth = new AuthClient('https://api.example.com');
await auth.login('user@example.com', 'password123');

const profile = await auth.request('/user/profile');
const posts = await auth.request('/user/posts');

Decoding JWT Tokens

// JWT tokens can be decoded (but not verified) client-side
// Never trust decoded data for security decisions!

function decodeJWT(token) {
  try {
    const [header, payload, signature] = token.split('.');
    
    // Base64 decode the payload
    const decodedPayload = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
    return JSON.parse(decodedPayload);
  } catch {
    return null;
  }
}

// Check if token is expired
function isTokenExpired(token) {
  const payload = decodeJWT(token);
  if (!payload || !payload.exp) return true;
  
  // exp is in seconds, Date.now() is in milliseconds
  return Date.now() >= payload.exp * 1000;
}

// Get time until expiration
function getTokenExpiresIn(token) {
  const payload = decodeJWT(token);
  if (!payload || !payload.exp) return 0;
  
  return Math.max(0, payload.exp * 1000 - Date.now());
}

// Usage
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';

const payload = decodeJWT(token);
console.log('User ID:', payload.sub);
console.log('Expires:', new Date(payload.exp * 1000));
console.log('Is expired:', isTokenExpired(token));
console.log('Expires in:', getTokenExpiresIn(token) / 1000, 'seconds');

// Example JWT payload
{
  "sub": "user-123",        // Subject (user ID)
  "name": "John Doe",       // Custom claim
  "email": "john@test.com", // Custom claim
  "role": "admin",          // Custom claim
  "iat": 1704067200,        // Issued at
  "exp": 1704070800         // Expires at
}

3. OAuth 2.0

Industry-standard protocol for authorization, enabling third-party apps to access user data:

OAuth 2.0 Flow Types

  • Authorization Code - For server-side apps (most secure)
  • PKCE - For SPAs and mobile apps
  • Client Credentials - For machine-to-machine
  • Implicit - Deprecated, don't use
// OAuth 2.0 with PKCE (Proof Key for Code Exchange)
// Best for browser-based applications

class OAuthClient {
  constructor(config) {
    this.clientId = config.clientId;
    this.redirectUri = config.redirectUri;
    this.authorizationUrl = config.authorizationUrl;
    this.tokenUrl = config.tokenUrl;
    this.scope = config.scope;
  }

  // Step 1: Generate PKCE challenge
  async generatePKCE() {
    // Generate random verifier
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    const verifier = btoa(String.fromCharCode(...array))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');

    // Create SHA-256 challenge
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const hash = await crypto.subtle.digest('SHA-256', data);
    const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');

    return { verifier, challenge };
  }

  // Step 2: Redirect to authorization
  async startAuth() {
    const { verifier, challenge } = await this.generatePKCE();
    const state = crypto.randomUUID();

    // Store for later verification
    sessionStorage.setItem('pkce_verifier', verifier);
    sessionStorage.setItem('oauth_state', state);

    // Build authorization URL
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: this.clientId,
      redirect_uri: this.redirectUri,
      scope: this.scope,
      state: state,
      code_challenge: challenge,
      code_challenge_method: 'S256'
    });

    // Redirect to auth server
    window.location.href = `${this.authorizationUrl}?${params}`;
  }

  // Step 3: Handle callback and exchange code for tokens
  async handleCallback() {
    const params = new URLSearchParams(window.location.search);
    const code = params.get('code');
    const state = params.get('state');
    const error = params.get('error');

    // Check for errors
    if (error) {
      throw new Error(params.get('error_description') || error);
    }

    // Verify state
    const savedState = sessionStorage.getItem('oauth_state');
    if (state !== savedState) {
      throw new Error('Invalid state parameter');
    }

    // Get verifier
    const verifier = sessionStorage.getItem('pkce_verifier');

    // Exchange code for tokens
    const response = await fetch(this.tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: this.clientId,
        redirect_uri: this.redirectUri,
        code: code,
        code_verifier: verifier
      })
    });

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

    const tokens = await response.json();
    
    // Cleanup
    sessionStorage.removeItem('pkce_verifier');
    sessionStorage.removeItem('oauth_state');

    return tokens;
  }
}

// Usage - Google OAuth example
const oauth = new OAuthClient({
  clientId: 'your-client-id.apps.googleusercontent.com',
  redirectUri: 'https://yourapp.com/callback',
  authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
  tokenUrl: 'https://oauth2.googleapis.com/token',
  scope: 'openid email profile'
});

// Start login
document.getElementById('login').onclick = () => oauth.startAuth();

// On callback page
if (window.location.pathname === '/callback') {
  const tokens = await oauth.handleCallback();
  console.log('Access token:', tokens.access_token);
}

Social Login Examples

// Using OAuth with popular providers

// GitHub OAuth
const githubAuth = new OAuthClient({
  clientId: 'your-github-client-id',
  redirectUri: 'https://yourapp.com/auth/github/callback',
  authorizationUrl: 'https://github.com/login/oauth/authorize',
  tokenUrl: 'https://github.com/login/oauth/access_token',
  scope: 'user:email read:user'
});

// After getting access token, fetch user info
async function getGitHubUser(accessToken) {
  const response = await fetch('https://api.github.com/user', {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Accept': 'application/json'
    }
  });
  return response.json();
}

// Google OAuth
const googleAuth = new OAuthClient({
  clientId: 'your-google-client-id',
  redirectUri: 'https://yourapp.com/auth/google/callback',
  authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
  tokenUrl: 'https://oauth2.googleapis.com/token',
  scope: 'openid email profile'
});

// Microsoft OAuth
const microsoftAuth = new OAuthClient({
  clientId: 'your-azure-client-id',
  redirectUri: 'https://yourapp.com/auth/microsoft/callback',
  authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
  tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
  scope: 'openid email profile User.Read'
});

Secure Token Storage

// Token storage best practices

// ❌ DON'T store in localStorage (vulnerable to XSS)
localStorage.setItem('accessToken', token);

// ✅ For access tokens: Keep in memory only
class TokenManager {
  #accessToken = null;  // Private field
  
  setAccessToken(token) {
    this.#accessToken = token;
  }
  
  getAccessToken() {
    return this.#accessToken;
  }
  
  clearTokens() {
    this.#accessToken = null;
  }
}

// ✅ For refresh tokens: Use httpOnly cookies (set by server)
// Server response sets cookie:
// Set-Cookie: refreshToken=xxx; HttpOnly; Secure; SameSite=Strict

// ✅ If you must use storage, encrypt sensitive data
class SecureStorage {
  constructor(encryptionKey) {
    this.key = encryptionKey;
  }

  async encrypt(data) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(JSON.stringify(data));
    
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.key,
      dataBuffer
    );

    return btoa(JSON.stringify({
      iv: Array.from(iv),
      data: Array.from(new Uint8Array(encrypted))
    }));
  }

  async decrypt(encryptedString) {
    const { iv, data } = JSON.parse(atob(encryptedString));
    
    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: new Uint8Array(iv) },
      this.key,
      new Uint8Array(data)
    );

    const decoder = new TextDecoder();
    return JSON.parse(decoder.decode(decrypted));
  }
}

// Session timeout - auto logout
function setupSessionTimeout(timeoutMinutes = 30) {
  let timeoutId;
  
  function resetTimer() {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      auth.logout();
      alert('Session expired. Please log in again.');
    }, timeoutMinutes * 60 * 1000);
  }

  // Reset on user activity
  ['click', 'keydown', 'mousemove', 'scroll'].forEach(event => {
    document.addEventListener(event, resetTimer);
  });

  resetTimer();
}

💡 Authentication Best Practices

  • Use HTTPS always - Never send tokens over HTTP
  • Short-lived access tokens - 15-60 minutes max
  • Refresh tokens securely - HttpOnly cookies preferred
  • Implement token rotation - New refresh token on each use
  • Use PKCE for SPAs - Protects against code interception
  • Validate state parameter - Prevents CSRF attacks