API authentication methods

Ten ways to prove who is calling your API — from no auth at all through Basic, API keys, bearer tokens, OAuth, cookies, mutual TLS and HMAC request signing — with what each one puts on the wire, what it costs to run, and how to choose between them. Every example here is something you can send from the online API tester and watch on the response side.

Authentication is not authorisation

Authentication answers who are you. Authorisation answers what may you do. They are separate steps with separate failure modes, and collapsing them into one word — "auth" — is the reason so many API bugs are misdiagnosed. Most incidents filed as authentication problems are authorisation problems: the credential was fine, the permission check was wrong or missing.

HTTP encodes the distinction in two status codes, and getting them right is free documentation for whoever is debugging your API at three in the morning. 401 Unauthorized means the request carried no credential, or one the server could not accept — the name is a historical misnomer, it really means unauthenticated. RFC 9110 requires a WWW-Authenticate header on every 401, telling the client which scheme to use. 403 Forbidden means the server knows exactly who you are and is refusing anyway: wrong scope, wrong role, wrong tenant, wrong object. Retrying a 401 with a fresh token is sensible. Retrying a 403 with the same identity never helps, which is precisely why the two codes must not be used interchangeably.

A useful mental model: authentication happens once per request at the edge and produces a principal — an application, a user, or a user acting through an application. Authorisation happens repeatedly, deep inside your handlers, every time that principal touches an object. The methods below are all about the first half. The section on authorisation models covers the second, and the second is where the real vulnerabilities live.

The methods, one by one

1. No authentication

Plenty of endpoints legitimately need no credential: a public status page, a currency conversion table, a documentation search. The mistake is treating "public" as "unprotected". An unauthenticated endpoint has no principal to key a quota on, which makes it the easiest thing in your estate to abuse — scraped, hammered, or used as an amplification target. Public endpoints therefore need the strongest rate limiting you can apply, keyed on IP or on a coarse fingerprint, and they need response sizes bounded so a single call cannot pull your entire catalogue.

Public also means cacheable, which is an advantage: a public endpoint can sit behind a CDN and never touch your origin, whereas anything carrying Authorization generally cannot be shared between callers.

2. HTTP Basic (RFC 7617)

The oldest scheme still in daily use. The client concatenates the username, a colon and the password, base64-encodes the result and sends it:

GET /v1/reports HTTP/1.1
Host: api.example.com
Authorization: Basic YWxpY2U6czNjcjN0

That value decodes to alice:s3cr3t in one step with no key and no effort — base64 is an encoding, not encryption, and you can prove it to yourself in the Base64 encoder in about four seconds. Basic therefore has exactly one security property, and it is TLS. Over plain HTTP it is equivalent to shouting the password.

Strengths: universally supported, zero client libraries required, works in curl with a single flag, and the server-side check is trivial. Weaknesses: the credential is long-lived and reusable, it is sent on every single request so every log and proxy is a potential leak point, it cannot be scoped to a subset of the API, and revoking it means changing the password for everything that uses it. There is no expiry. Use it for: internal service-to-service calls on a private network, CI jobs, admin tools behind a VPN, and the many APIs that use Basic with an API key as the username and an empty password because it was the easiest thing to bolt onto existing HTTP clients.

3. HTTP Digest (RFC 7616)

Digest was designed to fix Basic's biggest flaw without TLS. The server issues a challenge containing a nonce; the client hashes the username, password, nonce, method and URI together and sends only the digest, so the password itself never crosses the wire and a captured response cannot be replayed against a different nonce. RFC 7616 modernised it with SHA-256, superseding the MD5-only original.

In practice it is obsolete. It costs an extra round trip, it forces the server to store passwords in a form it can recompute the digest from, client and proxy support is patchy, and ubiquitous TLS removed the problem it solved. Tooling is poor enough that debugging a Digest failure is genuinely unpleasant. Mentioned here for completeness — if you are designing something new, do not choose it.

4. API keys

An API key is a shared secret string that identifies the calling application, not a human. It travels in a custom header or in Authorization:

