API Authentication Explained: API Keys, OAuth 2.0, and JWT
You've built the endpoint. Now someone needs to prove they're allowed to call it — and "just add an API key" is the answer that gets typed into a lot of production code that later gets rewritten under pressure. API keys, OAuth 2.0, and JWTs solve overlapping but distinct problems, and picking the wrong one doesn't usually fail in testing. It fails six months later, when you need per-user permissions, or a way to revoke one compromised credential without breaking every other integration.
This isn't a "what is an API" primer. You're already calling and building APIs — this is about which authentication mechanism actually fits the problem in front of you, and why treating API keys, OAuth 2.0, and JWT as interchangeable is how systems end up with security holes shaped like convenience shortcuts.
API keys: simple, but limited
An API key proves exactly one thing: the caller possesses a string that your system recognizes. That's it. It doesn't prove who the caller is, what they're allowed to do beyond whatever that single key was configured for, or that the request wasn't replayed from a leaked log file. It's a shared secret, not an identity.
Keys are typically sent as an HTTP header, most commonly a custom header like X-API-Key, or sometimes tucked into an Authorization header as a bearer-style value. They can also be passed as a query string parameter (?api_key=abc123), but that's the weaker option — query strings end up in server access logs, browser history, proxy logs, and Referer headers of any outbound links on the page. A header at least keeps the secret out of the URL, which is where most accidental leaks happen.
The real limits show up at scale. A single API key usually maps to an application or an account, not an individual end user — so if you need to know which user triggered a request, the key alone won't tell you. Fine-grained permissions are also awkward: you either issue one key with broad access, or you build and maintain a separate key per scope, which turns key management into its own subsystem. And revocation is blunt — if a key is shared across every server in a cluster and it leaks, rotating it means updating every place that uses it, simultaneously, with zero downtime for legitimate traffic. There's no built-in concept of "revoke this one session but leave the rest."
An API key proves you have a key. It doesn't prove you're who you say you are.
None of this makes API keys a bad choice — they're the right tool for a lot of server-to-server traffic where the caller is a known system, not a human sitting behind a login screen. The mistake is reaching for a key when the actual requirement is per-user identity or scoped permissions.
OAuth 2.0: delegated authorization, not authentication
The single most useful thing to internalize about OAuth 2.0 is that it's fundamentally about authorization, not authentication. OAuth exists to let one application act on a user's behalf against another service — read their calendar, post to their account, pull their files — without that user ever handing over their password to the requesting app. It answers "what is this app allowed to do," not "who is this person." (OpenID Connect, built on top of OAuth 2.0, is the layer that actually answers the identity question — that distinction trips up a lot of engineers who assume OAuth alone handles login.)
The most common pattern for this is the authorization code flow. At a high level: the client app redirects the user to the authorization server (say, a "Sign in with X" screen), the user authenticates there and approves the requested access, and the authorization server redirects back to the client with a short-lived authorization code. The client then exchanges that code — server-side, using its own client credentials — for an access token (and often a refresh token). The user's password never touches the requesting application at any point in that sequence.
Older guidance allowed an "implicit flow" for browser-based apps that couldn't safely hold a client secret, where the access token was returned directly in the redirect URL instead of via a code-exchange step. That's now discouraged, even for public clients like single-page apps and mobile apps, because tokens in a URL fragment are exposed to browser history, referrer leakage, and injection attacks. The current recommendation is authorization code flow with PKCE (Proof Key for Code Exchange) — the client generates a one-time secret, sends a hashed version upfront, and proves possession of the original when exchanging the code for a token. That closes the interception gap without requiring a stored client secret, which is why PKCE is now recommended for confidential clients too, not just public ones.
JWT: a token format, not a protocol
A JWT (JSON Web Token) is not an authentication method or a protocol — it's a data format, and it's frequently the format used to represent the tokens that OAuth 2.0 flows issue. Structurally, a JWT is three base64url-encoded segments separated by dots: header.payload.signature. The header describes the token type and signing algorithm, the payload holds claims (user ID, expiry, scopes, whatever the issuer decided to include), and the signature is what ties the first two segments to the issuer's key.
Here's the fact that catches people out: by default, a JWT is signed, not encrypted. Base64url encoding is not encryption — it's a reversible encoding scheme, and anyone can paste a JWT into a decoder and read the payload in plain text. The signature guarantees the token hasn't been tampered with since it was issued; it does nothing to hide the contents. That means you never put secrets, passwords, or sensitive personal data directly in a JWT payload — assume anything in there is as readable as an unencrypted cookie. (Encrypted JWTs — JWE — exist as a separate spec, but they're far less commonly used than signed JWTs, or JWS, in typical API auth setups.)
What actually makes a JWT trustworthy is signature verification on the receiving end. The API validates the signature against the issuer's public key (for asymmetric algorithms like RS256) or a shared secret (for HMAC-based algorithms like HS256). If the signature doesn't check out, or the algorithm in the header doesn't match what the server expects, the token is rejected outright, no matter what the payload claims. Skipping or misconfiguring that verification step is one of the more common ways JWT-based systems get broken.
Because JWTs are typically self-contained and stateless, they usually carry a short expiry (exp claim) — often minutes, not days — so a leaked token has a limited blast radius. Longer-lived sessions are handled with a separate refresh token, which is stored more carefully (often server-side or in an httpOnly cookie) and used only to request a new short-lived access token when the old one expires. That way the token doing the actual work on every request is disposable, and the long-lived credential is one that's much less frequently exposed.
How these fit together in a real system
These three aren't competing options you pick one of — in a lot of real systems, they layer. OAuth 2.0 is the flow that governs how a token gets issued and what it authorizes. JWT is a common format for the access token that flow produces. API keys are a separate, simpler mechanism that usually sits outside that whole exchange, most often for server-to-server calls where there's no human user redirecting through a login screen to begin with.
Put differently: it's entirely normal for a single platform to use OAuth 2.0 + JWT for its own logged-in users, and API keys for third-party developers integrating at the machine level. They're not redundant — they're solving for different callers.
A real-world example
Consider a SaaS analytics product with two distinct integration surfaces. For its own web app, a user logs in through a standard OAuth 2.0 authorization code flow with PKCE: they authenticate, the frontend receives a short-lived JWT access token plus a refresh token, and every subsequent API call attaches that JWT via Authorization: Bearer <token>. The backend verifies the signature and expiry on each request, checks the claims for the user's scopes, and never needs to hit the database to look up a session — the token carries what it needs. When the access token expires, the frontend silently uses the refresh token to get a new one, and the user never notices.
For its public API — the one third-party developers use to pull analytics data into their own tools — the same platform issues a plain API key per registered application. There's no user redirect, no consent screen, no browser involved; it's a backend service calling another backend service on a schedule. The key gets sent as X-API-Key on each request, scoped to that application's account, and it can be individually revoked from a dashboard if it's ever compromised, without touching any other integration or any logged-in user's session. Same platform, two different callers, two different mechanisms — chosen for what each caller actually is, not out of habit.
Common mistakes to avoid
Storing JWTs in localStorage. It's convenient and it's also directly readable by any JavaScript running on the page — including injected scripts from an XSS vulnerability. An httpOnly cookie can't be read by client-side JavaScript at all, which removes that entire attack surface for token theft. If you must keep a token accessible to JavaScript (for a mobile app or a non-cookie client), pair it with short expiry and tight scope so a theft is low-value.
Treating API keys as set-and-forget. A key that's never rotated and never expires is a permanent liability — every place it's ever been logged, committed, or pasted into a support ticket stays a valid credential indefinitely. Build rotation and expiry into the key lifecycle from day one, and make revoking a single key a fast, low-friction operation, not an incident.
Conflating authentication with authorization. Authentication answers "who are you" — verifying identity, whether via a password, an OAuth flow, or a signed token. Authorization answers "what are you allowed to do" — a separate check that has to run after identity is established. A system that verifies a valid JWT and stops there, without checking whether that specific user's claims actually permit the requested action, has authentication without authorization — and it will happily let a legitimately logged-in user do things they were never supposed to be able to do.
Frequently asked questions
Can I just encrypt the JWT payload instead of relying on signing?
You can, using JWE instead of the more common JWS, but it adds complexity most APIs don't need. The more practical default is to keep sensitive data out of the payload entirely and rely on HTTPS in transit plus short expiry, reserving actual payload encryption for cases where the token itself has to be stored somewhere less trusted.
Is OAuth 2.0 overkill for a simple internal API?
Often, yes. If the caller is a trusted internal service or a small set of known partners with no end-user delegation involved, a well-managed API key (or mutual TLS) is usually simpler to build, operate, and reason about than standing up an OAuth authorization server.
Should access tokens and refresh tokens both be JWTs?
Not necessarily. Access tokens are commonly JWTs because they're verified frequently and benefit from being self-contained. Refresh tokens are used far less often and typically need to be revocable server-side, so many systems store them as opaque, random strings in a database rather than as JWTs — that makes instant revocation possible, which a purely self-contained JWT doesn't easily support.
None of these three mechanisms is universally "better" — they answer different questions for different callers, and the systems that hold up under real traffic are the ones that pick deliberately rather than defaulting to whichever one shipped first. Get the authentication-versus-authorization distinction right, keep signing and encryption straight in your head, and match the mechanism to who's actually calling, and the rest of the implementation details tend to fall into place.
Keep Learning on ITVedas
One of many free guides across 8 IT chapters — all in plain English.
Explore All Chapters →