10 min read

API Security
Intermediate
API2

Authentication Testing

Authentication mechanisms are the gatekeepers of the API. Weaknesses here can lead to full account takeover. On an API the question is narrower than on a web app: where does the credential live, what does it claim, and does the resource server actually check those claims rather than trusting the header it was handed?

Authorization boundary

Credential testing writes failed-login records against real accounts and trips lockouts: use accounts you created for the engagement, read the lockout and alerting policy first, and never run a cracking job against material you were not given.

JWT: the API-Specific Part

The JWT attack catalogue — alg: none, weak HMAC secrets, RS256→HS256 confusion, kid path traversal, jku/x5u header injection — is owned by Web Pentest 06t — JWT Attacks and is not repeated here. What changes on an API is how the token travels, where it is stored, and which claims the resource server is willing to be lied to about.

Header or cookie: what the trade-off actually costs

Transport Buys you Costs you Test for
Authorization: Bearer No ambient browser credential, so no CSRF; works for mobile and machine clients. Any script on the origin that can read storage can read the token, and a permissive CORS policy hands it to any origin. CORS on authenticated routes, token lifetime, whether a revoked token still works.
Cookie Browser sends it automatically; httpOnly keeps it away from XSS. Ambient authority — CSRF unless SameSite and a token are both right, and a wildcard CORS policy turns the API into a cross-site reader. SameSite and Secure flags, state-changing GETs, CORS with credentials.

Token storage

Read the client before you touch the server. In a browser SPA, an access token in localStorage is readable by any injected script and survives a tab close; a refresh token there is worse, because it mints new access tokens indefinitely. Check Application → Storage in DevTools after login, then check whether the refresh endpoint accepts the same token from a different origin.

Token storage best practices

Keep the access token in memory (a JS variable) and put the refresh token in an httpOnly, Secure, SameSite cookie. If a mobile client stores it in SharedPreferences or NSUserDefaults, that is the same finding with a different screenshot.

aud and scope: the claims that are never checked

A token is valid for a purpose, not for everything the issuer signs. The failure mode on APIs is a resource server that verifies the signature and stops there: any token from the same issuer — a partner integration, a low-scope client, a different service — is then accepted as a user session.

  • Audience confusion: take a token minted for service A and replay it against service B. If B accepts it, aud is either absent from the check or compared as a substring.
  • Scope escalation: re-request the same grant with a wider scope than the client asked for, then use the token on an endpoint outside the original grant.
  • Scope not enforced: a read-only token that can still POST means scopes are decoration, not authorization.
  • Revocation: log out, then replay the access token. It should be dead server-side, not just absent from the client.

What the verifier has to do

The fix is a fixed algorithm allow-list passed to the verify call plus explicit claim checks — not "enforce RS256". Telling a verifier to require a named algorithm is the same shape as the bug: whatever the token header says decides how the signature is checked, so an HS256 token signed with the public key is accepted by an RS256 verifier that trusts the header.

verify-token.js
const jwt = require('jsonwebtoken');

// The allow-list is server-side state. The token header never chooses it.
const ALLOWED = ['RS256'];

function verifyAccessToken(token) {
  const decoded = jwt.verify(token, PUBLIC_KEY, {
    algorithms: ALLOWED,          // rejects alg:none and HS256 outright
    audience: 'api.target.com',   // exact match, not substring
    issuer: 'https://auth.target.com/',
    clockTolerance: 30            // seconds; do not disable exp
  });

  if (!decoded.sub || !decoded.scope) throw new Error('missing claims');
  return decoded;
}

Test it from the outside: an alg: none token, an HS256 token signed with the public key, and a token from a different issuer should each be rejected with the same generic 401 — a verifier that distinguishes them in the error body is telling an attacker which check failed.

API Key Testing

API keys should be treated as secrets. Check how they are transmitted and if they are leaked.

Check if API key is in URL (bad practice):

API key in the query string
GET /api/data?api_key=abc123

API key in header (better):

API key as a request header
curl -H "X-API-Key: abc123" https://api.target.com/data
curl -H "Authorization: Api-Key abc123" https://api.target.com/data

Key leakage

Search for leaked keys in source code, mobile apps, or browser storage.

Check JavaScript source files:

grep — key literals in a bundle
grep -r "api_key|apikey|api-key|apiKey" *.js

Permission testing

Check if the key has excessive permissions.

Probing admin routes with a key
curl -H "X-API-Key: $KEY" https://api.target.com/admin/users
curl -H "X-API-Key: $KEY" https://api.target.com/internal/config

Rate limiting bypass

Try to bypass rate limits by rotating keys or IP addresses.