GET /v1/products?limit=50 HTTP/1.1
Host: api.example.com
X-API-Key: pk_live_7Qa4xR2mZ9vL3nT8bK1wY6cJ

Keys are the workhorse of developer-facing APIs because they are easy to issue, easy to paste into a config file and easy for a customer to understand. Doing them properly means getting six things right.

  • Generate with a CSPRNG. At least 128 bits of entropy from a cryptographically secure random source, rendered as base62 or hex. Never a UUIDv1, never a hash of the account ID, never anything a customer could predict from their own key.
  • Store only a hash. Keep a SHA-256 of the key in your database, not the key itself, exactly as you would for a password — high-entropy random keys do not need a slow KDF, but they do need to be unreadable if the table leaks. Show the key once at creation and never again. You can see what a digest looks like in the hash generator.
  • Prefix them. A key that starts with pk_live_ or sk_test_ is instantly recognisable in a support ticket, tells you at a glance whether someone has pasted a production secret into a bug report, and — the real win — can be matched by a regular expression, so secret-scanning services can find your keys in public repositories and notify you automatically. An opaque 32-character blob is unfindable.
  • Rotate with overlapping validity. Allow at least two live keys per account so a customer can add the new one, deploy, and retire the old one without downtime. A rotation scheme that requires an outage will simply never be used.
  • Scope each key. Read-only keys, per-environment keys, per-integration keys. A key that can do everything turns every leak into a total compromise.
  • Never accept keys in the query string. ?api_key=… is written to every access log along the path, stored in browser history, cached by proxies, and — if the URL ever appears in a page — sent onward in the Referer header to somebody else's server. Header only.

Weaknesses: a key is a bearer credential like any other, it identifies an application rather than a user so it cannot express "this end user consented", and it typically has no expiry unless you build one.

5. Bearer tokens

A bearer token is any credential where possession alone is sufficient: Authorization: Bearer <token>, defined by RFC 6750. There is no signature proving the sender is the rightful holder, so a stolen token is a stolen identity until it expires — which is why bearer tokens are issued short-lived and paired with refresh tokens, and why they must never be logged.

The token itself may be an opaque random string that the server looks up, or a self-contained signed structure. Both are "bearer"; the scheme says nothing about the format. For the full treatment — token lifetimes, refresh flows, WWW-Authenticate error codes, revocation, storage in browsers and the sender-constrained alternatives to plain bearer — see the dedicated bearer token guide.

6. OAuth 2.0 and OpenID Connect

OAuth 2.0 (RFC 6749) is a delegated authorisation framework: it lets a user grant a third-party application limited access to their data on another service without handing over their password. The application receives a scoped, expiring access token instead of credentials, and the user can revoke it independently at any time. OpenID Connect is a thin identity layer on top, adding an id_token and a userinfo endpoint so the application can also learn who the user is rather than merely what it may do on their behalf.

OAuth is what you reach for when a party you do not control needs access to data you do not own. It is substantially more machinery than an API key, and it is not the right answer for your own backend calling your own backend. Grant types, PKCE, the authorisation code flow and how the framework relates to token formats are covered in OAuth vs JWT.

7. JWT

A JSON Web Token (RFC 7519) is a token format, not an authentication method: three base64url segments — header, claims, signature — joined by dots, carrying claims such as sub, exp, iss and aud, signed so the recipient can verify them without a database lookup. That statelessness is the whole appeal, and also the whole drawback: a JWT is valid until it expires, so revoking one early requires the very state you avoided.

The critical thing to internalise is that JWT is not an alternative to OAuth. OAuth is the protocol for obtaining a token; JWT is one possible encoding of that token. An OAuth access token can be a JWT or an opaque string, and a JWT can be issued by a system with no OAuth anywhere in it. Anyone asking "should we use OAuth or JWT?" has conflated two different layers — that comparison is its own page. To inspect a token you have in hand, paste it into the JWT decoder and read the exp claim; an expired token explains most sudden 401s.

8. Session cookies

