You built an endpoint that returns exactly the right data. Then someone else needs to call it: a mobile app, a partner's backend, a scheduled job at 3 a.m. Suddenly the interesting question isn't what the API does, but who is allowed to ask.
Authentication answers that question. Choosing the wrong method is the difference between a clean integration and a support ticket at midnight. Here's a practical comparison of the three approaches you'll meet most often — API keys, OAuth, and JWTs — including the cases where each is the wrong tool.
Start With Three Questions
Before comparing methods, get clear on what you're actually proving. Almost every design decision follows from these:
- Who is calling? An application, a specific user, or a service you own?
- What are they allowed to do? Read one resource, or act on a user's behalf across an entire account?
- How long should that permission last? Forever, an hour, or until someone revokes it?
Answer those and the choice usually makes itself. Most bad auth designs come from reaching for a heavy tool when an API key would do, or the reverse.
API Keys: The Shared Secret
An API key is a long random string that identifies a project or account. The client sends it on every request, usually in a header:
Authorization: Bearer sk_live_8f2c...a71
X-API-Key: 8f2c...a71
You look it up, match it to an account, and move on. That's the whole mechanism.
Where it shines: server-to-server calls, internal scripts, public read-only data with rate limits, and anywhere you need something working today. Implementation is a database lookup and a middleware function.
Where it hurts:
- It identifies an application, not a person. Every user of that app shares the same identity.
- Keys rarely expire on their own, so a leaked key is valid until someone rotates it.
- Fine-grained permissions are awkward. You usually end up with a few coarse tiers.
- Anything running in a browser or a decompiled mobile app can't keep a secret. Never ship a key there.
Two habits make keys far safer: store only a hash of the key on your side, and prefix keys by environment (sk_live_, pk_test_) so secret scanners and your own eyes can spot a leak quickly.
Basic Authentication: Simple but Fragile
Basic auth sends a username and password as base64 in the Authorization header. Base64 is encoding, not encryption, so the credentials are effectively plain text. It's acceptable only over HTTPS, and really only for internal admin tools or prototypes behind a network boundary. It has its place — a quick internal dashboard, a health check endpoint — but it doesn't survive contact with third-party developers, and it can't express scopes or short lifetimes.
OAuth 2.0: Letting Someone Else Hold the Password
OAuth exists to solve one problem: a user wants to let your app access their data on another service without handing over their password. Instead of credentials, you get a token with limited scope and a limited lifetime.
The flow, in plain terms: your app sends the user to the provider, the user approves specific permissions, the provider redirects back with a short-lived authorization code, and your server exchanges that code for an access token and a refresh token.
Authorization Code with PKCE
This is the flow for anything where a user is present: web apps, mobile apps, single-page apps. PKCE adds a one-time proof so an intercepted code can't be swapped for a token by someone else. If you're building user-facing integrations, this is your default.
Client Credentials
For machine-to-machine calls with no user involved. Your service authenticates with its own client ID and secret and receives a token scoped to its own permissions. No browser, no redirect, no refresh dance.
Trade-offs: OAuth gives you scoped access, revocability, and a familiar consent experience. It also gives you more moving parts — redirect URIs to register, tokens to refresh, error cases to handle. It's a framework of flows, not a single button, and picking the wrong flow is the most common OAuth mistake.
JWTs: A Token Format, Not a Strategy
A JSON Web Token is a compact, signed string with three base64url parts: a header, a payload of claims, and a signature. The signature is what matters — it lets any service verify the token without calling a central database.
Two things trip people up. First, a JWT is not encrypted. Anyone holding it can read the payload, so never put secrets or sensitive personal data inside. Second, JWTs don't authenticate anything by themselves; they're how you carry the result of an authentication step. Usually that step is OAuth or a login endpoint, and the JWT is the access token that comes out.
Short lifetimes are the standard defense. Set an expiry of minutes, not days, and pair it with a refresh token. Revocation is the hard part: a self-contained token stays valid until it expires, so if you need instant revocation, keep a denylist or check a session store for sensitive operations. Keep claims small, and validate the signature and expiry on every request — not just at the gateway.
Which One Fits Your Case?
- Internal scripts, server-to-server, public data with quotas: API key.
- Your app acting for a user on another platform: OAuth 2.0 authorization code with PKCE.
- Service-to-service inside your own infrastructure: client credentials, or signed requests if you need stronger guarantees.
- Stateless auth across many services: short-lived JWTs issued after one of the steps above.
- Quick internal admin tool: basic auth over TLS behind a network boundary — replaced before it escapes.
Mistakes That Show Up in Code Review
- Keys committed to a repository. Once they're in git history, rotate them; deleting the line isn't enough.
- Credentials in logs. Scrub Authorization headers before they reach your log aggregator.
- Confusing authentication with authorization. A valid token says who is calling, not what they may touch. Check ownership on every request.
- One credential for everything. Per-environment keys and per-service clients limit the blast radius of a leak.
- Client-side permission checks. Hide the button, but enforce the rule on the server.
- No plan for rotation. Decide how a credential gets replaced before you need to do it under pressure.
Pick the Simplest Method That Covers the Threat
Start with an API key if only your own servers call the endpoint. Move to OAuth the moment a third party or an end user's data is involved. Use short-lived JWTs when you need many services to verify a caller without a database round trip. Write down who holds each credential and how it gets revoked — that single document prevents most incidents.
If your API touches payments, health records, or personal data, have someone with security expertise review the design before launch. The methods above are sound foundations, but the details of your specific obligations are worth a professional opinion.
Photo: TheDigitalWay / Pixabay


