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
- Developer registers and receives an API key
- Key is sent with each request (header or query param)
- 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
- Combine username:password
- Encode in Base64
- 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
- User authenticates (login, OAuth, etc.)
- Server issues a bearer token
- Client includes token in requests
- 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
- User authenticates
- Server creates JWT with user data and signs it
- Client stores and sends JWT with requests
- 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: