API Authentication Guide: API Keys, OAuth 2.0, JWT & Bearer Tokens
Securing an API isn't just about throwing a password on an endpoint. If you choose the wrong auth method, you either build a fortress nobody can enter, or a house of cards waiting to be exploited. This guide breaks down exactly how to authenticate API requests, explaining the mechanics, pros, and cons of every major approach.
1. Basic Authentication
The oldest and simplest of the API authentication methods. The client sends a username and password with every single request, encoded in Base64.
How it works
The client concatenates username:password, encodes it in Base64, and sends it in the Authorization header.
curl -X GET "https://api.example.com/data" \
-H "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ="
import requests
from requests.auth import HTTPBasicAuth
response = requests.get(
'https://api.example.com/data',
auth=HTTPBasicAuth('username', 'password')
)
const headers = new Headers();
headers.append('Authorization', 'Basic ' + btoa('username:password'));
fetch('https://api.example.com/data', { headers })
.then(res => res.json());
When to use it
- Internal tools behind a secure VPN.
- Legacy systems that support nothing else.
- Simple scripts where setting up OAuth is overkill (but prefer API keys).
2. API Keys
An API key is a long, opaque string (like a password) generated by the server and given to a developer. It's designed to identify the project or the developer, not a specific end-user.
How it works
The server generates a unique string (e.g., pk_live_51H...). The client includes this string in requests. Best practice is to pass it via a custom header (like X-API-Key), though some APIs allow it in the query string.
curl -X GET "https://api.example.com/data" \
-H "X-API-Key: abcdef1234567890"
import requests
headers = { 'X-API-Key': 'abcdef1234567890' }
response = requests.get('https://api.example.com/data', headers=headers)
const headers = new Headers();
headers.append('X-API-Key', 'abcdef1234567890');
fetch('https://api.example.com/data', { headers })
.then(res => res.json());
API Key vs Bearer Token
A frequent point of confusion is the api key vs bearer token debate. An API key is a static secret meant to identify a client application (machine-to-machine). A Bearer token is a temporary token, usually obtained after a user logs in, meant to identify a specific user's session.
Security Considerations
- Do not commit them to GitHub. Use environment variables.
- Never expose API keys in frontend JavaScript (unless they are explicitly designed as public, "publishable" keys with restricted domains).
- They rarely expire automatically, meaning if they are leaked, they must be manually revoked.
3. Bearer Tokens
A Bearer Token is a string that explicitly says "grant access to the bearer of this token." It is not tied to the client's identity; whoever holds the token can use it.
How it works
The token is passed in the standardized Authorization header using the Bearer schema.
curl -X GET "https://api.example.com/user" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
import requests
headers = { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...' }
response = requests.get('https://api.example.com/user', headers=headers)
const headers = new Headers();
headers.append('Authorization', 'Bearer eyJhbGciOiJIUzI1NiIs...');
fetch('https://api.example.com/user', { headers })
.then(res => res.json());
Bearer tokens are typically the output of another process (like an OAuth flow or a login request). They are heavily used in modern web and mobile apps.
4. JWT (JSON Web Tokens)
JWT (pronounced "jot") is a specific format for a token, very often used as a Bearer token. Unlike opaque tokens (which are just random strings that the server must look up in a database), a JWT contains a verifiable payload.
How it works
A JWT consists of three base64url-encoded parts separated by dots: Header.Payload.Signature.
- Header: Specifies the algorithm (e.g., HMAC SHA256 or RSA).
- Payload (Claims): Contains the actual data (e.g., user ID, expiration time).
- Signature: Used to verify that the sender of the JWT is who it says it is, and the payload hasn't been tampered with.
curl -X GET "https://api.example.com/data" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1..."
import jwt
# Verifying a JWT in Python
try:
decoded = jwt.decode(token, 'secret_key', algorithms=["HS256"])
print(decoded['userId'])
except jwt.ExpiredSignatureError:
print("Token expired")
const jwt = require('jsonwebtoken');
// Verifying a JWT on the server
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log("User ID:", decoded.userId);
} catch(err) {
console.log("Invalid or expired token");
}
Why use JWT?
Statelessness. Because the server can verify the signature using its secret key, it doesn't need to query a database to check if the session is valid. This makes scaling APIs horizontally much easier.
5. OAuth 2.0 Explained
OAuth 2.0 is not an authentication protocol; it is a delegated authorization framework. It allows an application to access data on behalf of a user without needing the user's password.
If you've ever clicked "Log in with Google" or allowed a service to post to your Twitter account, you've used OAuth 2.0.
The 4 Grant Types
OAuth 2.0 defines different "flows" (grants) depending on the type of application making the request.
| Grant Type | Use Case | How it works |
|---|---|---|
| Authorization Code | Web Apps (Server-side) | User redirects to auth server, logs in, redirects back with a code. Server exchanges code for a token securely. |
| Client Credentials | Machine-to-Machine | No user involved. Two servers talk to each other using a Client ID and Secret to get a token. |
| Implicit | SPAs (Legacy) | Deprecated. Token returned directly in URL fragment. Replaced by Auth Code + PKCE. |
| Resource Owner Password | First-party Apps | Deprecated/Discouraged. App directly collects username/password to get a token. |
Client Credentials Flow Example
curl -X POST "https://auth.example.com/oauth/token" \
-d "grant_type=client_credentials" \
-u "client_id:client_secret"
import requests
data = {'grant_type': 'client_credentials'}
response = requests.post(
'https://auth.example.com/oauth/token',
data=data,
auth=('client_id', 'client_secret')
)
const body = new URLSearchParams();
body.append('grant_type', 'client_credentials');
fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: {
'Authorization': 'Basic ' + btoa('client_id:client_secret'),
'Content-Type': 'application/x-www-form-urlencoded'
},
body: body
}).then(res => res.json());
6. HMAC (Hash-based Message Authentication Code)
HMAC is highly secure and used by APIs dealing with financial transactions (like Stripe webhooks or AWS). It guarantees both the identity of the sender and the integrity of the payload.
How it works
Instead of sending the secret key over the wire, the client uses the secret key to create a cryptographic hash of the request body and timestamp. The server, holding the same secret key, independently generates the hash. If the hashes match, the request is authentic and hasn't been altered.
# Often pre-computed or generated in a script wrapping curl
curl -X POST "https://api.example.com/webhook" \
-H "X-Signature: c4b9..." \
-d '{"data": "value"}'
import hmac
import hashlib
payload = b'{"data": "value"}'
secret = b'my_secret_key'
signature = hmac.new(secret, payload, hashlib.sha256).hexdigest()
# Send signature in header 'X-Signature: signature'
const crypto = require('crypto');
const payload = '{"data": "value"}';
const secret = 'my_secret_key';
const signature = crypto.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Send signature in header 'X-Signature: signature'
Decision Tree: Which method should you use?
Frequently Asked Questions
What are the most common api authentication methods?
The most common API authentication methods include Basic Authentication (username/password), API Keys (a single static token), Bearer Tokens (dynamic tokens passed in the Authorization header), OAuth 2.0 (a delegated authorization framework), and JWT (JSON Web Tokens, a specific stateless token format often used as a Bearer token).
How is oauth 2.0 explained simply?
OAuth 2.0 is like a hotel keycard system. Instead of giving a valet your car keys (your actual password), you give them a temporary, limited-access pass (an access token). It allows third-party applications to access a user's data without ever seeing their actual password, by having the user log in directly with the provider (like Google or GitHub) who then issues the token.
What is the difference between an api key vs bearer token?
An API key is typically a long-lived, static string used to identify the project or developer making the request, often passed in a URL parameter or custom header. A Bearer token is usually short-lived, dynamic (often generated upon login via OAuth or a similar flow), identifies a specific user, and is strictly passed in the HTTP 'Authorization: Bearer <token>' header.
How to authenticate api requests securely?
To authenticate API requests securely, always use HTTPS to encrypt traffic. Avoid placing tokens or API keys in the URL (query parameters) where they can be logged. Instead, use standard HTTP headers like 'Authorization'. For machine-to-machine communication, use API keys or Client Credentials. For user-facing apps, use OAuth 2.0 or JWT-based Bearer tokens, and implement token expiration and rotation.
When should I use JWT over session cookies for APIs?
Use JWTs when building stateless APIs, mobile applications, or microservices where you don't want to look up a session in a database on every request. Because JWTs contain their own claims and signature, the server can verify them instantly. Use session cookies for traditional monolithic server-side rendered web applications where you need strict control over invalidating sessions instantly.