The browser-native approach: the user posts credentials to a login endpoint, the server creates a session and returns a cookie, and the browser attaches it to every subsequent request automatically.

Set-Cookie: sid=8f2c9ab1e7; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400

The three flags carry the security. HttpOnly hides the cookie from JavaScript, which is what makes it more resistant to token theft via XSS than anything you keep in localStorage. Secure restricts it to HTTPS. SameSite controls whether it is attached to cross-site requests: Lax allows it on top-level navigations only, Strict never, and None always — the last requiring Secure.

Cookies differ from token auth in one decisive way: they are ambient. The browser sends them whether or not your code asked it to. That makes them ideal for a first-party web app — nothing to store, nothing to attach, revocation is a row delete — and it is exactly what creates CSRF exposure, where another site triggers a request to yours and the browser helpfully authenticates it. Bearer tokens are immune to CSRF precisely because they are not ambient: an attacker's page cannot add an Authorization header it does not know. Defend cookie sessions with SameSite plus an anti-CSRF token on state-changing requests. Note that CORS is not a CSRF defence and CSRF is not what CORS is for — the two are constantly confused, and the CORS guide pulls them apart.

Cookies are awkward for mobile apps, server-to-server callers and any third-party client, all of which have to emulate a cookie jar for no benefit. Use them for same-site browser apps and tokens for everything else.

9. Mutual TLS

In ordinary TLS the client verifies the server's certificate. In mutual TLS the server also demands a certificate from the client and verifies it against a trusted CA, so authentication completes during the handshake, before a single byte of HTTP is exchanged. The identity is the certificate subject, and the private key never leaves the client. RFC 8705 defines how to bind OAuth access tokens to a client certificate, which upgrades those tokens from bearer to sender-constrained — a stolen token is then useless without the matching key.

Strengths: the strongest option on this list, immune to token theft and replay, and nothing secret transits the application layer. Weaknesses: you now operate a PKI — issuing, distributing, renewing and revoking certificates, plus every expiry becoming an outage. Terminating TLS at a load balancer complicates passing the verified identity through to the application. Use it for: banking and open-banking APIs, payment networks, and service-to-service traffic inside a service mesh where a sidecar handles certificate rotation automatically and the cost largely disappears.

10. HMAC request signing

Signing is the only scheme here in which the secret never crosses the wire. The client builds a canonical string from the parts of the request that must not be tampered with — method, path, sorted query, selected headers, a timestamp, and a hash of the body — computes an HMAC of it with a shared secret, and sends the resulting signature alongside the key identifier:

POST /v1/transfers HTTP/1.1
Host: api.example.com
X-Timestamp: 1793491200
X-Key-Id: AKIA7QA4XR2MZ9VL
Authorization: HMAC-SHA256 Signature=9f86d081884c7d659a2feaa0c55ad015a...

canonical string signed:
POST
/v1/transfers

x-timestamp:1793491200
sha256(body)=b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

The server recomputes the same string from the request it actually received and compares signatures in constant time. Anything an attacker modifies in flight — a changed amount, a different path — breaks the signature. Because the timestamp is inside the signed material, the server can reject anything outside a narrow window (typically five minutes) and remember recently seen nonces, which defeats replay. AWS Signature Version 4 is the canonical production example; several payment and telephony APIs use the same shape.

Compared with bearer tokens the gains are real: an intercepted request cannot be replayed, cannot be modified, and cannot be reused against a different endpoint, and because only the holder of the secret could have produced the signature you get a measure of non-repudiation. The cost is equally real. Canonicalisation is fiddly and every client must agree on it byte for byte — a trailing slash, a differently-ordered query string, a header your proxy added or normalised, a body re-serialised with different whitespace, or a client clock two minutes out will each produce a valid-looking request that fails to verify with an unhelpful error. Debugging signing bugs is the least pleasant work in this article, which is why most public APIs settle for bearer tokens over TLS.

The same idea runs in reverse for webhooks: the sender signs the payload with a secret you share, and your endpoint verifies the signature before trusting the event — otherwise anyone who learns your callback URL can post fake events into your system. Inspect real signed deliveries with the webhook tester.

