What Is API Authentication? A guide to OAuth 2.0, JWT, and key methods
API authentication ensures that only authorized requests access protected resources. It’s a mechanism for verifying credentials against predetermined rules to reject unauthorized traffic.
There are many ways to implement API auth, each featuring different security, complexity, and maintainability trade-offs.
In this article, we explore several popular approaches and when they’re most (and least) appropriate to use.
Basic authentication
How it works:
Basic authentication involves sending a username and password in the authorization header as a Base64-encoded string.
This method is widely supported and easy to configure, though it depends on transport-layer encryption (HTTPS) to avoid exposing credentials and lacks built-in expiration or rotation features.
Example (Node.js with Axios):
const axios = require('axios');
async function basicAuthRequest() {
const username = 'myUser';
const password = 'myPassword';
const token = Buffer.from(`${username}:${password}`).toString('base64');
try {
const response = await axios.get('https://api.example.com/protected', {
headers: {
Authorization: `Basic ${token}`,
},
});
console.log(response.data);
} catch (error) {
console.error('Request failed:', error);
}
}
When to use:
- Local development or simple internal services where security risks and complexity are minimal.
- Quick testing or prototyping scenarios that do not require advanced token management.
When to avoid:
- Production-grade or public-facing APIs where stolen credentials remain indefinitely valid without rotation.
- Scalable or high-security systems that need granular access control or frequent credential expiration.
API keys
How it works:
API keys act as simple shared secrets transmitted with each request, commonly sent via a custom header. They can be rotated or invalidated when needed; however, they do not natively enforce fine-grained scopes or expiration.
Example (Node.js with Axios):
const axios = require('axios');
async function apiKeyRequest() {
const apiKey = 'YOUR_API_KEY';
try {
const response = await axios.get('https://api.example.com/protected', {
headers: {
'X-API-Key': apiKey,
},
});
console.log(response.data);
} catch (error) {
console.error('Request failed:', error);
}
}
When to use:
- Service-to-service communication within a controlled environment.
- Public APIs with basic access controls where rotating the key is acceptable when leaks occur.
- Legacy systems that lack more advanced token mechanisms.
When to avoid:
- Use cases needing granular permission levels (e.g., user-specific scopes).
- High-security applications where short-lived tokens and robust access policies are mandatory.
Bearer tokens
How it works:
Bearer tokens are short-lived credentials distributed by an identity provider or authentication service.
The client includes the token in an Authorization header using the Bearer scheme. This approach restricts token lifetimes so that a leaked or stolen token eventually expires.
Example (Node.js with Axios):
const axios = require('axios');
async function bearerTokenRequest() {
try {
// Typically obtained from a token endpoint
const authResponse = await axios.post('https://auth.example.com/token', {
username: 'myUser',
password: 'myPassword',
});
const token = authResponse.data.accessToken;
const response = await axios.get('https://api.example.com/protected', {
headers: {
Authorization: `Bearer ${token}`,
},
});
console.log(response.data);
} catch (error) {
console.error('Request failed:', error);
}
}
When to use:
- Modern APIs. Short-lived tokens reduce the window of vulnerability if compromised.
- Systems have already integrated with an identity provider to generate Bearer tokens.
When to avoid:
- Environments without a trusted identity provider to issue and verify tokens.
- Scenarios where continuous token refresh might be problematic (e.g., extremely resource-constrained systems).
OAuth 2.0
How it works:
OAuth 2.0 is a framework offering a structured approach to token-based authorization. Combined with the complimentary OpenID Connect (OIDC) protocol, it can also return identity information about logged-in users.
It is commonly used for delegated authorization, enabling applications to access user data without storing passwords. Though flexible, OAuth 2.0 adds complexity, as each flow (e.g., Authorization Code or Client Credentials) targets specific use cases.
Example (Node.js with Axios, client credentials flow):
const axios = require('axios');
async function oauth2ClientCredentials() {
const clientId = 'YOUR_CLIENT_ID';
const clientSecret = 'YOUR_CLIENT_SECRET';
try {
// Retrieve token via Client Credentials flow
const tokenResponse = await axios.post('https://oauth.example.com/token', null, {
params: {
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
},
});
const token = tokenResponse.data.access_token;
// Send the token to the API
const response = await axios.get('https://api.example.com/protected', {
headers: {
Authorization: `Bearer ${token}`,
},
});
console.log(response.data);
} catch (error) {
console.error('Request failed:', error);
}
}
When to use:
- Third-party integration scenarios requiring delegated or granular scopes (e.g., logging in via Google, GitHub, etc.).
- Enterprise-grade applications where a robust identity and access management (IAM) system is necessary.
- User-based access with fine-grained permissions and consent flows.
When to avoid:
- Simpler internal services that do not warrant the overhead of managing OAuth flows.
- Single-purpose scripts or prototypes where the complexity of OAuth is overkill.
JSON Web Tokens (JWT)
How it works:
JWTs are self-contained tokens that incorporate authentication and authorization claims within an encoded structure, removing the need for server-side sessions.
They are frequently used with OAuth 2.0 flows or as standalone tokens. Still, invalidating an active JWT without short expiration intervals or a revocation list can be difficult.
Example (Node.js):
const jwt = require('jsonwebtoken');
function generateJwt() {
const secretKey = 'MY_SUPER_SECRET_KEY';
const payload = { userId: 123, role: 'admin' };
// Tokens typically expire, e.g., in one hour
return jwt.sign(payload, secretKey, { expiresIn: '1h' });
}
function verifyJwt(token) {
const secretKey = 'MY_SUPER_SECRET_KEY';
try {
const decoded = jwt.verify(token, secretKey);
console.log('Decoded payload:', decoded);
} catch (error) {
console.error('JWT validation failed:', error.message);
}
}
When to use:
- Stateless, distributed systems that benefit from embedding claims directly in tokens.
- High-performance APIs where storing and verifying sessions on the server side is undesirable.
- Single Page Applications (SPAs) or mobile clients that rely on local token storage.
When to avoid:
- Applications requiring instant token revocation (e.g., you must immediately lock out compromised tokens).
- High-security contexts without strong token rotation policies and robust claim validation.
HMAC signatures
How it works:
HMAC-based authentication calculates a cryptographic hash (e.g., using SHA-256) by combining the request data with a shared secret. This hash is sent to the server for verification, confirming the request’s authenticity and integrity.
const axios = require('axios');
const crypto = require('crypto');
async function hmacSignedRequest(url, payload, secret) {
const payloadString = JSON.stringify(payload);
const signature = crypto
.createHmac('sha256', secret)
.update(payloadString)
.digest('hex');
try {
const response = await axios.post(url, payload, {
headers: {
'X-Signature': signature,
},
});
console.log(response.data);
} catch (error) {
console.error('Request failed:', error);
}
}
When to use:
- Tamper-proofing data in high-integrity or regulated environments (e.g., banking or financial systems).
- Server-to-server communication where both parties share a well-protected secret key.
- Use cases requiring cryptographic verification of the message body or headers.
When to avoid:
- Complex client-side scenarios where securely managing the secret is difficult.
- Publicly consumed APIs where the HMAC secret cannot be securely distributed to diverse clients.
Session cookies
How it works:
Session-based authentication is prevalent in traditional web applications. The server maintains session data keyed by an identifier, which is sent to the client as a cookie.
Although straightforward to implement using various Node.js frameworks, it does not inherently align with stateless REST principles.
Example (Node.js with Express):
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'MY_SESSION_SECRET',
resave: false,
saveUninitialized: false,
}));
app.get('/', (req, res) => {
req.session.userId = 123;
res.send('Session established.');
});
app.get('/protected', (req, res) => {
if (req.session.userId) {
res.send(`Hello user ${req.session.userId}`);
} else {
res.status(401).send('Unauthorized');
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
When to use:
- Traditional server-rendered web apps where storing session state on the server is acceptable.
- Applications that need features like flash messages or multi-step forms that rely on a server session.
When to avoid:
- Scalable, microservices-based APIs meant to be stateless.
- High-traffic deployments where session synchronization or distribution is complex.
In review
Each approach has unique implications for security, scalability, and user experience:
- Basic Authentication and API keys: Simple to implement and useful for internal or legacy systems, but require strict oversight for credential storage and rotation.
- Bearer Tokens, OAuth 2.0, and JWTs: These provide enhanced flexibility and can be tailored to various modern use cases—especially where short token lifetimes or delegated authorization are required—but they come with additional setup complexity and overhead.
- HMAC Signatures: Offer strong integrity guarantees, making them valuable for tamper-evident or highly sensitive requests. Requires careful key management.
- Session cookies: These are common for browser-based applications but can violate stateless REST principles and add server-side session management overhead.
For all of these patterns, best practices include:
- Using encrypted connections (TLS/HTTPS)
- Frequent credential rotation
- Principle of least privilege
- Secure secret storage
- Auditing authentication logs