Rate-limit bypass via X-Forwarded-For
for i in {1..100}; do
  curl -H "X-API-Key: $KEY" -H "X-Forwarded-For: 192.168.1.$i" https://api.target.com/data
done

OAuth 2.0 / OIDC: the API Delta

The attack mechanics of the authorization-code flow — missing state, redirect URI tricks, code injection, consent phishing — are covered in Web Pentest 06u — OAuth Attacks. On an API the interesting question is what the resource server does with the resulting token, and what the authorization server is still allowed to be sloppy about. Keep the flow order in mind, because each control below is one step of it:

Authorization Code Flow — Where Each Control Sits

The diagram is wider than this screen — swipe it sideways to read the labels.

RFC 9700 (BCP 240, January 2025) is the current security baseline for OAuth 2.0 — use it as the checklist rather than a blog post. The controls that matter at the API boundary:

Control Correct behaviour What to test
redirect_uri Exact string match against a pre-registered value — no prefix, wildcard or traversal normalisation. Append a path, a subdomain, @evil.com, or a second redirect_uri parameter.
state Unpredictable, bound to the user's session, single-use, and verified before the code is exchanged. Omit it, replay it, and use one issued to a different session.
PKCE S256 challenge on every public client; required for the code flow, not treated as optional for confidential clients. Send an authorize request without code_challenge, then exchange with no verifier.
aud / iss Exact expected values on the resource server; one token, one intended audience. Replay a token issued for a different API or client at this one.
Client credentials Machine tokens carry only the scopes the integration needs, and client secrets are not shipped in mobile or SPA code. Ask for wider scopes than the client needs; look for a secret in the app bundle.

Tokens in URLs leak everywhere

Any OAuth response that puts an access token in a query string has written it into browser history, proxy logs, and the Referer header. Test the token endpoint for the code exchange in a request body, and check whether response_mode=fragment or the form post is accepted.

Evidence Collection

For a token or key finding, capture:

The credential itself: The exact JWT or key you replayed, which client it was issued to, and the endpoint that accepted it — redact the signature, keep the header and claims.

The decoded claim set before and after the change (aud, iss, scope, exp, sub) next to the response the resource server returned.

Token storage: the DevTools storage and cookie panel after login, showing where both tokens landed and whether HttpOnly, Secure and SameSite are set.

False Positive Identification

  • A rejected token is the control working: A 401 on an expired, revoked or foreign-issuer token proves nothing. Re-mint the token immediately before the request — a stale token left in a saved collection is the usual cause of both the false alarm and the missed finding.
  • A second key with access you already have: A key reaching data your own key can already read is not excessive permission. The finding is a key reaching data its owner cannot, or a credential visible in a URL, bundle or response body to someone who is not its owner.

Testing Checklist

Issuance and transport

  • ☐ Decode every token the API returns and confirm the algorithm, audience, issuer and lifetime are what the client expects.
  • ☐ Check how the credential travels: header versus cookie versus query string, and what the browser or mobile client persists after logout.

Enforcement

  • ☐ Replay a token minted for another service, a low-scope client and a different tenant against this resource server.
  • ☐ Log out, then replay the access token; confirm server-side revocation rather than client-side deletion.

Remediation & Defense

Authentication controls are verifier code plus issuance policy — both sides have to change together.

Algorithm allow-list, not "enforce RS256"

  • • Pass the accepted algorithms to the verify call and reject anything else before the signature is checked. Never let the token header select the algorithm, and never resolve jku, x5u or kid to an attacker-chosen location.

Validate aud, iss and exp

  • • Compare audience and issuer to exact expected values, enforce expiry with a small clock tolerance, and bind every token to the service that consumes it.

OAuth per RFC 9700

  • • Exact redirect_uri matching, a validated single-use state, PKCE with S256 on the code flow, no tokens in URLs, and refresh-token rotation with reuse detection.

No long-lived secrets in the client

  • • Access tokens in memory, refresh tokens in HttpOnly cookies, API keys never in URLs, bundles or mobile binaries — rotate on exposure and cap each key scope.

Rate limit the auth surface

  • • Throttle login, token, refresh and password-reset per identity and per source, and alert on distributed attempts. Lockout alone is a denial-of-service lever against real users.

Authentication Attack Practice

Test token handling, claim validation, and OAuth scope enforcement on real vulnerable API applications.

crAPI — JWT Attacks Custom Lab medium
JWT none algorithmWeak secret brute forceAlgorithm confusion
PortSwigger — JWT Labs PortSwigger medium
kid injectionJKU header injectionRS256/HS256 confusion