Comparison table

MethodSent on the wireIdentifiesRevocableScopedExpiresReplay-resistantBrowser-friendlyCost to build
NoneNothingNobodyn/aNon/an/aYesNone
BasicPassword (base64)User or appPassword changeNoNoNoYesVery low
DigestDigest of passwordUserPassword changeNoPer noncePartlyYesMedium
API keyShared secretApplicationYes, per keyIf designedRarelyNoPoorlyLow
Bearer tokenTokenUser or appYes, if statefulYes, scopesYesNoYesMedium
OAuth 2.0Access tokenUser via appYesYes, scopesYesNoYesHigh
JWT (format)Signed claimsWhatever it saysHardVia claimsYes, expNoYesMedium
Session cookieSession idUserYes, immediatelyVia rolesYesNoNativeLow
mTLSCertificate, no secretClient machineCRL or short certsVia subjectCert lifetimeYesAwkwardVery high
HMAC signingSignature onlyApplicationYes, per keyPer keyPer requestYesAwkwardHigh

"Browser-friendly" means usable from front-end JavaScript without exposing a long-lived secret in code the user can read. Anything marked awkward or poorly should live behind your own backend, never in a single-page app bundle.

Which one should you use?

ScenarioRecommendedWhy
Internal service to service mTLS in a mesh, otherwise a scoped API key over TLS No human is present, so no consent flow is needed. If a sidecar already rotates certificates, mTLS is nearly free; if not, a key is honest and cheap.
Public developer API API keys, or OAuth client credentials for larger customers Developers understand keys, they paste into config files, and per-key scoping plus rotation gives you a revocation story.
First-party single-page app Session cookie with HttpOnly, Secure, SameSite Same site means the browser does the work, nothing is exposed to JavaScript, and logout is instant. Add CSRF tokens on writes.
Mobile app OAuth authorisation code flow with PKCE, short access tokens plus refresh No cookie jar, no embeddable secret in a binary anyone can unpack, and tokens survive being stored in the platform keychain.
Third party accessing user data OAuth 2.0, with OIDC if you also need identity The only model where the user grants scoped access without sharing a password and can revoke it later independently.
CI/CD scripts Short-lived tokens from the platform, else a scoped key in a secret store Prefer credentials minted per job with a lifetime of minutes over a static secret sitting in a repository variable forever.
Webhooks you send or receive HMAC signature with a timestamp window The receiver has no session with you, so the payload must prove its own origin and freshness.

Authorisation models, briefly

Once you know who the caller is, you need a model for what they may do. Three are common, and they compose rather than compete.

  • Scopes constrain the token: orders:read, invoices:write. They express what an application was permitted to ask for, and they are a ceiling, not a grant — a token with orders:read can still only read the orders its owner could read.
  • Roles (RBAC) constrain the subject: admin, support, viewer. Simple to reason about and easy to audit, but roles multiply once real organisations need per-team and per-region variants.
  • Attributes (ABAC) evaluate a policy over facts about the subject, the object and the context — department, ownership, time of day, IP range. Far more expressive, considerably harder to test and to explain to a user who was unexpectedly denied.

None of these substitutes for the fourth check, and the fourth check is the one that gets skipped: per-object ownership. Every handler that accepts an identifier must verify that the authenticated principal is entitled to that specific object. Failing to do so is broken object level authorisation — listed as API1, the number-one entry in the OWASP API Security Top 10, and long known as IDOR. The shape is always the same: a valid token for user A, a request for GET /v1/orders/1084, and an order belonging to user B returned in full because the code checked the token and then trusted the path parameter. Sequential integer identifiers make it trivial to enumerate; UUIDs raise the effort but are not an access control. The fix is not obscurity, it is scoping every query by owner at the data layer — WHERE id = ? AND account_id = ? — so an unauthorised object cannot be loaded even by accident. Try changing an ID in a request from the REST API tester against your own staging environment; if it returns somebody else's data, you have found your API1.

