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.signatureCreating 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):
- Client redirects user to authorization server
- User authenticates and grants permission
- Authorization server redirects back with authorization code
- Client exchanges code for access token
- 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.