Skip to content

Lesson 4 of 12 · 9 min read

OAuth 2.0 and PKCE

Free lessonReviewed By the AISecurityInterviews editorial team
Reading position
In this lesson

TL;DR: OAuth 2.0 (RFC 6749) lets a client application act on a resource owner's behalf at a resource server, using an access token from an authorization server, limited to approved scopes, without the client ever seeing the user's password. The authorization code flow returns a one-time code to a registered redirect URI, which the client exchanges for tokens. PKCE (RFC 7636) adds a secret the client generates per login, so redemption requires the verifier bound to that code. This does not authenticate a public app as a trusted client. The 2025 OAuth security best current practice (RFC 9700) says: use the code flow with PKCE, exact redirect URI matching, and no implicit or password grants.

Before you read

Pick an answer before you read further. The lesson below explains it, and guessing first, right or wrong, helps the explanation stay with you.

  1. 1A calendar app asks for your email password so it can read your meeting invites. What does OAuth change about this arrangement?

The problem OAuth solves

Before OAuth, an app that needed your data from another service asked for your password. That gave the app full, permanent access to your account, which you could only revoke by changing the password everywhere. OAuth replaces the password with a token: limited to specific permissions (scopes), time-bound, revocable, and issued without the app ever handling your credentials.

Note what OAuth is: delegated authorization. It answers "may this app access this resource for this user?" It does not, on its own, tell the app who the user is. Identity on top of OAuth is OpenID Connect.

The four roles

RoleExample
Resource ownerYou
ClientThe app that wants access (web app, mobile app, CLI, AI agent)
Authorization serverThe service that authenticates you and issues tokens
Resource serverThe API that holds your data and accepts tokens

The authorization code flow with PKCE

LESSON · OAUTH 20 AND PKCE
Authorization code flow with PKCE

User + browser

Client app

Authorization server

Resource server

  1. Client app to Client app

    1make code_verifier, hash to challenge
1/7

1. make code_verifier, hash to challenge

A fresh high-entropy secret for this login. The client sends only its SHA-256 hash, the code_challenge.

What each piece defends against:

PieceDefends against
Redirect to the authorization serverThe client handling the user's password
Scopes and consentThe client getting more access than the user approved
stateCross-site request forgery: an attacker's code being attached to the victim's session
Exact redirect URI matchingCodes being sent to an attacker-controlled URL
Short-lived, single-use codeReplay of a captured code
PKCE verifier and challengeRedemption of an intercepted code without the bound verifier
Token audience and scope checksA token issued for one API being accepted by another

Requirements for public and confidential clients

PKCE was designed for mobile and single-page apps, which cannot keep a client secret, because on those platforms a malicious app could register the same redirect scheme and intercept the code. It turned out to help confidential server-side clients too, by defeating code injection attacks. RFC 9700, the OAuth 2.0 Security Best Current Practice published in January 2025, requires PKCE for public clients and recommends it for confidential ones; the OAuth 2.1 specification, still an Internet-Draft in November 2026, makes it the default.

The same documents retire two older grants. The implicit flow returned tokens directly in the browser redirect, exposing them in history and to scripts; it is deprecated. The resource owner password credentials grant had the client collect the user's password, defeating OAuth's purpose; it should not be used.

The security best current practice, as a checklist

RFC 9700 (January 2025), the OAuth 2.0 Security Best Current Practice, updates the original specifications with what a decade of attacks taught. The rules interviewers expect you to know, with their strength:

RuleLevelWhat it stops
PKCE for public clients; authorisation servers support it and reject a verifier when no challenge was sentMUSTCode interception, and downgrade by stripping the challenge
PKCE for confidential clientsRECOMMENDEDCode injection into a legitimate client's session
Exact string matching of redirect URIs (port flexibility only for native-app loopback)MUSTCodes delivered to an attacker-chosen path or subdomain
No open redirectors at clients or authorisation serversMUSTCodes and tokens forwarded through a redirect
Resource owner password credentials grantMUST NOTApps collecting users' passwords
Implicit grant (tokens in the redirect)SHOULD NOTTokens leaking through URLs, history and referrers
Sender-constrained access tokens (mTLS or DPoP)SHOULDReplay of stolen tokens
Audience-restricted access tokens; resource servers refuse tokens not meant for themSHOULD / MUSTToken reuse across services
Refresh tokens for public clients sender-constrained or rotatedMUSTUse of stolen refresh tokens
A mix-up defence when a client uses several authorisation serversMUSTCodes sent to the wrong server

