is the delegated-authorization framework most modern apps lean on, and most of its failure modes show up at the same three seams: the redirect URI, the authorization code, and the state parameter. Each seam is enforced somewhere between the client and the identity provider, and each gets worn through differently.
This primer covers the authorization-code replay attack, the role PKCE plays in stopping it, the related state-parameter and redirect-URI gaps, and how Pentrova reproduces each with deterministic evidence.
The authorization-code flow in one paragraph#
In the authorization-code grant, the client redirects the user to the identity provider, the provider authenticates the user and redirects back with a short-lived code, and the client exchanges that code for an access token at the token endpoint. The security of the whole flow rests on the code being usable exactly once, by exactly the client that initiated the request. RFC 6749 defines the grant; RFC 7636 adds the PKCE extension that hardens it.
Authorization-code replay#
The classic attack: if the provider does not bind the code to the client’s PKCE verifier, and the code is accepted more than once before expiry, an attacker who intercepts the code (through a leaky redirect, referrer header, or a malicious app registered on a mobile redirect scheme) can exchange it for a token as though they were the legitimate client.
# Attacker replays an intercepted authorization code
POST /oauth/token
grant_type=authorization_code
code=<intercepted_code>
redirect_uri=https://app.example/callback
# → 200 { "access_token": "…" } ← code accepted a second time
PKCE: the fix, when every client actually sends it#
Proof Key for Code Exchange (PKCE) plugs the hole by binding the code to a per-request secret. The client sends a hashed code_challenge on the authorization request and the matching code_verifier on the token exchange. An intercepted code is useless without the verifier.
The catch is coverage. PKCE only protects clients that use it. The OAuth 2.0 Security Best Current Practice now recommends PKCE for all client types, including confidential web clients — but plenty of deployments still apply it to public mobile clients only. A provider that accepts a code exchange with no verifier, for a client that is supposed to send one, is exploitable.
The other two seams#
stateparameter (). Thestatevalue binds the authorization response to the user’s session. A missing or unverifiedstateallows a login- attack where the victim is silently logged into an attacker-controlled account. This is broken access control territory — see OWASP A01.- Redirect URI validation. Loose redirect-URI matching (prefix matches, wildcard subdomains, unvalidated paths) lets an attacker steal the code by redirecting it to a host they control.
How Pentrova reproduces it deterministically#
Pentrova models the full flow against configured targets — one of the six auth modes API Pentesting wires in. It captures the issued code, replays the exchange with a crafted verifier (or none), and reports only when the provider accepts the forged call. The bundle includes the exact grant path that succeeded and the response tokens, so fixing the issue is a matter of closing the verifier gap rather than arguing about whether the scanner saw something real. As with every Pentrova finding, the report leads with deterministic proof, not a severity guess.
Key takeaways#
- fails at three seams: the authorization code, the
stateparameter, and the redirect URI. - Authorization-code replay works when codes are reusable and not bound to a PKCE verifier.
- PKCE closes the replay hole — but only for clients that actually send the challenge/verifier pair.
- Validate
stateto stop login-, and enforce exact redirect-URI matching.
FAQ#
Does PKCE replace the client secret? No. PKCE protects the code exchange against interception; it does not authenticate the client. Confidential clients still use a secret, and current best practice recommends they use PKCE as well.
Is OAuth 2.1 different here? OAuth 2.1 consolidates the best current practices — mandatory PKCE, exact redirect-URI matching, and removal of the implicit grant — into the baseline. Testing for the same three seams still applies.
How do I test my own OAuth implementation?
Exercise each seam: replay an authorization code twice, attempt a token exchange without a PKCE verifier, tamper with state, and fuzz redirect-URI matching. Pentrova automates this against configured targets and reports only confirmed acceptances.
See how OAuth flows are exercised in API Pentesting, or start a free engagement.