Choosing the right API authentication method is critical for security, usability, and scalability. This guide compares major authentication approaches—API keys, OAuth 2.0, JWT bearer tokens, Basic Auth, and mTLS—with clear recommendations for when to use each.
Authentication Methods Overview
┌─────────────────────────────────────────────────────────────────┐
│ API AUTHENTICATION SPECTRUM │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Simple ◄─────────────────────────────────────────────► Complex │
│ Low Security High Security │
│ │
│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ Basic │ │ API │ │ JWT │ │ OAuth │ │ mTLS │ │
│ │ Auth │ │ Keys │ │Bearer │ │ 2.0 │ │ │ │
│ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘ │
│ │
│ Features added as complexity increases: │
│ • Expiration ─────────────────────────────────────► │
│ • Scopes/Permissions ─────────────────────────────► │
│ • User Delegation ────────────────────────────────► │
│ • Revocation ─────────────────────────────────────► │
│ • Cryptographic Identity ─────────────────────────► │
│ │
└─────────────────────────────────────────────────────────────────┘
Quick Comparison Table
| Method | Complexity | Security | User Context | Expiration | Best For |
|---|---|---|---|---|---|
| Basic Auth | Low | Low | Optional | No | Dev/testing, simple internal APIs |
| API Keys | Low | Medium | No | Optional | Server-to-server, public APIs |
| JWT Bearer | Medium | Medium-High | Yes | Yes | Stateless APIs, microservices |
| OAuth 2.0 | High | High | Yes | Yes | User-authorized access, 3rd-party apps |
| mTLS | High | Very High | Service | Cert-based | Zero-trust, service mesh |
Decision Tree
What are you building?
│
├─► Internal tool / Development only
│ └─► Basic Auth or API Keys (keep it simple)
│
├─► Public API for developers
│ │
│ ├─► Developers access their own data
│ │ └─► API Keys (simple, familiar)
│ │
│ └─► Developers access user data
│ └─► OAuth 2.0 (user authorization required)
│
├─► Mobile or Single-Page App
│ │
│ └─► Users log in to access their data
│ └─► OAuth 2.0 + PKCE → JWT Bearer
│
├─► Microservices / Service-to-Service
│ │
│ ├─► Internal, trusted network
│ │ └─► JWT Bearer or API Keys
│ │
│ └─► Zero-trust / High security
│ └─► mTLS + Optional JWT for user context
│
└─► Partner Integration
│
├─► Fixed, known partners
│ └─► API Keys (with IP allowlisting)
│
└─► Partners accessing user data
└─► OAuth 2.0
Method 1: Basic Authentication
How It Works
┌──────────────────────────────────────────────────────────────┐
│ BASIC AUTHENTICATION │
├──────────────────────────────────────────────────────────────┤
│ │
│ Credentials: username:password │
│ Encoding: Base64 (NOT encryption!) │
│ │
│ Request: │
│ GET /api/resource HTTP/1.1 │
│ Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= │
│ ▲ │
│ │ │
│ base64("username:password") │
│ │
└──────────────────────────────────────────────────────────────┘
Implementation
// Client: Making authenticated request
const credentials = btoa(`${username}:${password}`);
fetch('/api/data', {
headers: {
'Authorization': `Basic ${credentials}`
}
});
// Server: Validating Basic Auth
function basicAuthMiddleware(req, res, next) {
const auth = req.headers.authorization;
if (!auth?.startsWith('Basic ')) {
return res.status(401).set('WWW-Authenticate', 'Basic').send();
}
const credentials = Buffer.from(auth.slice(6), 'base64').toString();
const [username, password] = credentials.split(':');
if (validateCredentials(username, password)) {
req.user = { username };
next();
} else {
res.status(401).send('Invalid credentials');
}
}
When to Use
Good for:
- Development and testing environments
- Simple internal tools
- CLI applications with user prompts
- When simplicity outweighs security needs
Avoid for:
- Production public APIs
- Browser-based applications (credentials exposed)
- Anywhere without HTTPS
- When you need token expiration or scopes
Method 2: API Keys
How It Works
┌──────────────────────────────────────────────────────────────┐
│ API KEY AUTH │
├──────────────────────────────────────────────────────────────┤
│ │
│ Key Format: Random string (32-64 characters) │
│ Example: sk_live_abc123xyz789... │
│ │
│ Common Placements: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Header: X-API-Key: sk_live_abc123xyz789... │ │
│ │ Header: Authorization: ApiKey sk_live_abc123... │ │
│ │ Query: ?api_key=sk_live_abc123... (avoid!) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Identifies: Application (not user) │
│ Contains: No embedded data (opaque token) │
│ │
└──────────────────────────────────────────────────────────────┘
Implementation
// Server: Generate API key
const crypto = require('crypto');
function generateApiKey(prefix = 'sk_live') {
const randomPart = crypto.randomBytes(32).toString('hex');
return `${prefix}_${randomPart}`;
}
// Server: Store hashed key (never store plaintext!)
async function createApiKey(clientId) {
const key = generateApiKey();
const hashedKey = crypto.createHash('sha256').update(key).digest('hex');
await db.query(
'INSERT INTO api_keys (client_id, key_hash, created_at) VALUES ($1, $2, NOW())',
[clientId, hashedKey]
);
return key; // Return to client ONCE, never stored in plaintext
}
// Server: Validate API key
async function validateApiKey(req, res, next) {
const key = req.headers['x-api-key'];
if (!key) {
return res.status(401).json({ error: 'API key required' });
}
const hashedKey = crypto.createHash('sha256').update(key).digest('hex');
const result = await db.query(
'SELECT client_id, scopes FROM api_keys WHERE key_hash = $1 AND status = $2',
[hashedKey, 'active']
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid API key' });
}
req.client = result.rows[0];
next();
}
Best Practices
| Practice | Implementation |
|---|---|
| Use prefixes | sk_live_, sk_test_, pk_ for public keys |
| Hash in database | SHA-256 hash, never store plaintext |
| Environment separation | Different keys for dev/staging/prod |
| Scoped permissions | read:users, write:orders per key |
| Rotation support | Multiple active keys during transition |
| Logging | Track usage, detect anomalies |
When to Use
Good for:
- Server-to-server API calls
- Third-party developer integrations
- Simple rate limiting and tracking
- When user context isn't needed
Avoid for:
- Browser/mobile apps (can't keep secret)
- User-specific data access
- When you need fine-grained permissions per user
Method 3: JWT Bearer Tokens
How It Works
┌──────────────────────────────────────────────────────────────┐
│ JWT BEARER AUTH │
├──────────────────────────────────────────────────────────────┤
│ │
│ Token Format: header.payload.signature │
│ Contains: Embedded claims (user, expiry, scopes) │
│ │
│ Request: │
│ GET /api/resource HTTP/1.1 │
│ Authorization: Bearer eyJhbGciOiJSUzI1NiIs... │
│ │
│ Validation: Verify signature + check claims │
│ Stateless: No server-side session lookup needed │
│ │
└──────────────────────────────────────────────────────────────┘
Implementation
const jwt = require('jsonwebtoken');
// Issue JWT
function issueToken(user) {
return jwt.sign(
{
sub: user.id,
email: user.email,
roles: user.roles,
scopes: ['read:profile', 'write:profile']
},
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
}
);
}
// Validate JWT middleware
function jwtMiddleware(req, res, next) {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Bearer token required' });
}
const token = auth.slice(7);
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
});
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
When to Use
Good for:
- Stateless REST APIs
- Microservices (each can validate independently)
- Mobile applications
- Cross-domain authentication
Avoid for:
- When you need instant revocation (JWTs are valid until expiry)
- Long-lived sessions without refresh tokens
- When token size is a concern (JWTs are larger than session IDs)
See JWT Security Best Practices for detailed guidance.
Method 4: OAuth 2.0
How It Works
┌──────────────────────────────────────────────────────────────┐
│ OAUTH 2.0 FLOW │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌──────┐ 1. Auth Request ┌──────────────┐ │
│ │ User │ ───────────────────► │ Auth │ │
│ └──────┘ │ Server │ │
│ ▲ └──────────────┘ │
│ │ │ │
│ │ 2. User consents │ │
│ │ to scopes │ │
│ │ ▼ │
│ ┌──────┐ 3. Auth Code ┌──────────────┐ │
│ │ App │ ◄─────────────────── │ Callback │ │
│ └──────┘ └──────────────┘ │
│ │ │
│ │ 4. Exchange code │
│ │ for tokens │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Access Token │ ───────────► │ Resource │ │
│ │ + Refresh │ 5. API │ Server │ │
│ └──────────────┘ Request └──────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Key Concepts
| Concept | Description |
|---|---|
| Scopes | Permissions granted (read:email, write:repos) |
| Access Token | Short-lived credential for API calls |
| Refresh Token | Long-lived credential to get new access tokens |
| Authorization Code | One-time code exchanged for tokens |
| PKCE | Proof Key for Code Exchange (prevents interception) |
When to Use
Good for:
- Third-party apps accessing user data
- "Login with Google/GitHub/Facebook"
- APIs where users control data sharing
- Mobile and single-page applications
Avoid for:
- Simple server-to-server calls (use Client Credentials or API keys)
- Internal microservices (adds unnecessary complexity)
See OAuth 2.0 & OIDC Implementation Guide for detailed implementation.
Method 5: mTLS (Mutual TLS)
How It Works
┌──────────────────────────────────────────────────────────────┐
│ MUTUAL TLS (mTLS) │
├──────────────────────────────────────────────────────────────┤
│ │
│ Standard TLS: Server proves identity to client │
│ mTLS: BOTH client and server prove identity │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Client │ │ Server │ │
│ │ │ ─── 1. ClientHello ────► │ │ │
│ │ │ ◄── 2. ServerHello ───── │ │ │
│ │ │ ◄── 3. Server Cert ───── │ │ │
│ │ │ ◄── 4. CertRequest ───── │ (new) │ │
│ │ │ ─── 5. Client Cert ────► │ │ │
│ │ │ ─── 6. Verify ─────────► │ │ │
│ │ │ ◄── 7. Verify ────────── │ │ │
│ │ │ ◄──► 8. Encrypted ◄────► │ │ │
│ └──────────┘ └──────────┘ │
│ │
│ Client Certificate identifies the calling service │
│ No shared secrets needed - cryptographic proof │
│ │
└──────────────────────────────────────────────────────────────┘
Implementation (Node.js)
const https = require('https');
const fs = require('fs');
// Server: Require client certificates
const server = https.createServer({
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
ca: fs.readFileSync('client-ca.pem'), // CA that signed client certs
requestCert: true,
rejectUnauthorized: true
}, (req, res) => {
// Client certificate info available
const clientCert = req.socket.getPeerCertificate();
console.log('Client:', clientCert.subject.CN);
res.end('Authenticated!');
});
// Client: Present certificate
const options = {
hostname: 'api.example.com',
port: 443,
path: '/data',
method: 'GET',
key: fs.readFileSync('client-key.pem'),
cert: fs.readFileSync('client-cert.pem'),
ca: fs.readFileSync('server-ca.pem')
};
https.request(options, (res) => {
// Handle response
});
When to Use
Good for:
- Zero-trust architectures
- Service mesh (Istio, Linkerd)
- High-security financial/healthcare APIs
- Compliance requirements (PCI-DSS, HIPAA)
Avoid for:
- Public APIs (certificate distribution is complex)
- Browser clients (certificate management UX is poor)
- When simpler methods suffice
Combining Authentication Methods
Many production systems layer multiple methods:
┌─────────────────────────────────────────────────────────────────┐
│ LAYERED AUTHENTICATION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: Transport Security │
│ └─► mTLS (service identity) or TLS (encryption) │
│ │
│ Layer 2: Application Identity │
│ └─► API Key (which app is calling?) │
│ │
│ Layer 3: User Identity │
│ └─► JWT/OAuth token (which user, what permissions?) │
│ │
│ Example Request: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ TLS: Client cert = service-a │ │
│ │ Header: X-API-Key: sk_live_abc123... │ │
│ │ Header: Authorization: Bearer eyJhbGc... │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Security Comparison Summary
| Aspect | Basic | API Key | JWT | OAuth | mTLS |
|---|---|---|---|---|---|
| Credentials in request | Every request | Every request | Every request | Every request | TLS handshake |
| Secret storage client | Required | Required | Token only | Token only | Cert file |
| Replay protection | None | None | exp claim | exp claim | TLS |
| Revocation | Immediate | Immediate | Expiry-based | Immediate | CRL/OCSP |
| User context | Optional | No | Yes | Yes | Service only |
| Delegation | No | No | No | Yes | No |
Next Steps
- OAuth 2.0 & OIDC Guide - Implement OAuth flows
- JWT Security Best Practices - Secure token handling
- API Security Complete Guide - Comprehensive security overview