Mix-up attacks. A client that works with more than one authorisation server can be tricked into sending a code obtained from an honest server to an attacker-controlled one. RFC 9207 (2022) adds an iss parameter to authorisation responses; the client compares it with the issuer it started the flow with and rejects mismatches. Per-server redirect URIs are the fallback.

Pushed authorisation requests (PAR, RFC 9126). Instead of putting the authorisation request's parameters in a browser URL, where they can be tampered with or logged, the client posts them to the authorisation server over the back channel with client authentication and receives a single-use request_uri to put in the redirect. Servers can require PAR for every client.

Resource indicators (RFC 8707). The client names the API it wants a token for with a resource parameter, so the issued token's audience is that one API; the Model Context Protocol requires this for MCP servers.

OAuth 2.1 consolidates these practices into one document, but in November 2026 it was still an Internet-Draft (revision 17), not an RFC. Cite RFC 9700 for requirements, and "the OAuth 2.1 draft" for the consolidated profile.

Worked example: one mobile login, with the numbers

  1. The app generates 32 random bytes, encodes them as a 43-character base64url code_verifier, and computes the challenge as base64url(SHA-256(verifier)), also 43 characters.
  2. It opens the system browser to the authorize endpoint with response_type=code, code_challenge, code_challenge_method=S256, a random state, resource=https://api.bank.example and the claimed redirect https://app.bank.example/cb.
  3. The authorisation server stores the challenge with a single-use code that lives 60 seconds (a design choice, not an RFC number) and redirects back with the code, the state and iss.
  4. The app checks state and iss, then posts the code and verifier to the token endpoint.
  5. It receives a 5-minute access token whose audience is the banking API and a rotating, DPoP-bound refresh token.

Two attacks fail along the way. A malicious app that intercepts the code cannot redeem it without the verifier: the token endpoint answers invalid_grant. An attacker who strips the challenge from the request, hoping the server will skip PKCE, fails because the server rejects a verifier-carrying redemption for a code that was issued without a challenge, which is exactly RFC 9700's anti-downgrade rule.

LESSON · OAUTH 2.0 AND PKCE
What each parameter in a modern authorization request defends
  1. 1

    1 · code_challenge with S256

    PKCE, RFC 7636

    • Only the starter of the flow can redeem the code
    • Server rejects downgrade attempts

    Stops: intercepted and injected codes

  2. 2

    2 · state

    per-request random value

    • Binds the response to this browser session

    Stops: cross-site request forgery on the callback

  3. 3

    3 · redirect_uri, exact match

    registered in advance

    • No wildcards, no open redirectors

    Stops: codes sent to attacker paths

  4. 4

    4 · resource

    RFC 8707

    • Token audience is one API

    Stops: token reuse across services

  5. 5

    5 · iss in the response

    RFC 9207

    • Client checks which server answered

    Stops: mix-up between authorisation servers

1/3

Protect the code

PKCE and state make an authorization code useless to anyone but the session that asked for it.

OAuth for agents and tools

OAuth is the natural way to give an AI agent access to a user's accounts: the agent is a client, the user approves scopes, and the agent never holds the user's password. Two points matter:

  • Scopes should match the task, not the whole API. An agent that drafts replies does not need delete permissions.
  • Tokens must be issued for the server that receives them. The Model Context Protocol's authorization specification builds on OAuth 2.1 and requires MCP servers to accept only tokens issued for them and forbids passing a client's token through to other services (checked November 2026). Accepting tokens meant for someone else creates a confused deputy.

Compute S256, then test the transaction bindings

The PKCE transformation is base64url of the SHA-256 digest of the verifier's ASCII bytes, without padding. A fresh verifier must be generated for each flow and remain private to that flow. The following offline code generates one with the standard library. It prints only a matching result, not the verifier or a live credential.

import base64
import hashlib
import hmac
import secrets

verifier = secrets.token_urlsafe(32)
def s256(value):
    digest = hashlib.sha256(value.encode("ascii")).digest()
    return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")

challenge = s256(verifier)
assert len(verifier) == 43 and len(challenge) == 43
print(hmac.compare_digest(s256(verifier), challenge))  # => True

