Most API breaches don't happen because someone cracked an encryption algorithm. They happen because a developer misunderstood what their authentication mechanism was actually doing. An API key left in a public GitHub repo, a JWT accepted without signature verification, an OAuth flow that skips state validation — these are the real attack surfaces. If you want to know how to make API endpoints secure, the first step is understanding the mechanics underneath the abstractions.
The Difference Between Authentication and Authorization (And Why It Matters)
Before diving into mechanisms, this distinction matters enormously. Authentication answers "who is this caller?" Authorization answers "what is this caller allowed to do?" A system can authenticate a request perfectly and still authorize it incorrectly — granting a user access to another user's data, for instance. Most authentication schemes bundle some authorization metadata in, which makes the two feel interchangeable. They aren't, and conflating them is where bugs hide.
API Keys: Simple, Stateless, and Dangerously Easy to Misuse
An API key is essentially a long, randomly generated string that acts as a shared secret between a client and a server. When a client sends a request, it includes the key — usually in an HTTP header like Authorization: ApiKey abc123... or, less ideally, as a query parameter. The server looks up the key in its database, finds the associated identity and permissions, and decides whether to proceed.

As an Amazon Associate, I earn from qualifying purchases.
The appeal is obvious: API keys are dead simple to implement and to hand out. The problems are equally straightforward once you think about them.
What API Keys Don't Do
API keys are credentials, not sessions. They don't expire by default. They don't rotate themselves. They carry no cryptographic proof of the requester's identity — anyone who has the key string can use it. This makes them highly portable, which is both their strength and their core vulnerability. A key embedded in a mobile app binary can be extracted. A key committed to version control gets scraped by automated bots within minutes. A key passed as a query parameter ends up in server logs, browser history, and CDN logs in plain text.
For backend service-to-service calls where the key never leaves a controlled server environment, API keys are often perfectly adequate. For anything that touches a client-side application or a shared environment, they demand careful handling: stored in environment variables or secrets managers, transmitted only in headers over HTTPS, rotated regularly, and scoped to minimum necessary permissions.
Hashing Keys at Rest
One underappreciated practice: store only a hash of the API key in your database, not the key itself. When a request arrives, hash the submitted key and compare it to the stored hash. This means a database breach doesn't immediately compromise all your keys — the same principle that applies to passwords. GitHub does this for its personal access tokens.
As an Amazon Associate, we earn from qualifying purchases.
JWT: What's Actually Inside the Token
A JSON Web Token (JWT) is a compact, self-contained credential. It consists of three Base64URL-encoded segments separated by dots: a header, a payload, and a signature. The header specifies the algorithm used for signing. The payload carries claims — statements about the user and the token itself, like sub (subject/user ID), exp (expiration time), and iss (issuer). The signature is computed over the header and payload using a secret key (for HMAC algorithms like HS256) or a private key (for RSA/ECDSA algorithms like RS256).
The critical insight: the payload is not encrypted in a standard JWT — it's only encoded. Anyone can Base64-decode it and read the contents. JWTs are about integrity and authenticity, not confidentiality. Don't put sensitive data in the payload unless you're using JWE (JSON Web Encryption) instead.
The Signature Verification Trap
The security of a JWT depends entirely on verifying the signature. If you skip this step — or worse, if your library allows the alg: "none" attack — you'll happily accept forged tokens. The "none" algorithm attack works like this: an attacker takes a valid JWT, modifies the payload to elevate their privileges, sets the algorithm to "none" (meaning no signature), and submits it. Vulnerable libraries that trust the algorithm specified in the token header rather than enforcing a server-side expected algorithm will accept it as valid.
Always specify and enforce the expected algorithm on the server side. Never trust the algorithm claimed by the incoming token.
Expiration and Revocation
JWTs are stateless by design — the server doesn't store them. This makes them efficient but creates a revocation problem. Once issued, a JWT is valid until it expires. If a user logs out, changes their password, or gets their access revoked, the token keeps working until the exp claim is reached. The common mitigations are: use short expiration windows (15 minutes is typical for access tokens), pair them with longer-lived refresh tokens stored server-side and revocable, and maintain a token blocklist for immediate revocation scenarios where the latency tradeoff is acceptable.
OAuth 2.0: A Framework, Not a Protocol
OAuth 2.0 is frequently mischaracterized as an authentication protocol. It's actually an authorization framework — its job is to let a user grant a third-party application limited access to their resources on another service, without handing over their credentials. The authentication part is often handled separately, either by the underlying identity provider or by OpenID Connect (OIDC), which is a thin identity layer built on top of OAuth 2.0.
The Authorization Code Flow
For web applications, the authorization code flow is the standard approach, and understanding each step reveals where things can go wrong.
First, the client redirects the user to the authorization server with parameters including client_id, redirect_uri, scope, and a state parameter — a random, unguessable value generated by the client. The user authenticates with the authorization server and grants consent. The authorization server redirects back to the client's redirect_uri with a short-lived authorization code and the state value echoed back. The client verifies that the returned state matches what it sent — this is a CSRF protection. Then the client exchanges the authorization code for an access token by making a back-channel request directly from the server to the token endpoint, including the client secret.
The authorization code itself is useless without the client secret to exchange it. This two-step process means the actual token is never exposed in browser redirects or logs.
The State Parameter: Skipping It Invites CSRF
Omitting or not validating the state parameter is one of the most common OAuth implementation errors. Without it, an attacker can craft a malicious link that initiates an OAuth flow on behalf of a victim — potentially linking the attacker's account to the victim's session. It's a subtle attack that many developers don't encounter until it's too late.
PKCE: Protecting Public Clients
For native apps and single-page applications that can't keep a client secret (because any secret embedded in client-side code is effectively public), the Proof Key for Code Exchange (PKCE) extension solves the authorization code interception problem. The client generates a random code_verifier, hashes it to produce a code_challenge, and sends the challenge with the initial authorization request. When exchanging the code for a token, it sends the original verifier. The authorization server verifies that the verifier hashes to the original challenge. This prevents an attacker who intercepts the authorization code from exchanging it, because they don't have the verifier.
Token Storage: Where Good Implementations Fail
How you store tokens on the client side matters as much as how you issue them. The common options — and their tradeoffs — are:
- LocalStorage / SessionStorage: Accessible to any JavaScript on the page, making it vulnerable to cross-site scripting (XSS). If an attacker can inject script into your page, they can exfiltrate every token in storage.
- In-memory (JavaScript variable): Not persistent across page reloads, but not accessible to other scripts. Good for short-lived access tokens. Loses state on refresh.
- HttpOnly cookies: Inaccessible to JavaScript entirely — the browser sends them automatically but scripts can't read them. This eliminates the XSS token-theft vector but requires careful CSRF protection (using SameSite attributes and CSRF tokens). Generally the recommended approach for web apps storing anything sensitive.
There's no perfect option. The right choice depends on your threat model: are you more worried about XSS or CSRF? HttpOnly cookies with SameSite=Strict or Lax and short-lived tokens provide a reasonable default stance for most web applications interacting with APIs.
Scope and Least Privilege: The Authorization Half
Even a perfectly authenticated token can be the vector for a breach if authorization is coarse-grained. OAuth scopes define what an access token is permitted to do — read:profile, write:orders, admin:users. Issuing tokens with broader scopes than necessary means that if a token is compromised, the blast radius is larger. Request minimal scopes at authorization time, and enforce them rigorously on the server side — don't rely on the client to only call endpoints appropriate to its scopes.
Similarly, API design should enforce object-level authorization on every request. Passing an ID in a request like GET /orders/12345 should always verify that the authenticated user owns or has permission to access order 12345 — never assume the authentication check alone is sufficient. Broken Object Level Authorization (BOLA) — also called IDOR — is consistently one of the most exploited API vulnerability classes.
Transport Security Is Table Stakes
None of the above matters if credentials are transmitted over plain HTTP. TLS is non-negotiable for any API carrying authentication material. Beyond just enabling HTTPS, HTTP Strict Transport Security (HSTS) headers prevent downgrade attacks that could strip TLS in transit. Certificate pinning — fixing the expected certificate or public key in the client — goes further for high-value native applications, though it introduces operational complexity when certificates rotate.
Rate Limiting and Anomaly Detection: Defense in Depth
Authentication mechanisms gate initial access, but they don't help once a valid token or key is compromised. Rate limiting slows down credential stuffing and brute-force attacks against token endpoints. Logging and anomaly detection catch unusual patterns — a token suddenly being used from a new geography, an API key making requests at machine speed against sensitive endpoints, a refresh token being used from two locations simultaneously. These signals are how you detect the breach that got past your authentication layer.
The Practical Security Checklist
Pulling the above together into concrete decisions:
- Never put API keys or secrets in source code, client-side bundles, or query parameters.
- Hash API keys before storing them.
- Always verify JWT signatures server-side with an explicitly configured algorithm — never accept
alg: none. - Use short-lived access tokens (minutes to an hour) paired with revocable refresh tokens.
- Implement the full OAuth authorization code flow with state validation; use PKCE for public clients.
- Store tokens in HttpOnly cookies for web apps, or in memory for short-lived use.
- Enforce scopes and object-level authorization on every request, server-side.
- Mandate TLS and enable HSTS.
- Rate-limit authentication endpoints and monitor for anomalous usage.
API authentication is one of those areas where the gap between "it works" and "it's secure" is wide and invisible right up until it isn't. Understanding what each mechanism actually does — rather than treating it as a black box — is the only reliable way to implement it correctly.


