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
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
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,
audis either absent from the check or compared as a substring. - Scope escalation: re-request the same grant with a wider
scopethan the client asked for, then use the token on an endpoint outside the original grant. - Scope not enforced: a
read-only token that can stillPOSTmeans 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.
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):
GET /api/data?api_key=abc123API key in header (better):
curl -H "X-API-Key: abc123" https://api.target.com/data
curl -H "Authorization: Api-Key abc123" https://api.target.com/dataKey leakage
Search for leaked keys in source code, mobile apps, or browser storage.
Check JavaScript source files:
grep -r "api_key|apikey|api-key|apiKey" *.jsPermission testing
Check if the key has excessive permissions.
curl -H "X-API-Key: $KEY" https://api.target.com/admin/users
curl -H "X-API-Key: $KEY" https://api.target.com/internal/configRate limiting bypass
Try to bypass rate limits by rotating keys or IP addresses.
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
doneOAuth 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
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.
Related Guides & Resources
← Back to API Pentesting
Overview of API security testing
API Reconnaissance
Discovering API endpoints
Authorization Flaws
BOLA and BFLA attacks
JWT Attacks
none-alg, weak secrets, kid/jku injection
OAuth Attacks
Auth-code flow abuse and redirect_uri tricks
Authentication Cheatsheet
Quick reference for auth testing