API Authentication - Complete Guide

API authentication is crucial for securing web services and controlling access to resources. This guide covers the most common authentication methods and how to implement them.

Why Authentication Matters #

Without proper authentication:

  • Anyone can access your API
  • Sensitive data can be exposed
  • Malicious users can abuse your resources
  • You cannot track or limit usage

Common Authentication Methods #

1. API Keys #

API keys are simple tokens that identify the client making the request.

How it works:

  • Client includes API key in request header or query parameter
  • Server validates the key against stored keys
  • If valid, request is processed

Implementation Example:

// Server-side (Express)
const express = require('express');
const app = express();

const VALID_API_KEYS = new Set([
  'key_abc123xyz',
  'key_def456uvw'
]);

function authenticateAPIKey(req, res, next) {
  const apiKey = req.headers['x-api-key'];

  if (!apiKey) {
    return res.status(401).json({ error: 'API key required' });
  }

  if (!VALID_API_KEYS.has(apiKey)) {
    return res.status(403).json({ error: 'Invalid API key' });
  }

  next();
}

app.use(authenticateAPIKey);

app.get('/api/data', (req, res) => {
  res.json({ message: 'Protected data' });
});

app.listen(3000);

Client-side:

fetch('https://api.example.com/data', {
  headers: {
    'X-API-Key': 'key_abc123xyz'
  }
})
.then(response => response.json())
.then(data => console.log(data));

Pros:

  • Simple to implement
  • Good for server-to-server communication
  • Easy to revoke individual keys

Cons:

  • Keys can be exposed in URLs or logs
  • No expiration mechanism
  • Limited granularity in permissions

2. Basic Authentication #

Basic Auth sends username and password with each request, encoded in Base64.

Format:

Authorization: Basic base64(username:password)

Implementation:

// Server-side
const express = require('express');
const app = express();

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

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

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

  // Validate credentials (use bcrypt in production)
  if (username === 'admin' && password === 'secret') {
    next();
  } else {
    res.setHeader('WWW-Authenticate', 'Basic');
    return res.status(401).json({ error: 'Invalid credentials' });
  }
}

app.use(basicAuth);

app.get('/api/data', (req, res) => {
  res.json({ message: 'Protected data' });
});

Client-side:

const username = 'admin';
const password = 'secret';
const credentials = btoa(`${username}:${password}`);

fetch('https://api.example.com/data', {
  headers: {
    'Authorization': `Basic ${credentials}`
  }
})
.then(response => response.json())
.then(data => console.log(data));

Pros:

  • Simple and widely supported
  • Built into HTTP standard

Cons:

  • Credentials sent with every request
  • Must use HTTPS (Base64 is not encryption)
  • No logout mechanism
  • Credentials cached by browser

3. Bearer Token Authentication #

Bearer tokens are access tokens sent in the Authorization header.

Format:

Authorization: Bearer <token>

Implementation:

// Server-side
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

app.use(express.json());

const SECRET_KEY = 'your-secret-key';

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

  // Validate credentials (use database in production)
  if (username === 'admin' && password === 'secret') {
    const token = jwt.sign(
      { username, userId: 123 },
      SECRET_KEY,
      { expiresIn: '1h' }
    );

    return res.json({ token });
  }

  res.status(401).json({ error: 'Invalid credentials' });
});

// Authentication middleware
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

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

  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid token' });
    }

    req.user = user;
    next();
  });
}

app.get('/api/data', authenticateToken, (req, res) => {
  res.json({
    message: 'Protected data',
    user: req.user
  });
});

Client-side:

// Login
const loginResponse = await fetch('https://api.example.com/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: 'admin',
    password: 'secret'
  })
});

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

