Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Web Security (OWASP Top 10:2025)  Module 8 – A07:2025 Authentication Failures

OAuth 2.0 and OpenID Connect: done right

Use the authorization code flow with PKCE, check the callback and the ID token, keep tokens away from page script, and link accounts by issuer and subject.

  • Advanced
  • 30 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free intro, Pro: 6 of 30 minutes free
Free: Use the authorization code flow with PKCE, and avoid the other grants for people. With a pass: the other 8 sections and 3 interview answers, about 24 minutes.

What you will learn

  • Choose the authorization code flow with PKCE, and avoid the implicit and password grants
  • Validate the state, the issuer, the redirect URI and the ID token claims, including the nonce, in a sign-in callback
  • Separate authentication by the ID token from authorisation by the access token, and check the audience at a resource server
  • Pick a browser-app architecture, a backend for frontend or a token-mediating backend, using RFC 10017
  • Link accounts by issuer and subject, and prevent a sign-in from taking over another person's account through an email address

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose the authorization code flow with PKCE, and avoid the implicit and password grants
  • Validate the state, the issuer, the redirect URI and the ID token claims, including the nonce, in a sign-in callback
  • Separate authentication by the ID token from authorisation by the access token, and check the audience at a resource server
  • Pick a browser-app architecture, a backend for frontend or a token-mediating backend, using RFC 10017
  • Link accounts by issuer and subject, and prevent a sign-in from taking over another person's account through an email address

Before you start

On this page

A “Sign in with…” button hands your app a code and leaves the app to check what came back. OAuth 2.0 and OpenID Connect are the rules for that exchange, and the same exchange can be built so that a copy of a code is worthless or so that it is a login. In this lesson’s first program, a server that ignores PKCE: Proof Key for Code Exchange (RFC 7636). The app makes a random verifier, sends its SHA-256 hash, the challenge, with the authorization request, and sends the verifier itself when it swaps the code for tokens. A copy of the code is useless without it. issued tokens for 4 of 5 requests, a bare copy of a code among them, and a server that enforces it issued tokens for 1.

By the end you will be able to run the authorization code flow with PKCE, check the callback and the ID token, give each token only to the party it is for, choose where a browser app keeps its tokens, and link accounts without letting an email address decide who someone is.

Use the authorization code flow with PKCE, and avoid the other grants for people

OAuth 2.0 lets an app act for a person at an API: the person signs in at an authorization server, and the app receives an access token: A token that an app presents to an API to act for a person, limited by scope and by the audience it was made for. The API decides what it allows. The app treats it as an opaque value. that the API accepts. OpenID Connect is a layer on top of OAuth 2.0 that adds sign-in, because it also returns an ID token: A signed JWT from an OpenID Connect provider that says who signed in: the issuer, the subject, the audience and the times. It is for the app that asked for it, and not for APIs., a signed statement of who signed in. Step through the flow that carries both:

Interactive · message by message A sign-in with the authorization code flow and PKCE Step through one sign-in and see what the app keeps, what travels, and what it checks before it believes the person.
A sign-in with the authorization code flow and PKCE, step 8 of 8. Messages so far: Your app to Browser: redirect with challenge; Browser to Auth server: GET /authorize; Auth server to Browser: redirect with code; Browser to Your app: GET /callback; Your app to Auth server: code and verifier; Auth server to Your app: ID and access token.Your appsigned in as issuer + subBrowsercarries the personAuth serverknows the personredirect with challengeGET /authorizeredirect with codeGET /callbackcode and verifierID and access token

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Make three secrets. The app makes a state, a nonce and a PKCE verifier, all random, and saves them in this person's session. The server will get a hash of the verifier, not the verifier.
  2. Send the person away. The app redirects the browser to the server's authorization endpoint with its client ID, its registered redirect address, the scope, the state, the nonce and the PKCE challenge.
  3. The person signs in there. The server shows its own sign-in page. The person signs in and agrees to what the app asks for. The app never sees the password.
  4. Back with a code. The server sends the browser back to the registered address with a short-lived code that works once, the state and the name of the issuer.
  5. Check what came back. The callback reaches the app. Before it uses the code, it checks that the state is one it saved for this session and that the issuer is the one it expects.
  6. Swap the code for tokens. The app sends the code and the verifier to the token endpoint. The server hashes the verifier and compares it with the challenge it was given at the start.
  7. Tokens come back. The server returns an ID token, which says who signed in, and an access token, which lets the app call an API for the person.
  8. Believe the ID token. The app checks the ID token's signature, issuer, audience, expiry and nonce. Only then does it treat the person as signed in, by the issuer and the subject in the token.

