Skip to content

REST API Authentication Methods

Core Concept LearningJuly 1, 202612 min readUpdated July 27, 2026

A partner script scrapes your API with a leaked key. A mobile build ships a password in every header. A "Sign in with GitHub" button works until someone treats the access token as proof of who the user is. Authentication is not one checkbox — it is matching a credential type to a caller and a threat model.

This article is the REST wire-format guide: API keys, Basic Auth, Bearer tokens, JWTs, OAuth 2.0, OpenID Connect, HMAC signatures, and mutual TLS — how each request proves something and where it fails. For when to prefer sessions vs JWTs vs OAuth as models, see JWT vs Session vs OAuth. For JWT claim validation, see JWT deep dive. For IDOR and rate limits after auth, see API security.

REST API authentication methods grouped by what each credential proves
REST API authentication methods grouped by what each credential proves

Skimmer: which method fits?

API keys and HMAC signatures are usually machine-client patterns. Basic Auth is the old HTTP-native username/password shape. Bearer tokens and JWTs are token-carrying request formats after some login or token exchange. OAuth 2.0 delegates API access through an authorization server. OpenID Connect adds identity on top of OAuth. mTLS authenticates clients at the TLS layer with certificates.

Decision order: human using your app → Bearer token, JWT, OAuth, or OIDC. Machine you trust → API key, HMAC, or mTLS. Prototype only → Basic over HTTPS, then replace. Always return clear 401 vs 403 shapes so clients know whether to re-auth or give up.

Quick reference

  • Basic — Authorization: Basic … ; credentials every request.
  • Bearer — Authorization: Bearer … ; whoever holds the token can present it.
  • JWT — signed Bearer token with claims the API verifies locally.
  • OAuth — redirects + code exchange; scopes on the access token.
  • OIDC — OAuth 2.0 plus identity via an ID token and UserInfo.
  • API key — X-API-Key or similar; rotate and scope per client.
  • HMAC/mTLS — stronger machine-to-machine proof, but higher operational cost.
  • TLS is mandatory for all header-based methods — headers are visible without it.

Remember this

Basic, Bearer, OAuth, and API keys solve different caller/risk combinations — swapping one in for another changes what a compromised credential can do.

Basic Authentication

Basic Authentication is the simplest HTTP-native approach. The client requests a protected resource; the server may respond with 401 and a WWW-Authenticate challenge; the client retries with Authorization: Basic plus Base64(username:password).

There is no token exchange. Credentials travel on every request. Base64 is encoding, not encryption — without TLS they are effectively plaintext. Even with TLS, logging the Authorization header leaks passwords. Prefer tokens or OAuth for anything user-facing.

Basic authentication request flow
Basic authentication request flow

Quick reference

  • Flow: request → 401 challenge → send username/password → resource.
  • Suitable for quick prototypes, VPN-only tools, or legacy clients.
  • Never ship to public clients without TLS and a replacement plan.
  • Prefer token or OAuth flows for user-facing APIs.
Basic Auth — request
1const credentials = Buffer.from("alice:s3cret").toString("base64");2const res = await fetch("https://api.example.com/v1/reports", {3  headers: { Authorization: `Basic ${credentials}` },4});5if (res.ok) {6  const report = await res.json();7}
Basic Auth — challenge / error shape
1// First request without credentials2// HTTP/1.1 401 Unauthorized3// WWW-Authenticate: Basic realm="api"4// { "error": { "code": "unauthorized", "message": "Authentication required" } }5 6// Wrong password7// HTTP/1.1 401 Unauthorized8// { "error": { "code": "invalid_credentials", "message": "Bad username or password" } }

Remember this

Basic Auth is easy to implement but risky for production — treat it as temporary.

Token Authentication

Token authentication separates login from later calls. The client authenticates once; the server returns a token (opaque ID looked up server-side, or a signed JWT). The client sends Authorization: Bearer <token> on each API call so the password is not re-sent.

This fits SPAs and mobile apps. JWTs are stateless but harder to revoke — pair short expiry with refresh tokens. Store tokens in httpOnly cookies (web) or a secure store (mobile), not localStorage when you can avoid it. Validate signature, expiry, and audience on every protected route.

Token authentication flow
Token authentication flow

Quick reference

  • Best for SPAs, mobile, and any client that can store a token securely.
  • Opaque tokens need a session/store lookup; JWTs verify locally.
  • Always check signature, exp, and aud — see JWT deep dive.
  • Short-lived access + refresh reduces steal-and-reuse windows.