// Use token for subsequent requests
const dataResponse = await fetch('https://api.example.com/data', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

const data = await dataResponse.json();

Pros:

  • Stateless authentication
  • Tokens can contain claims/metadata
  • Can have expiration times
  • More secure than API keys

Cons:

  • Token management required
  • Tokens can be stolen if not secured
  • Revocation requires additional infrastructure

4. JWT (JSON Web Tokens) #

JWTs are a specific type of bearer token that contain encoded JSON data.

JWT Structure:

header.payload.signature

Creating JWTs:

const jwt = require('jsonwebtoken');

const payload = {
  userId: 123,
  username: 'john',
  role: 'admin'
};

const token = jwt.sign(payload, 'secret-key', {
  expiresIn: '24h',
  issuer: 'myapp',
  audience: 'api'
});

console.log(token);

Verifying JWTs:

const jwt = require('jsonwebtoken');

function verifyToken(req, res, next) {
  const token = req.headers['authorization']?.split(' ')[1];

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

  try {
    const decoded = jwt.verify(token, 'secret-key');
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(403).json({ error: 'Invalid token' });
  }
}

Refresh Tokens:

app.post('/login', (req, res) => {
  const { username, password } = req.body;

  // Validate credentials
  if (username === 'admin' && password === 'secret') {
    const accessToken = jwt.sign(
      { username, userId: 123 },
      ACCESS_SECRET,
      { expiresIn: '15m' }
    );

    const refreshToken = jwt.sign(
      { username, userId: 123 },
      REFRESH_SECRET,
      { expiresIn: '7d' }
    );

    // Store refresh token in database
    return res.json({ accessToken, refreshToken });
  }

  res.status(401).json({ error: 'Invalid credentials' });
});

app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;

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

    const newAccessToken = jwt.sign(
      { username: decoded.username, userId: decoded.userId },
      ACCESS_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(403).json({ error: 'Invalid refresh token' });
  }
});

5. OAuth 2.0 #

OAuth 2.0 is an authorization framework commonly used for third-party authentication.

Common Flow (Authorization Code):

  1. Client redirects user to authorization server
  2. User authenticates and grants permission
  3. Authorization server redirects back with authorization code
  4. Client exchanges code for access token
  5. Client uses access token to access protected resources

Example with Google OAuth:

const express = require('express');
const axios = require('axios');
const app = express();

const CLIENT_ID = 'your-client-id';
const CLIENT_SECRET = 'your-client-secret';
const REDIRECT_URI = 'http://localhost:3000/callback';

// Step 1: Redirect to Google
app.get('/auth/google', (req, res) => {
  const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
    `client_id=${CLIENT_ID}&` +
    `redirect_uri=${REDIRECT_URI}&` +
    `response_type=code&` +
    `scope=openid email profile`;

  res.redirect(authUrl);
});

// Step 2: Handle callback
app.get('/callback', async (req, res) => {
  const { code } = req.query;

  try {
    // Exchange code for token
    const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', {
      code,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      redirect_uri: REDIRECT_URI,
      grant_type: 'authorization_code'
    });

    const { access_token } = tokenResponse.data;

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

    res.json(userResponse.data);
  } catch (error) {
    res.status(500).json({ error: 'Authentication failed' });
  }
});

Best Practices #

1. Always Use HTTPS #

Never send credentials or tokens over unencrypted HTTP.

2. Store Secrets Securely #

// Don't hardcode secrets
const SECRET = process.env.JWT_SECRET;

// Use environment variables
require('dotenv').config();

3. Hash Passwords #

const bcrypt = require('bcrypt');

// Hash password
const hashedPassword = await bcrypt.hash(password, 10);

// Verify password
const isValid = await bcrypt.compare(password, hashedPassword);

4. Implement Rate Limiting #

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.use('/api/', limiter);

5. Set Token Expiration #

const token = jwt.sign(payload, secret, { expiresIn: '1h' });

6. Validate Input #

function validateLogin(req, res, next) {
  const { username, password } = req.body;

  if (!username || !password) {
    return res.status(400).json({ error: 'Missing credentials' });
  }

  if (username.length < 3 || password.length < 8) {
    return res.status(400).json({ error: 'Invalid credentials format' });
  }

  next();
}

7. Implement Logout for Tokens #

// Token blacklist (use Redis in production)
const blacklist = new Set();

app.post('/logout', authenticateToken, (req, res) => {
  const token = req.headers['authorization'].split(' ')[1];
  blacklist.add(token);
  res.json({ message: 'Logged out successfully' });
});

function authenticateToken(req, res, next) {
  const token = req.headers['authorization']?.split(' ')[1];

  if (blacklist.has(token)) {
    return res.status(401).json({ error: 'Token revoked' });
  }

  // Continue with verification
}

Which Method to Choose? #

API Keys:

  • Simple internal APIs
  • Server-to-server communication
  • When you control both client and server

Basic Auth:

  • Internal tools and admin panels
  • Simple applications
  • Development and testing

Bearer Tokens/JWT:

  • Modern web applications
  • Mobile apps
  • Single-page applications (SPAs)

OAuth 2.0:

  • Third-party integrations
  • “Login with Google/Facebook”
  • When you need delegated access

Proper authentication is essential for API security. Choose the method that best fits your use case and always follow security best practices.