API Authentication Methods: API Keys, Bearer Tokens, and OAuth Compared
When to use API keys, when Bearer tokens are correct, how OAuth 2.0 flows actually work, and how to avoid the authentication mistakes that cause breaches.
Authentication vs authorization: the distinction that matters
Before comparing specific mechanisms, the distinction between authentication and authorization is worth making explicit, because conflating them leads to subtle design errors. Authenticationanswers "who are you?" — it establishes the identity of the caller. Authorizationanswers "what are you allowed to do?" — it determines what the identified caller can access.
API keys, Bearer tokens, and OAuth are all primarily authentication mechanisms. They prove identity. What that identity can access is a separate concern determined by your authorization layer — permissions, scopes, roles, or access control lists attached to the verified identity. A valid API key tells you which application is making the request; it does not, by itself, tell you what that application should be permitted to do.
API keys: the simplest credential
An API key is an opaque string — typically a random 32-64 byte value encoded as hex or base58 — that a client sends with every request. The server looks up the key in its database, identifies the associated account or application, and either allows or denies the request. That is the entire model.
API keys are the right choice for server-to-server communication where one system calls another, the key is stored securely on the server (never in client-side code or version control), and the caller is a known, trusted application rather than a human user. They are used by virtually every API platform — OpenAI, Stripe, Twilio, SendGrid — for this exact use case. The simplicity is the point: one string, stored securely, sent with every request.
Standard ways to send an API key
# Authorization header (preferred — not logged by default) curl -H "Authorization: Bearer sk_live_abc123..." https://api.example.com/data # Custom header (common for some APIs) curl -H "X-API-Key: sk_live_abc123..." https://api.example.com/data # Query parameter (avoid — keys appear in server logs and browser history) curl "https://api.example.com/data?api_key=sk_live_abc123" # bad practice
The main vulnerability of API keys is that they do not expire by default and grant long-lived access. If a key is leaked — through a git commit, a log file, a Slack message, or a compromised server — it remains valid until explicitly revoked. Mitigation: rotate keys periodically, restrict keys to specific IP addresses or referrer domains where possible, use separate keys per environment (development, staging, production), and set up monitoring for unusual usage patterns.
Bearer tokens: short-lived, scoped credentials
A Bearer token is sent in the Authorization: Bearer <token> HTTP header. The format of the token itself is not specified by "Bearer" — it can be an opaque random string (like an API key) or a self-contained signed token like a JWT. What defines Bearer semantics is the pattern: present the token, get access. No additional proof of identity is required beyond possession of the token (hence "Bearer" — whoever bears it can use it).
The key difference from plain API keys in practice is that Bearer tokens are typically short-lived. They are issued after authentication, carry an expiry time, and must be refreshed or replaced when they expire. This limits the damage from a leaked token: an attacker who intercepts a Bearer token valid for 15 minutes has 15 minutes of access, not indefinite access. The corresponding cost is that the system must handle token refresh — either transparently in the client or through user re-authentication.
JWTs as Bearer tokens: self-contained credentials
A JSON Web Token (JWT) is a specific format for a Bearer token that encodes claims — user ID, roles, expiry time, issuer — directly in the token, signed by the server with a secret or private key. When a service receives a JWT, it can verify the signature and read the claims without making a database lookup. This makes JWTs attractive for distributed systems and microservices: any service that knows the signing key can independently verify a JWT.
The tradeoff is that JWTs cannot be revoked before expiry without additional infrastructure. An opaque Bearer token stored in a database can be deleted from that database to immediately revoke access. A JWT remains valid until its expiry timestamp, even if the user logs out or is suspended — unless you maintain a token blocklist, which reintroduces the database lookup you were trying to avoid.
Anatomy of a JWT
// A JWT has three base64url-encoded parts separated by dots
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 // header
.eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTc1MDAwMDAwMH0 // payload
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c // signature
// Decoded payload
{
"sub": "user_123",
"email": "user@example.com",
"role": "admin",
"exp": 1750000000 // Unix timestamp — token expires at this time
}
// The signature is computed as:
// HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
// If the payload is tampered with, the signature no longer matchesOAuth 2.0: delegated access
OAuth 2.0 is not an authentication protocol — it is an authorization framework for delegated access. The classic use case: a user grants your application permission to read their Google Calendar without giving your application their Google password. OAuth allows the user to authorize a specific set of scopes (permissions) for a specific application, and that application receives a token limited to those scopes on behalf of that user.
The most common OAuth 2.0 flow for web applications is the Authorization Code flow. The user is redirected to the authorization server (Google, GitHub, etc.), approves access, and the authorization server redirects back to your application with an authorization code. Your server exchanges that code for an access token using its client secret. The user's credentials never touch your server — and if your server is compromised, the attacker only gets tokens with the scopes the user approved, not the user's actual password.
OAuth 2.0 Authorization Code flow, step by step
1. Your app redirects the user to the auth server:
GET https://auth.example.com/oauth/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=read:calendar write:calendar
&response_type=code
&state=RANDOM_CSRF_TOKEN // must validate this on return
2. User logs in and approves access.
3. Auth server redirects back with authorization code:
GET https://yourapp.com/callback?code=AUTH_CODE&state=RANDOM_CSRF_TOKEN
4. Your server exchanges the code for a token (server-side, with client_secret):
POST https://auth.example.com/oauth/token
client_id=YOUR_CLIENT_ID
client_secret=YOUR_CLIENT_SECRET
code=AUTH_CODE
grant_type=authorization_code
redirect_uri=https://yourapp.com/callback
5. Auth server returns access token (and usually a refresh token):
{ "access_token": "...", "expires_in": 3600, "refresh_token": "..." }
6. Your server uses the access token to call the API on behalf of the user.Choosing the right method
Use API keys when your caller is a trusted server-side application you control, keys can be stored securely in environment variables, and you want simplicity without token refresh complexity. Examples: internal microservice calls, third-party API integrations (Stripe, Twilio) where your backend calls their API.
Use short-lived Bearer tokens (opaque or JWT) when the caller is a user session in a web or mobile application, you want tokens to expire automatically, or you need to support token revocation. Short expiry (15 min to 1 hour) with silent refresh via refresh tokens is the standard pattern.
Use OAuth 2.0 when you need delegated access — allowing one application to act on behalf of a user in another system — or when you are building a platform that other applications will integrate with. If you are adding "Login with Google" or "Connect to GitHub" to your app, you are implementing OAuth as a client. If you are building an API that third-party apps will call on behalf of your users, you are implementing OAuth as a provider.
Security mistakes common to all methods
Sending credentials in query parameters. Query strings appear in server logs, browser history, and HTTP Referer headers. Always use request headers for credentials.
Storing secrets in client-side code. API keys, client secrets, and symmetric JWT signing keys must never appear in JavaScript that runs in the browser or in mobile app binaries. They can be extracted by anyone who inspects the code. Secrets belong on the server.
Not validating the OAuth state parameter. The state parameter in OAuth prevents cross-site request forgery attacks. If you skip generating and validating a random state value, an attacker can trick your application into completing an OAuth flow with their authorization code, potentially linking their account to your victim's session.
Logging full request headers. If your application logs the Authorization header, credentials appear in log files. Either redact the header in your logging middleware or log only the first and last few characters for debugging purposes.