Three random values tie the flow together. The state: A random value that an app sends in an authorization request and checks again on the callback, tied to the person's browser session. It shows that the callback answers a request that this browser made. ties the callback to the browser that asked. The nonce ties the ID token to this sign-in. The PKCE verifier ties the code to the app that asked for it. The grants differ in how they get a token, and for a person signing in only one is right:

  • Authorization code with PKCE. The app sends the person to the server, receives a code that works once and swaps it for tokens, proving with the verifier that it is the app that asked.
  • Client credentials. A service gets a token for itself with its own credentials, and no person is involved.
  • Implicit. The server puts the tokens in the redirect URL itself, and RFC 9700 says that clients should not use it.
  • Resource owner password credentials. The app collects the person’s password and sends it on, and RFC 9700 says that it must not be used.

The program checks the PKCE test vector of RFC 7636 and then sends five requests to two small servers, one that ignores PKCE and one that enforces it:

Five requests to a server that ignores PKCE and one that enforces it Python · start/pkce.py
"""PKCE: the verifier stays with the app, its hash goes with the request, and a copy of the code alone is worth nothing."""

import base64
import hashlib


def s256(verifier):
    """The code challenge: BASE64URL(SHA-256(verifier)), with no padding (RFC 7636)."""
    return base64.urlsafe_b64encode(hashlib.sha256(verifier.encode("ascii")).digest()).rstrip(b"=").decode()


# The test vector of RFC 7636, appendix B.
VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
print("RFC 7636 appendix B, challenge as computed:", s256(VERIFIER) == CHALLENGE)


class AuthServer:
    """A small authorization server. One that enforces PKCE requires a challenge from this client and checks the verifier."""

    def __init__(self, enforces_pkce):
        self.enforces_pkce = enforces_pkce
        self.codes = {}
        self.count = 0

    def authorize(self, challenge=None):
        if self.enforces_pkce and challenge is None:
            return None, "refused: no challenge"
        self.count += 1
        code = f"code-{self.count}"
        self.codes[code] = challenge
        return code, None

    def token(self, code, verifier=None):
        if code not in self.codes:
            return "refused: code used"
        challenge = self.codes.pop(code)  # a code works once
        if self.enforces_pkce and (verifier is None or s256(verifier) != challenge):
            return "refused: verifier"
        return "tokens issued"


def cases(server):
    results = []
    code, _ = server.authorize(CHALLENGE)
    results.append(("the app exchanges its code and its verifier", server.token(code, VERIFIER)))
    code, _ = server.authorize(CHALLENGE)
    results.append(("a copy of the code arrives with no verifier", server.token(code)))
    code, _ = server.authorize(CHALLENGE)
    results.append(("a copy arrives with a verifier it made up", server.token(code, "x" * 43)))
    code, _ = server.authorize(CHALLENGE)
    server.token(code, VERIFIER)
    results.append(("the app's own code is used a second time", server.token(code, VERIFIER)))
    code, error = server.authorize(None)
    results.append(("an authorization request with no challenge", server.token(code) if code else error))
    return results


ignores, enforces = cases(AuthServer(False)), cases(AuthServer(True))
print(f"\n{'case':<46}{'server that ignores':<22}server that enforces")
for (name, a), (_, b) in zip(ignores, enforces):
    print(f"{name:<46}{a:<22}{b}")

Output

RFC 7636 appendix B, challenge as computed: True

case                                          server that ignores   server that enforces
the app exchanges its code and its verifier   tokens issued         tokens issued
a copy of the code arrives with no verifier   tokens issued         refused: verifier
a copy arrives with a verifier it made up     tokens issued         refused: verifier
the app's own code is used a second time      refused: code used    refused: code used
an authorization request with no challenge    tokens issued         refused: no challenge

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pkce.py

The challenge computed from the verifier matched the vector. The server that ignores PKCE issued tokens for a bare copy of the code, for a copy with a made-up verifier and for a request that carried no challenge. The one that enforces it refused all three and issued tokens only for the app’s own exchange. Both refused a code used a second time, because a code works once. RFC 9700 asks every public client to use PKCE and recommends it for confidential clients too.