Transport and hygiene

  • TLS everywhere, no exceptions. Every scheme above except mTLS and HMAC signing collapses entirely without it. Add Strict-Transport-Security so a client that once reached you over HTTPS never tries plaintext again, and redirect rather than serving anything on port 80.
  • Never log the Authorization header. Or cookies, or request bodies from a login endpoint. Add them to your logger's redaction list before your first deploy, not after your first incident — and remember your reverse proxy, APM agent and error tracker each have their own capture rules.
  • Compare secrets in constant time. A naive equality check returns faster on an early mismatch, which leaks the secret byte by byte to a patient attacker. Use your language's hmac.compare_digest equivalent for keys, signatures and tokens alike.
  • Keep credentials out of URLs. Paths and query strings end up in access logs, browser history, bookmarks, chat previews and Referer headers. Headers do not.
  • Rotate on a schedule, and practise it. Overlapping validity windows are what make rotation possible without downtime. A rotation procedure that has never been rehearsed does not exist.
  • Least privilege and short lifetimes. Every credential should be able to do the smallest useful thing for the shortest useful time. These two properties turn a compromise into an incident rather than a catastrophe.
  • Return a proper challenge. A 401 without WWW-Authenticate is a spec violation and leaves generic clients unable to respond correctly.

Debugging authentication failures

SymptomLikely causeWhat to check
401 with no WWW-Authenticate Framework rejecting before the auth middleware, or a misimplemented challenge Whether the header reached the app at all; a gateway may be answering on the origin's behalf
403 with a token you know is valid Authorisation, not authentication — missing scope, wrong role, wrong tenant Decode the token and compare its scopes and aud against what the endpoint demands
Works in curl, fails in the browser CORS: Authorization not permitted in the preflight, or the origin is not allowed The OPTIONS response headers — see the CORS guide
Header appears to vanish in production only A proxy, load balancer or CDN stripping unknown or Authorization headers Echo the received headers from a debug endpoint; compare direct-to-origin against through-the-edge
Intermittent failures, often at deploy time Clock skew — signature windows and exp/nbf claims are time-sensitive NTP on both machines; a 90-second drift breaks a 60-second signing window
Token rejected after copy and paste Trailing whitespace, a newline, a smart quote, or a truncated middle from a wrapped terminal Length of the string, and that it still has exactly two dots if it is a JWT
Everything 401s right after it previously worked Expiry, or a rotated key still cached in a client The exp claim in the JWT decoder

The fastest way to isolate any of these is to take the browser and your application out of the loop. The Auth tab in the API tester builds the header for you: choose Bearer and paste a token to get Authorization: Bearer …, choose Basic and enter a username and password to have the base64 encoding done correctly, or choose API key to set a custom header such as X-API-Key with the value you specify. Because requests are sent server-side rather than from your browser, CORS cannot interfere — so if a call succeeds in the REST API tester and fails in your front end, the credential is fine and the problem is in the browser. Compare the two request header sets before touching any code; the difference is nearly always the answer.

Frequently asked questions

What is the difference between authentication and authorisation?

Authentication establishes who the caller is; authorisation decides what they may do. HTTP splits them into 401 for a missing or invalid credential and 403 for a valid credential without permission.

Is an API key the same as a bearer token?

They look alike on the wire, but a key is usually a long-lived secret identifying an application, while a bearer token is short-lived, issued by an authorisation server and carries scopes.

Is HTTP Basic authentication secure?

Only over TLS. Base64 is encoding, not encryption. It is acceptable for internal services and CI, but offers no scoping, no expiry and no revocation short of changing the password.

Should API keys go in a header or a query string?

Always a header. Query strings leak through access logs, browser history, proxies and the Referer header sent to third-party sites.

Why does my token work in curl but fail in the browser?

Almost always CORS — the server did not permit the Authorization header in the preflight, or did not allow your origin. The request usually reached the server; the browser just refused to hand you the response.

What is the most common API security flaw?

Broken object level authorisation — API1 in the OWASP API Security Top 10, also called IDOR. The token is checked but object ownership is not, so user A reads user B's records by changing an identifier.