pending = {"browser": "session-A", "issuer": "https://id.example",
           "state": "synthetic-state-A", "consumed": False}

def callback_matches(browser, issuer, state):
    return (not pending["consumed"] and browser == pending["browser"]
            and issuer == pending["issuer"] and state == pending["state"])

print(callback_matches("session-B", "https://id.example", "synthetic-state-A"))  # => False
print(callback_matches("session-A", "https://id.example", "synthetic-state-A"))  # => True
pending["consumed"] = True
print(callback_matches("session-A", "https://id.example", "synthetic-state-A"))  # => False

This is a component fixture, not an OAuth implementation. The synthetic state label must be unpredictable in production. The authorization server separately enforces the code's client, redirect URI, challenge and single-use constraints. The client stores the expected issuer and endpoints with the transaction; it must not choose a token endpoint from an arbitrary callback field.

state, PKCE and an OIDC nonce protect different bindings. State can correlate a callback with a pending browser transaction. PKCE makes code redemption depend on the verifier. Nonce binds an ID token to the authentication request when used. One passing comparison does not establish the others. Follow the selected protocol profile and library instead of treating the three strings as interchangeable.

Diagram
A callback resumes one pending transaction
1/4

Start

Store browser binding, issuer, redirect and fresh secrets

On network uncertainty, reconcile the flow or restart it safely. Do not create a session from an unvalidated callback.

The fixture uses a single-process mutation to illustrate consumption. Real concurrent callbacks need an atomic state transition in the shared transaction store. Define how an exchange timeout differs from a definitive rejection: an authorization server may have consumed the code even if the client did not receive the response. A retry policy must not attach a result from another pending login to recover from that uncertainty.

For acceptance testing, change one binding at a time: wrong browser session, wrong expected issuer, reused state, missing stored verifier and an expired transaction. Pair each with a successful flow through your configured library in an authorized test environment. The offline fixture checks only the local bookkeeping and S256 transformation.

Misconceptions interviewers listen for

  • "PKCE is only for mobile apps." RFC 9700 recommends it for confidential clients too, against code injection.
  • "State and PKCE are redundant." PKCE protects code redemption; state ties the callback to the browser session, unless the server's PKCE support is relied on for that purpose.
  • "OAuth 2.1 is an RFC." It was still a draft in November 2026.
  • "The implicit flow is fine with short-lived tokens." Tokens in URLs leak through history, logs and referrers; use the code flow.
  • "Wildcard redirect URIs are fine if we own the domain." Any open redirect or takeover on that domain receives codes.

What if · interview follow-ups

Hold your answer as the constraints change

You are reviewing a company's OAuth deployment: a web app, a mobile app, partner integrations and 2,000 internal services calling each other. The interviewer probes one flow at a time.

  1. Start here

    What if the web app trusts two authorisation servers, and one is compromised?

Primary references

References for the mechanisms discussed above, accessed 6 November 2026:

How this shows up in interviews

"Walk me through the OAuth authorization code flow with PKCE, and what each part stops" is a standard identity question. Strong candidates draw the four roles, step through the flow, and attach an attack to each piece: state for CSRF, exact redirect matching for code theft, PKCE for code interception and injection. They also say clearly that OAuth is authorization, not authentication, and that implicit and password grants are obsolete.

Say it in 30 seconds: "OAuth lets a client act for a user at a resource server with a scoped, revocable token, without seeing the user's password. In the code flow the user logs in at the authorization server, a one-time code comes back to an exact registered redirect URI, and the client exchanges it for tokens. PKCE binds code redemption to the per-flow verifier, state stops CSRF, and the API checks audience and scopes. Best practice now is code plus PKCE for every client, no implicit or password grant."

Check yourself

Say or write your answer first, then reveal. Explaining it from memory, in your own words, is the skill an interviewer is checking.

  1. 1A mobile app uses the authorization code flow without PKCE. Another app on the same phone registers the same custom URL scheme. What can it do, and how does PKCE stop it?

  2. 2An agent platform asks users to grant full read-write access to their entire email account so the agent can summarise unread messages. What would you change?

  3. 3What does RFC 9700 require an authorisation server to do to stop PKCE downgrade?

  4. 4What is a mix-up attack, and what stops it?

Practise it as an interviewer asks it

Related lessons