API · Rest

REST API Authentication Patterns

A practical comparison of API key, OAuth 2.0, and mutual TLS authentication for REST APIs, and how to pick the right one for a given client relationship.

John Kihiu12 min read

Authentication for a REST API is really three separate decisions wearing one name: how the caller proves who they are, how long that proof stays valid, and what happens the moment it's stolen. Most teams pick a scheme by copying whatever the last API they used did, without asking which of those three questions actually matters for their own callers. It's worth separating them, because the right answer for a server-to-server integration and the right answer for a third-party developer building against your public API are usually different schemes entirely.

API keys are for machines, not users

An API key is a static bearer credential: whoever holds the string can call the API as that identity, full stop. That's fine for a server-to-server integration where the key lives in an environment variable behind a firewall and never touches a browser. It's a bad fit for anything a human logs into, because a static key has no session, no expiry by default, and no way to distinguish "the user" from "whatever script the user's laptop is running." The mistake I see most often is issuing API keys to end users and then bolting on password-reset-style rotation later, once someone has already committed one to a public GitHub repo.

HTTP · API KEY REQUEST
GET /v1/invoices?status=open HTTP/1.1
Host: api.example.com
Authorization: Bearer sk_live_51H8x...
Accept: application/json

OAuth 2.0: when a third party acts on a user's behalf

OAuth 2.0 solves a different problem than API keys: letting a third-party application act on behalf of a user without ever seeing that user's password. The authorization code flow (with PKCE, always, even for confidential clients now) redirects the user to your login page, gets their consent for a specific scope, and hands the third-party app a short-lived access token plus a longer-lived refresh token. The access token is what gets sent on API calls; it typically expires in 15-60 minutes, so a leaked token has a short shelf life. The trade-off is complexity — you're now running a token issuer, a scope model, and a revocation story, not just checking a header against a database row.

Scopes are the access control, not the token itself

A valid OAuth token proves identity; it says nothing about what that identity is allowed to do until you check its scopes. invoices:read and invoices:write should be enforced on every endpoint, not just accepted at login — a token minted for read-only access that later gets treated as read-write because a scope check was skipped on one route is one of the more common REST API authorization bugs.

Mutual TLS for high-trust server-to-server links

For integrations between two systems you both control — a payment processor and your billing service, say — mutual TLS (mTLS) is worth the operational overhead. Both sides present a certificate, both verify each other, and the identity check happens at the transport layer before a single byte of application data moves. There's no bearer token to steal because there's no bearer token; the private key never leaves the client's key store. The cost is certificate lifecycle management: issuance, rotation before expiry, and a plan for what happens when a cert needs emergency revocation. That overhead is worth it for a handful of high-value internal links, not for every API consumer you'll ever have.

Token storage is where this actually breaks

The scheme matters less than what happens to the credential after issuance. Access tokens in a single-page app's localStorage are readable by any script on the page, which makes a stored XSS bug a full account-takeover bug. The safer pattern is an HttpOnly, Secure, SameSite cookie for browser clients, with the actual bearer token never exposed to JavaScript at all. For native mobile apps, the platform keychain (iOS Keychain, Android Keystore) is the equivalent — not shared preferences, not a plist file. None of this is REST-specific, but it's where most real-world authentication failures actually happen: not in the handshake, in the storage.

Refresh tokens need rotation, not just expiry

A refresh token that's valid for 30 days and never rotates is a 30-day-long attack window if it leaks once. Rotate the refresh token on every use (issue a new one, invalidate the old), and detect reuse of an already-rotated token as a signal that a copy leaked — that's the pattern OAuth's refresh token rotation guidance recommends, and it turns a stolen token into a one-time-use credential instead of a month-long one.

Wrapping up

Pick the authentication scheme based on who's calling: static API keys for trusted server-to-server jobs, OAuth 2.0 with PKCE for third-party apps acting on a user's behalf, mTLS for high-trust internal links where the operational cost is justified. Whichever you choose, the credential's storage and rotation story matters as much as the handshake — a technically correct OAuth flow feeding tokens into localStorage is still a broken authentication system.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.