Choosing the Right Scheme

Choosing the Right Scheme: Decision Framework

flowchart TB
    Start([API Authentication
Decision]) --> Q1{Who is the caller?} Q1 -->|"End user
via browser/mobile"| OAuth[OAuth 2.0 + PKCE
→ JWT Bearer Token] Q1 -->|"External partner
server-to-server"| Q2{Need request
integrity?} Q2 -->|"Yes — financial,
webhook"| HMAC[API Key +
HMAC Signing] Q2 -->|"No — simple
read API"| AK[API Key
in header] Q1 -->|"Internal
microservice"| Q3{Infrastructure
maturity?} Q3 -->|"Service mesh
or Vault"| MTLS[mTLS
certificate-based] Q3 -->|"Simple setup"| JWTINT[JWT with
short expiry] Q1 -->|"Third-party
OAuth integration"| CC[OAuth 2.0
Client Credentials] style OAuth fill:#bfb style HMAC fill:#bfb style AK fill:#ffb style MTLS fill:#bfb style JWTINT fill:#bfb style CC fill:#bfb

Head-to-Head Comparison

PropertyAPI KeyHMAC SigningmTLSJWT Bearer
Identity proofApplication IDApplication ID + request integrityService identity (certificate)User + application identity
Request integrityNoYes (signed body + path + timestamp)Yes (TLS encryption)No (token only, not request body)
Replay protectionNoYes (timestamp window)Yes (TLS session)Partial (expiry, but within window)
Setup complexityMinimalModerate (signing logic on client)High (CA, cert distribution, rotation)Moderate (auth server, JWKS)
RevocationDelete key from DBDelete key from DBRevoke certificate (or wait for expiry)Short expiry + refresh token revocation
Credential exposure riskKey in every request headerKey never sent — only signatureCertificate on disk, auto-rotatedToken in every request header
Validation costDB lookup per requestDB lookup + HMAC computationTLS handshake (then session reuse)Local crypto only (~50µs)

Real-World Patterns

ScenarioRecommended schemeExample
Public API for developersAPI key (identity) + OAuth 2.0 (user data access)Google Maps API key + OAuth for user data
Webhook receiverHMAC signature verificationStripe webhook Stripe-Signature header
Internal microservicesmTLS (service mesh) or JWT service tokensIstio sidecar proxies handle mTLS transparently
Mobile app accessing user dataOAuth 2.0 Authorization Code + PKCE → JWT BearerInstagram API, Spotify API
Partner server integrationAPI key + HMAC signingAWS S3 API (Signature V4)
CLI tool or CI/CD pipelineOAuth 2.0 Device Flow or short-lived API tokengh auth login, aws configure
Service-to-service (no user)OAuth 2.0 Client Credentials → JWTBackend billing service calling user API

Layering Schemes Together

In practice, production APIs often combine multiple schemes:

flowchart LR
    subgraph "External Request"
        R1[API Key
identifies the app] --> R2[OAuth JWT
identifies the user] R2 --> R3[Rate limit
per API key] end subgraph "Webhook Callback" W1[HMAC Signature
proves authenticity] --> W2[Timestamp
prevents replay] end subgraph "Internal Service Call" I1[mTLS
proves service identity] --> I2[JWT
carries user context] end

Example: Stripe’s model

  • Public API calls: secret API key (sk_live_...) sent via HTTP Basic auth (Authorization: Basic <base64(sk_live_...:)>) identifies the merchant account + authenticates
  • Webhook deliveries to your server: HMAC-SHA256 signature in Stripe-Signature header, verified with your webhook secret
  • User-initiated actions: OAuth 2.0 Connect for platform integrations, JWT for session management
ℹ️

Interview tip: When API authentication comes up, say: “I’d choose the scheme based on the caller type. For user-facing requests from a mobile app, OAuth 2.0 with PKCE gives me delegated auth and scoped JWT Bearer tokens — validated locally at the API gateway with zero DB calls. For external partner integrations that modify data, I’d use API keys for identity plus HMAC request signing — the partner signs the method, path, timestamp, and body hash with their secret key, which prevents tampering and replay attacks. For internal service-to-service calls, mTLS with short-lived certificates from an internal CA gives me mutual identity verification at the transport layer — the service mesh handles it transparently. API keys are stored hashed (like passwords), JWTs are validated by signature check against JWKS, and HMAC uses constant-time comparison to prevent timing attacks.” This shows you match the scheme to the trust model and understand the security details.

Test Your Understanding

You run a public developer API. Each call must (a) identify which app is calling (for rate limits and billing) and (b) access a specific end user’s private data. Why is a single scheme the wrong answer?

The two requirements are about two different principals, so you layer two schemes. An API key identifies the application — perfect for rate-limiting and billing per client — but it says nothing about the user and can’t safely grant access to a user’s private data. For that you need OAuth 2.0: the user delegates scoped access and your app receives a JWT Bearer token representing that user.

Combined: API key (app identity + quota) + OAuth/JWT (user identity + scoped data access). Google Maps-style keys plus OAuth for user data is the canonical example.

Stripe sends webhooks to your server. There’s no bearer token or API key from Stripe in the request — so how do you authenticate that the webhook is genuinely from Stripe and not a spoof?

Verify the HMAC signature Stripe attaches, using your shared webhook secret. Stripe signs the raw payload (plus a timestamp) with a secret only you and Stripe know and sends it in the Stripe-Signature header. You recompute HMAC-SHA256(payload, webhook_secret) and compare (constant-time). A spoofed request can’t produce a valid signature without the secret, and the timestamp bounds replay.

Why not mTLS or a bearer token here? The caller is them calling you — you can’t hand Stripe a token, and you don’t control their client certs. A shared-secret signature is the natural fit for verifying inbound webhooks.