Three Choices, One Common Mistake
Walk into any backend code review and you will eventually hear the question: should we use an API key, a JWT, or full OAuth 2.0? The wrong answer costs you data breaches, angry users, or a six-month refactor. The right answer takes five minutes to explain once you understand what each mechanism was actually designed to do.
The confusion is understandable. All three end up as a string in an HTTP Authorization header. All three authenticate requests. But they solve fundamentally different problems. API keys authenticate applications. JWTs carry signed claims about an entity. OAuth 2.0 is a delegation framework, not an authentication protocol at all. Mix them up and you end up building an OAuth flow for a cron job, or shipping long-lived JWTs that never rotate, or exposing an API key in a public mobile app.
This guide cuts through the ambiguity. We will look at each mechanism as senior engineers see them in production at Stripe, GitHub, Slack, and Firebase. You will get working code in Node.js and Python, a security comparison table, a decision tree you can apply in minutes, and concrete patterns for the hybrid setups most real APIs use. By the end you will know exactly which mechanism fits your problem and how to implement it without introducing the classic footguns.
Quick Definitions: What Each One Actually Is
An API key is a shared secret. You generate a random string on the server, give it to a client, and the client sends it back on every request. The server looks it up in a database. That is the entire model. There is no expiration unless you enforce one, no structure, no signature. Stripe uses API keys like sk_live_51H... and they remain valid until you revoke them.
A JSON Web Token (JWT, RFC 7519) is a self-contained, signed payload. It has three base64url-encoded parts separated by dots: header, payload (claims), and signature. The server can verify a JWT using only a key, no database lookup, because the signature guarantees integrity. JWTs typically expire in minutes, are stateless, and carry claims like sub (subject), iat (issued at), and exp (expiration).
OAuth 2.0 (RFC 6749) is not a token format. It is a delegation framework that lets User A grant Application B limited access to their data hosted by Service C, without sharing A's password with B. OAuth defines flows (authorization code, client credentials, device code, PKCE) that produce access tokens. Those access tokens are often JWTs, but they can also be opaque strings. OpenID Connect (OIDC) sits on top of OAuth 2.0 and adds actual authentication via an ID token, which is always a JWT.
How Each Mechanism Works Under the Hood
API keys are the simplest. The client sends the key in a header and the server validates it:
# curl example curl -H "Authorization: Bearer sk_live_abc123" https://api.example.com/v1/charges
Server-side validation in Express looks like this:
// Node.js: API key validation middleware const crypto = require("crypto"); async function validateApiKey(req, res, next) { const header = req.headers.authorization || ""; const key = header.replace(/^Bearer\s+/i, ""); if (!key) return res.status(401).json({ error: "missing key" }); const hash = crypto.createHash("sha256").update(key).digest("hex"); const record = await db.apiKeys.findOne({ hash, revokedAt: null }); if (!record) return res.status(401).json({ error: "invalid key" }); req.account = record.accountId; next(); }
Notice the hash. You never store the raw key at rest. Stripe shows the secret exactly once at creation time; after that only a prefix like sk_live_...XYZ9 is visible.
JWT verification is stateless and cryptographic:
// Node.js: JWT verification const jwt = require("jsonwebtoken"); function verifyJwt(req, res, next) { const token = (req.headers.authorization || "").split(" ")[1]; try { const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY, { algorithms: ["RS256"], issuer: "https://auth.example.com", audience: "api.example.com" }); req.user = payload; next(); } catch (e) { return res.status(401).json({ error: "invalid token" }); } }
The payload is readable without the key (base64 is not encryption), but any tampering invalidates the signature. For asymmetric signing (RS256, ES256) the server only needs the public key, which is why Auth0, Firebase, and Cognito publish a JWKS endpoint.
OAuth 2.0 authorization code flow is a multi-step dance:
// Step 1: redirect the user GET https://github.com/login/oauth/authorize ?client_id=Iv1.abc &redirect_uri=https://app.example.com/callback &scope=repo user:email &state=xyz123 &code_challenge=E9Melhoa... // PKCE &code_challenge_method=S256
// Step 2: user approves, GitHub redirects back with ?code=AUTH_CODE // Step 3: server exchanges code for tokens POST https://github.com/login/oauth/access_token { client_id, client_secret, code, code_verifier } -> { access_token, refresh_token, expires_in, token_type: "Bearer" }
The access_token is then used like any bearer token. PKCE (RFC 7636) is mandatory for public clients (SPAs, mobile apps) because they cannot safely hold a client_secret.
Real-World Use Cases and Who Uses What
API keys dominate server-to-server integrations. Stripe's entire API runs on them. Twilio, SendGrid, Algolia, OpenAI, Anthropic, Mailgun, and every payment processor you can name use API keys for backend calls. The reason is simple: the caller is a server under your control, the key lives in an environment variable or secrets manager, and there is no user context to worry about.
JWTs are everywhere users log in to web apps. Firebase Authentication, Auth0, AWS Cognito, Supabase, and Clerk all hand out JWT access tokens after a user signs in. Your React SPA stores a short-lived JWT (10-15 minutes), attaches it to every API call, and refreshes it silently using a refresh token stored in an httpOnly cookie. Because verification is stateless, your API can scale horizontally with no session store.
OAuth 2.0 is the right choice when a third-party application needs to act on behalf of a user. GitHub uses OAuth to let Vercel read your repos. Slack uses OAuth so integrations can post messages as a bot. Google, Microsoft, Facebook, and Apple all expose OAuth for "Sign in with..." (that is OIDC on top of OAuth). If your users will ever click an "Authorize" button that says "X wants permission to Y," you are building OAuth.
Hybrid setups are the norm at scale. GitHub's REST API accepts both personal access tokens (API keys) and OAuth access tokens. Firebase issues JWTs after authenticating users via OAuth providers. Stripe Connect uses OAuth to onboard connected accounts and then issues scoped API keys. None of these mechanisms are mutually exclusive.
A Practical Decision Tree
Answer these questions in order and you will land on the correct mechanism almost every time.
1. Is the caller a server you or your customer controls, with no end user involved? Use API keys. Generate a high-entropy secret (at least 32 bytes), prefix it with an environment tag like sk_live_ or sk_test_, hash before storing, and expose rotation.
2. Does a human user log in to your own frontend and make calls to your own backend? Use JWTs, typically issued by your own auth service or a provider like Auth0/Clerk/Firebase. Keep access tokens under 15 minutes, use refresh tokens, sign with RS256 or ES256 so only the auth server holds the private key.
3. Do you need to let a third-party application access your API on behalf of a user? Use OAuth 2.0 authorization code flow with PKCE. Define scopes carefully. Issue access tokens (JWT or opaque) that expire in an hour and refresh tokens rotated on use.
4. Do you need to identify the user in addition to authorizing the app? Use OpenID Connect, which adds an ID token (always a JWT) on top of OAuth.
5. Is the caller a CLI, a cron job, a CI runner, or a Kubernetes pod? Use client credentials (OAuth's machine-to-machine flow) if you already run an OAuth server, otherwise API keys with IP allowlists and short rotation windows.
6. Building a public mobile or SPA app that hits a third-party API? Never embed an API key. Use OAuth with PKCE, or proxy calls through your backend.
If you are tempted to pick OAuth because it sounds more secure, stop. OAuth done wrong (implicit flow without PKCE, wildcard redirect URIs, no state parameter) is worse than a well-managed API key.
Common Mistakes That Get Systems Breached
Storing API keys in git. GitHub's secret scanning finds thousands of leaked Stripe, AWS, and OpenAI keys every week. Use .env files, Doppler, AWS Secrets Manager, or Vault, and add a pre-commit hook like gitleaks.
Using the alg: none JWT. The JWT spec allows an unsigned token type. Many libraries used to accept it by default. Always pin algorithms explicitly: jwt.verify(token, key, { algorithms: ['RS256'] }).
Not validating iss and aud claims. A JWT signed by your auth server for service A will verify just fine at service B if you only check the signature. Always validate issuer and audience.
Long-lived JWTs. A 30-day access token cannot be revoked without a server-side denylist, which defeats the entire point of stateless verification. Keep access tokens short (5-15 minutes) and use refresh tokens.
Storing JWTs in localStorage. XSS trivially steals them. Use httpOnly, Secure, SameSite=Strict cookies for refresh tokens, and keep access tokens only in memory.
OAuth without PKCE. The implicit flow is deprecated (OAuth 2.1). Always use authorization code + PKCE even for confidential clients.
Wildcard or open redirect_uri. One of the oldest OAuth vulnerabilities. Allowlist exact redirect URIs, no wildcards, no path traversal.
Shipping an API key in a mobile binary. It will be extracted. Use a backend-for-frontend pattern or OAuth with PKCE instead.
Best Practices and Advanced Patterns
Rotate everything. Stripe lets you roll API keys with overlap windows. Your system should too: support two active keys per account, mark one as primary, let users rotate without downtime.
Scope aggressively. Stripe's restricted keys let you grant read-only access to charges but nothing else. OAuth scopes should be granular (repo:read vs repo:write). Principle of least privilege applies.
Use key prefixes. sk_live_, pk_test_, ghp_ (GitHub personal), xoxb- (Slack bot). Prefixes let secret scanners and your own code identify key types instantly.
Log with fingerprints, not values. Log only the first 8 and last 4 characters of any key. A full key in a log aggregator is a breach.
For JWTs, use asymmetric signing (RS256 or ES256) so verification services never hold the signing key. Publish a JWKS endpoint with key rotation (kid header).
Bind tokens where possible. DPoP (RFC 9449) and mTLS (RFC 8705) bind access tokens to a client key or certificate so a stolen token cannot be replayed.
Implement token introspection (RFC 7662) for opaque OAuth tokens if you need real-time revocation. For JWTs, keep expiration short and maintain a small revocation list keyed by jti.
For user-facing flows, use Sign in with Apple, Google, or GitHub via OIDC rather than rolling your own password database.
Side-by-Side Comparison
Here is the cheat sheet you can keep next to your keyboard.
Format — API key: opaque string • JWT: header.payload.signature • OAuth: framework (token can be either)
State — API key: stateful (DB lookup) • JWT: stateless (signature only) • OAuth: varies
Typical lifetime — API key: months to years • JWT: 5-15 minutes • OAuth access token: 1 hour
Revocable in real time — API key: yes • JWT: no (unless denylist) • OAuth: yes via introspection
Best for — API key: server-to-server • JWT: first-party SPA/mobile sessions • OAuth: third-party delegation
Carries identity — API key: via DB lookup • JWT: yes, inside claims • OAuth: only with OIDC
User consent — API key: no • JWT: no • OAuth: yes (scope approval screen)
Rotation — API key: manual or automated • JWT: via re-issue • OAuth: refresh tokens
Standard — API key: none • JWT: RFC 7519 • OAuth: RFC 6749/6750/7636
Risk if leaked — API key: full access until revoked • JWT: until exp • OAuth token: until exp + refresh revoked
Implementation complexity — API key: low • JWT: medium • OAuth: high
Use what matches the row, not what sounds most secure.
Security Considerations for Each
API keys are only as safe as your secret management. Treat them like root database passwords. Enforce TLS everywhere (no HTTP endpoints that accept keys), use IP allowlists for production keys, set rate limits per key, monitor for anomalous usage patterns, and rotate on personnel changes. Hash keys at rest using SHA-256 at minimum; bcrypt is overkill because keys have high entropy.
JWT security hinges on three things: key management, claim validation, and transport. Use RS256/ES256 with the private key in HSMs or KMS. Validate exp, nbf, iss, aud, and algorithms on every verify. Never accept tokens over plain HTTP. For refresh tokens, use rotation (each refresh returns a new refresh token and invalidates the old one); if an old one is reused, revoke the entire family as it indicates theft.
OAuth security has its own class of attacks: authorization code injection, token substitution, mix-up attacks, and open redirect. Mitigations: mandatory PKCE, exact redirect_uri matching, the state parameter for CSRF protection, nonce for OIDC, short-lived authorization codes (60 seconds, single-use), and the jarm response mode for high-value scenarios. Read the OAuth 2.0 Security Best Current Practice (draft-ietf-oauth-security-topics) at least once; it is the canonical reference.
Across all three, log authentication events (success and failure), alert on rate spikes, and run regular secret-scanning on your repos and container images. For a deeper dive, see our guide on [JWT tokens explained](/blog/jwt-tokens-explained) and [API security best practices](/blog/api-security-best-practices).
Frequently Asked Questions
Is JWT more secure than API keys?
No. Both can be implemented securely or insecurely. JWTs are harder to revoke in real time because they are stateless, while API keys require a database hit but are trivially revoked. Security depends on key management, lifetime, and transport, not on format.
Can I use JWT as an API key?
You can issue a long-lived JWT and call it an API key, but you lose the main advantage of API keys (real-time revocation) and the main advantage of JWTs (short lifetime). Most teams that try this regret it. Use one or the other for its intended purpose.
Does OAuth require JWT?
No. OAuth 2.0 is agnostic about access token format. Google returns JWTs, GitHub returns opaque strings. OpenID Connect does require JWTs for the ID token specifically.
Should I put secrets in JWT claims?
Never. JWTs are signed, not encrypted. Anyone can base64-decode the payload and read every claim. Use JWE (RFC 7516) if you must encrypt, but most teams should just avoid putting secrets in tokens altogether.
What about session cookies?
For server-rendered apps where frontend and backend share a domain, a signed session cookie is often simpler and more secure than a JWT. Use JWTs when you have distributed services that need to verify tokens without a shared session store.
How long should an access token live?
5 to 15 minutes is the sweet spot for JWT access tokens. OAuth access tokens typically live an hour. API keys can live months if rotation and monitoring are in place. The shorter the lifetime, the smaller the blast radius of a leak.
Do I need OAuth just to authenticate my own users?
No. If you are the identity provider and the resource server, a JWT issued after a password login is sufficient. OAuth is for delegation to third parties.
Can I test tokens without building a full auth server?
Yes. Use our [API Client](/api-client) to send requests with Authorization headers and inspect responses. For OAuth flows, providers like Auth0 and Clerk offer free developer tiers with full flows in minutes.
Putting It All Together
There is no single best authentication mechanism. There is only the right tool for the job. Server-to-server? API key. User session in your own app? JWT. Third-party delegation? OAuth 2.0 with PKCE, and OIDC if you need identity. Everything else is a variation on these three themes. The best engineers do not pick based on hype; they pick based on the threat model and the user experience they are building.
Start with the decision tree, implement the boring version correctly, and only reach for hybrid patterns when you have a concrete reason. The number of outages caused by overcomplicated auth flows dwarfs the number caused by plain, well-rotated API keys.
Try crafting authenticated requests in our [API Client](/api-client) to see how Authorization headers, bearer tokens, and OAuth responses behave in practice. Then revisit your own services and check that you are using each mechanism for its intended purpose.
Related Tools and Guides
Build and test authenticated requests with the [API Client](/api-client). Learn token internals in [JWT Tokens Explained](/blog/jwt-tokens-explained). Harden your endpoints with [API Security Best Practices](/blog/api-security-best-practices). Generate high-entropy secrets with our password generator for creating rotation-ready API keys.