Checkpoint

Check: The code flow with PKCE

  1. Question 1 of 2 In the first program, a copy of an authorization code arrived with no verifier. Why did the server that enforces PKCE refuse it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It stored the challenge with the code, and a verifier that hashes to that challenge is needed to swap the code

    The server that ignores PKCE issued tokens for 4 of the 5 requests, a bare copy of a code among them. The one that enforces it issued tokens for the app's own exchange only.

  2. Question 2 of 2 A web app wants to sign a person in with an identity provider. Which grant does the lesson choose?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The authorization code flow with PKCE

    The implicit grant and the password grant are the two that ASVS 10.4.4 no longer allows. The code flow replaces both.

Flashcards · 4 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What do OAuth 2.0 and OpenID Connect add, and what are the access token and the ID token?

    OAuth 2.0 lets an app act for a person at an API: the person signs in at an authorization server and the app receives an access token that the API accepts. OpenID Connect adds sign-in on top and also returns an ID token, a signed statement of who signed in.

  2. Which three random values tie the authorization code flow together?

    The state links the callback to the browser that made the request, the nonce binds the ID token to this sign-in, and the PKCE verifier binds the code to the app that asked for it.

  3. Which OAuth grant suits a person signing in, and which does RFC 9700 rule out?

    Authorization code with PKCE: the app receives a code that works once and swaps it for tokens, proving with the verifier that it asked. Client credentials serves a service that acts on its own behalf, with no person signing in. Implicit puts tokens in the redirect URL and clients should not use it, and the resource owner password credentials grant collects the person's password in the app and must not be used (RFC 9700).

  4. What does PKCE change for a stolen authorization code?

    In the lesson's run, a server that ignores PKCE handed out tokens for 4 of 5 requests, including a bare copy of the code, a copy with a made-up verifier and a request with no challenge; one that enforces it issued tokens only for the app's own exchange. A code works once on both. RFC 9700 asks every public client to use PKCE and recommends it for confidential clients too.

A callback is only as safe as the checks it runs Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • A redirect URI is compared as a whole string, never by prefix, pattern or wildcard
  • The ID token says who signed in, and the access token says what an API may do
  • Browser apps: keep the tokens on a server, or accept what a script can take
  • Link accounts by issuer and subject, never by an email address alone
  • Practise: a redirect check and a callback you can trust
    • Redirect URIs, compared as whole strings
    • A sign-in callback that checks before it signs anyone in
  • Interview questions
    • What does PKCE protect against, and who has to use it?
    • A pull request adds "Sign in with …". Its callback reads the code, exchanges it, takes the email from the ID token and signs the user in to the local account with that email. List what is missing, and the order in which you would fix it.
    • A single-page app signs people in with an identity provider and calls two of your own APIs and one partner API. Choose the architecture, say where each token lives, and name what an XSS hole would still allow.
    • Your app has password accounts. You add "Sign in with" a provider, and a person with a password account signs in with a provider that returns the same email address. Design the linking so that a provider sign-in can never land in another person's account, the first one included.
  • Key takeaways

1 diagram · 4 runnable examples (JavaScript, Python) · 2 exercises with sample tests · 3 interview answers · about 43 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Terms in this lesson

access token
A token that an app presents to an API to act for a person, limited by scope and by the audience it was made for. The API decides what it allows. The app treats it as an opaque value.
backend for frontend (BFF)
A server component between a browser app and the APIs it calls. It runs the sign-in flow and keeps the app's tokens itself, so that the browser holds only a session cookie and no token.
ID token
A signed JWT from an OpenID Connect provider that says who signed in: the issuer, the subject, the audience and the times. It is for the app that asked for it, and not for APIs.
mix-up attack
An attack on an app that works with more than one authorization server, in which a rogue server makes the app send a code or token meant for an honest server to the wrong place. The app defends by checking the issuer of every response.
PKCE
Proof Key for Code Exchange (RFC 7636). The app makes a random verifier, sends its SHA-256 hash, the challenge, with the authorization request, and sends the verifier itself when it swaps the code for tokens. A copy of the code is useless without it.
state
A random value that an app sends in an authorization request and checks again on the callback, tied to the person's browser session. It shows that the callback answers a request that this browser made.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.