Token — login then Bearer call
1// 1) Login2const login = await fetch("https://api.example.com/v1/login", {3  method: "POST",4  headers: { "Content-Type": "application/json" },5  body: JSON.stringify({ email, password }),6});7if (!login.ok) {8  // 401 { "error": { "code": "invalid_credentials", "message": "…" } }9  throw new Error("login failed");10}11const { accessToken } = await login.json();12 13// 2) Authenticated call14const res = await fetch("https://api.example.com/v1/orders", {15  headers: { Authorization: `Bearer ${accessToken}` },16});17const orders = await res.json();
Token — 401 vs 403 shapes
1if (res.status === 401) {2  // Missing/expired/invalid token → client should refresh or re-login3  // { "error": { "code": "invalid_token", "message": "jwt expired" } }4}5if (res.status === 403) {6  // Valid token, insufficient permission7  // { "error": { "code": "forbidden", "message": "role user cannot list all orders" } }8}

Remember this

Token Auth is the default for modern clients after initial login.

OAuth Authentication

OAuth is designed for delegated authorization — a user grants your app limited access to data on another service without sharing their password. Parties: client app, user, authorization server, and resource server. After consent, the client presents an access token to the API.

Your app never sees the user's Google or GitHub password. Use Authorization Code with PKCE for public clients. Distinguish authorization (OAuth scopes) from authentication (OIDC ID tokens). The resource server must validate issuer, signature, and scopes. Deeper model trade-offs: JWT vs Session vs OAuth.

OAuth delegated authorization flow
OAuth delegated authorization flow

Quick reference

  • Ideal for external IdPs and third-party integrations.
  • Authorization Code + PKCE for SPAs/mobile — never Implicit for new apps.
  • Validate iss, aud, exp (and nonce for OIDC) before trusting identity.
  • Resource server enforces scopes on every request.
OAuth — call API with access token
1// After Authorization Code + PKCE exchange you hold access_token2const res = await fetch("https://api.github.example/user/repos", {3  headers: {4    Authorization: `Bearer ${accessToken}`,5    Accept: "application/json",6  },7});8if (res.status === 401) {9  // { "message": "Bad credentials" }  // provider-specific body10  throw new Error("token rejected");11}12const repos = await res.json();
OAuth — insufficient scope
1// Valid token but missing scope2// HTTP/1.1 403 Forbidden3// {4//   "error": {5//     "code": "insufficient_scope",6//     "message": "Required scope: repo:write",7//     "required_scopes": ["repo:write"]8//   }9// }

Remember this

Choose OAuth when users sign in through a third party or apps need scoped external access.

API Key Authentication

API key authentication is the straightforward machine-to-machine pattern. The server issues a key; the client sends it on later requests — often X-API-Key or a query parameter (prefer headers). The server looks up the key, checks it is active, and authorizes.

Keys are long-lived shared secrets. They fit internal jobs, partner integrations, and small projects where full OAuth is overkill. They are a poor fit for end-user apps: a leaked key grants broad access until rotated. Combine with rate limits and per-key scopes; never embed keys in mobile or frontend bundles.

API key authentication flow
API key authentication flow

Quick reference

  • Good for cron, ETL, partner server-to-server, and low-risk internal tools.
  • Never commit keys; load from a secrets manager.
  • Support dual keys during rotation windows.
  • Scope and rate-limit per key to limit blast radius.
API key — request
1const res = await fetch("https://api.example.com/v1/weather?city=DAC", {2  headers: { "X-API-Key": process.env.WEATHER_API_KEY! },3});4if (!res.ok) throw new Error(`weather ${res.status}`);5const data = await res.json();
API key — error shapes
1// Missing or unknown key2// HTTP/1.1 401 Unauthorized3// { "error": { "code": "invalid_api_key", "message": "API key missing or unknown" } }4 5// Key valid but revoked / plan blocked6// HTTP/1.1 403 Forbidden7// { "error": { "code": "key_revoked", "message": "Rotate your key in the dashboard" } }8 9// Quota exceeded (often paired with rate limiting)10// HTTP/1.1 429 Too Many Requests11// Retry-After: 3012// { "error": { "code": "rate_limited", "message": "Quota exceeded" } }

Remember this

API keys fit machine clients — not end-user authentication.

Bearer Tokens

A Bearer token is a possession credential: the request is accepted because the caller presents a token in Authorization: Bearer <token>. The word Bearer is important. The API is not proving that the original user is still present; it is accepting whoever currently holds that token.

Bearer is a transport shape, not a token format. The token may be opaque and looked up server-side, or it may be a JWT verified locally. Use short expiry, TLS, secure storage, refresh rules, and token revocation where the risk requires it.

Bearer token flow: possession of the token authorizes the request
Bearer token flow: possession of the token authorizes the request

Quick reference

  • Bearer means possession is enough; stolen tokens are usable until expiry or revocation.
  • Opaque Bearer tokens require lookup; JWT Bearer tokens can be signature-verified.
  • Never put Bearer tokens in URLs because logs and browser history can leak them.
  • Use 401 for missing/invalid token and 403 for valid token without permission.
  • For browser apps, prefer httpOnly secure cookies when that fits your architecture.
Bearer request
1curl https://api.example.com/v1/orders \2  -H "Authorization: Bearer $ACCESS_TOKEN"
Bearer failure
1// Missing, expired, malformed, or revoked token2// HTTP/1.1 401 Unauthorized3// WWW-Authenticate: Bearer error="invalid_token"4// { "error": { "code": "invalid_token", "message": "Refresh or sign in again" } }

Remember this

Bearer is a possession rule: protect storage and lifetime because the API trusts whoever presents the token.

JSON Web Tokens

A JWT is a signed JSON token, commonly carried as a Bearer token. The API verifies the signature and claims such as issuer, audience, expiry, and subject. That avoids a database lookup on every request, which is why JWTs are popular for distributed APIs.

The trade-off is revocation and correctness. A valid signature only proves the token was issued by a trusted signer and has not been changed. It does not prove the user still exists, the account is active, or the role was not revoked after issuance. Keep access tokens short-lived and re-check sensitive authorization against durable state when needed.

JWT verification: signature and claims are checked before authorization
JWT verification: signature and claims are checked before authorization

Quick reference

  • JWTs are self-contained and signed; they are not automatically encrypted.
  • Always verify signature, issuer, audience, expiry, and key ID handling.
  • Use short-lived access tokens when roles or access can change quickly.
  • Do not store secrets or private profile data in a readable JWT payload.
  • Pair with refresh tokens or sessions when user-facing login needs renewal.
JWT verification checklist
1function validateJwt(claims: JwtClaims) {2  assertSignatureTrusted(claims);3  assertEqual(claims.iss, "https://auth.example.com");4  assertIncludes(claims.aud, "orders-api");5  assertAfter(claims.exp, Date.now());6  assertPresent(claims.sub);7}
Common JWT mistake
1// Bad: decoding is not validation2const claims = JSON.parse(3  Buffer.from(token.split(".")[1], "base64url").toString("utf8")4);5// This reads claims but does not prove signature, issuer, audience, or expiry.

Remember this

A JWT removes per-request lookup only if every API verifies the signature and claims correctly.

OpenID Connect

OpenID Connect, or OIDC, is the identity layer on top of OAuth 2.0. OAuth answers “can this client access this API with these scopes?” OIDC adds an ID token and discovery conventions so the client can learn who signed in. The access token is for the API; the ID token is for the client to understand the user identity.

This distinction prevents a common bug: treating an OAuth access token as proof of login. A resource server validates access tokens and scopes. A client validates the ID token issuer, audience, nonce, expiry, and signature before creating a local session or personalizing the UI.

OpenID Connect: OAuth access token plus ID token for client sign-in
OpenID Connect: OAuth access token plus ID token for client sign-in

Quick reference

  • Use OIDC for sign-in and SSO; use OAuth scopes for API access.
  • ID tokens are intended for the client, not as generic API credentials.
  • Validate nonce to bind the login response to the request that started it.
  • Use provider discovery metadata for issuer, JWKS, and endpoints.
  • Map external identity to local roles with explicit authorization rules.
OAuth vs OIDC tokens
1// OAuth 2.02access_token -> sent to API for scoped resource access3 4// OpenID Connect5id_token     -> client validates identity6access_token -> API validates authorization
OIDC callback checks
1validateIdToken(idToken, {2  issuer: "https://login.example.com",3  audience: "web-client-id",4  nonce: expectedNonce,5  maxAgeSeconds: 300,6});

Remember this

OIDC proves identity to the client; OAuth access tokens authorize API calls.

HMAC Signature Authentication

HMAC authentication signs the request with a shared secret instead of sending the secret itself. The client builds a canonical string from method, path, timestamp, body hash, and selected headers, then sends the signature. The server rebuilds the same string and compares signatures. If any signed part changes, verification fails.

This pattern is useful for server-to-server APIs and webhooks because it can prove integrity and reduce replay risk. It is also easy to get wrong: both sides must canonicalize exactly the same way, clocks must be bounded, and secrets still need rotation. Use constant-time comparison and include a timestamp or nonce.

HMAC authentication: client and server compute the same request signature
HMAC authentication: client and server compute the same request signature

Quick reference

  • Signs the request; the shared secret is not sent on the wire.
  • Protects integrity of signed fields and body hash.
  • Timestamp or nonce prevents replay within the accepted time window.
  • Canonicalization bugs cause false rejects or signature bypasses.
  • Great for webhooks and service calls; too complex for browser clients.
Representative signed request
1const canonical = [2  "POST",3  "/v1/webhooks/payment",4  timestamp,5  sha256(body),6].join("\n");7 8const signature = hmacSha256(secret, canonical);9await fetch(url, {10  method: "POST",11  headers: {12    "X-Timestamp": timestamp,13    "X-Signature": signature,14  },15  body,16});
Server verification
1if (Math.abs(Date.now() - Number(timestamp)) > 300_000) {2  return deny("stale_request");3}4const expected = hmacSha256(secret, canonicalFrom(req));5if (!timingSafeEqual(expected, req.headers["x-signature"])) {6  return deny("bad_signature");7}

Remember this

HMAC proves the request was signed with a shared secret and was not changed after signing.

Mutual TLS

Mutual TLS authenticates both sides during the TLS handshake. The server presents its certificate as usual, and the client also presents a certificate issued by a trusted authority. The server verifies the client certificate before application code handles the request.

mTLS is strongest when service identity belongs at the network boundary: internal service mesh traffic, banking or partner APIs, and high-trust machine-to-machine integrations. It is not a replacement for application authorization. After the certificate proves the client service, the API still checks whether that service can perform the requested action.

Mutual TLS: client certificate is verified during the TLS handshake
Mutual TLS: client certificate is verified during the TLS handshake

Quick reference

  • Authentication happens before HTTP handlers run.
  • Works well for service-to-service traffic with managed certificates.
  • Certificate issuance, rotation, revocation, and trust stores are operational work.
  • Still pair with API authorization, scopes, or service policy.
  • Usually overkill for public browser or mobile clients.
mTLS request path
1client certificate -> TLS handshake2server verifies:3  - trusted CA4  - certificate not expired5  - subject/SAN maps to allowed service6then HTTP request reaches the API
App still authorizes
1// Certificate says this is billing-worker.2// Policy still decides whether billing-worker may refund order 123.3if (!policy.allows(clientService, "refund:create", order.tenantId)) {4  return forbidden("service_not_allowed");5}

Remember this

mTLS proves service identity at the transport layer; application policy still decides allowed actions.

Which method should you use?

The methods solve different proof problems. Basic sends a password-like credential. API keys identify a machine client. Bearer tokens prove possession. JWTs prove signed claims. OAuth delegates scoped access. OIDC proves identity to the client. HMAC proves request integrity. mTLS proves client service identity during TLS.

Most production systems combine them: OAuth/OIDC for sign-in, Bearer JWTs for your own API, API keys or HMAC for partners and webhooks, and mTLS for service-to-service traffic where certificate operations are worth the cost. Goal: least privilege — each client gets the narrowest credential and scope it needs. Pair with API security for authorization after the credential is accepted.

One bearer-token request: valid signature but missing scope is denied
One bearer-token request: valid signature but missing scope is denied

Quick reference

  • User-facing app you control → Token (JWT + refresh) or OAuth/OIDC.
  • Sign in with Google/GitHub/enterprise SSO → OAuth 2.0 + OIDC.
  • Internal cron or ETL → API key, HMAC, or OAuth client credentials.
  • Webhooks → HMAC signature with timestamp/nonce and replay protection.
  • Service mesh or high-trust partner link → mTLS plus application authorization.
  • Quick prototype → Basic over HTTPS only, then replace.
  • Match credential type to client and risk — do not force one method everywhere.

Remember this

Pick the method that matches who is calling — human, your app, or another service.

Key takeaway

REST authentication is a set of wire contracts, not one universal best practice. Basic trades safety for simplicity. API keys keep machine access lightweight. Bearer tokens carry possession. JWTs carry signed claims. OAuth delegates access. OIDC proves identity. HMAC signs request integrity. mTLS proves service identity before HTTP handlers run.

Practice (30 min): For one endpoint, pick the credential type and write curl for the happy path plus JSON bodies for 401, 403, and replay/expiry failure. Then state what is proven: possession, signature, delegated scope, user identity, request integrity, or client certificate identity. Pass when you can explain why a valid credential may still receive 403.

Share:

Related Articles

You log out and the admin panel still accepts the old token. Or you build "Sign in with Google" and accidentally treat a

Read

Selecting the correct authorization flow is essential for securing modern applications. The OAuth 2.1 specification cons

Read

HTTPS and a valid JWT only prove the front door locked. Many real API incidents happen after authentication succeeds: a

Read

Explore this topic

Keep learning

Follow a structured path or browse all courses to go deeper.