Skip to content
Start with Identity
Protocols · Advanced

How OAuth 2.0 and OAuth 2.1 Work, Message by Message

By SWI Community Team · Updated 2026-11-05 · 15 min

OAuth 2.0 is a set of HTTP exchanges that end with an API receiving an access token and deciding whether to honor it. There are three grants worth knowing in 2026: authorization code with PKCE when a user is present, client credentials when no user is, and refresh to renew either without starting over. This page shows each message with fake but realistic values, explains every parameter with its section in RFC 6749 and the OAuth 2.1 draft (draft-17, 31 October 2026), and lists what the authorization server, the client and the API each have to check.

For the overview, the roles and status, see the OAuth 2.0 and OAuth 2.1 standard pages. For the identity layer and the ID token, see how OIDC works, message by message.

The sequence at a glance

# Message OAuth 2.0 OAuth 2.1 draft-17
1 Authorization request (browser) RFC 6749 §4.1.1 + RFC 7636 §4.3 §4.1.1
2 Authorization response (redirect with code) §4.1.2 §4.1.2
3 Token request (code exchange) §4.1.3 §3.2.2, §4.1.3
4 Token response §5.1, errors §5.2 §3.2.3–3.2.4
5 Client credentials request §4.4.2 §4.2
6 Refresh request §6 §4.3
7 API request with bearer token RFC 6750 §2.1 §5.1.1
8 API error RFC 6750 §3 §5.3

Messages 1 and 2: authorization request and response

GET /authorize?response_type=code
  &client_id=reports-web-example
  &redirect_uri=https%3A%2F%2Freports.example.com%2Foauth%2Fcallback
  &scope=invoices.read
  &state=7d1f2b9e-example
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &resource=https%3A%2F%2Fapi.example.com%2Finvoices HTTP/1.1
Host: as.example.com
Parameter Meaning Spec
response_type=code Only code survives in OAuth 2.1; token (implicit) is gone. RFC 6749 §4.1.1; RFC 9700 §2.1.2
client_id Registered client identifier. RFC 6749 §2.2
redirect_uri Compared by exact string match against registration (loopback ports excepted for native apps). RFC 9700 §2.1, §4.1.3; 2.1 §4.1.1
scope Space-delimited permissions requested. Design scopes per API action, not per app. RFC 6749 §3.3
state CSRF protection and client context. RFC 6749 §10.12
code_challenge, code_challenge_method=S256 PKCE. Required by default in 2.1; plain removed. RFC 7636 §4.2–4.3; 2.1 §4.1.1
resource Names the API the token is for, so the AS can audience-restrict it. RFC 8707 §2.1

The response is a redirect:

HTTP/1.1 302 Found
Location: https://reports.example.com/oauth/callback?code=Qcb0Orv1zh30vL1MPRsbm-example
  &state=7d1f2b9e-example&iss=https%3A%2F%2Fas.example.com

RFC 6749 §4.1.2 recommends codes live at most 10 minutes and says a code used twice must be refused, with tokens already issued from it revoked where possible. The iss parameter is RFC 9207; a client talking to more than one AS must check it to stop mix-up attacks (RFC 9700 §4.4).

Messages 3 and 4: token request and response

POST /token HTTP/1.1
Host: as.example.com
Authorization: Basic cmVwb3J0cy13ZWItZXhhbXBsZTpub3QtYS1yZWFsLXNlY3JldA==
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=Qcb0Orv1zh30vL1MPRsbm-example
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&redirect_uri=https%3A%2F%2Freports.example.com%2Foauth%2Fcallback

Authorization: Basic is client_secret_basic (RFC 6749 §2.3.1); stronger options are private_key_jwt (RFC 7523 §2.2) and mutual TLS (RFC 8705 §2). The redirect_uri line is required in OAuth 2.0 when it was sent in message 1, and dropped from the code exchange in OAuth 2.1 (draft §10). The verifier and challenge are the illustrative values from RFC 7636 Appendix B.

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImFzLWtleS0yMDI2LTEwIn0.eyJpc3MiOi...sig",
  "token_type": "Bearer",
  "expires_in": 600,
  "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA-example",
  "scope": "invoices.read"
}
Field Meaning Spec
access_token Opaque to the client. Here a JWT under RFC 9068, decoded below. RFC 6749 §5.1
token_type Bearer (RFC 6750) or DPoP (RFC 9449). RFC 6749 §7.1
expires_in Lifetime in seconds. RFC 6749 §5.1
refresh_token Long-lived credential for message 6. RFC 6749 §1.5
scope Granted scope, which may be narrower than requested. Read it. RFC 6749 §5.1, §3.3
Cache-Control: no-store Tokens must not be cached by intermediaries. RFC 6749 §5.1

An error looks like {"error":"invalid_grant","error_description":"code already used"} with HTTP 400 (RFC 6749 §5.2).

Message 5: client credentials (no user)

POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&scope=invoices.export
&resource=https%3A%2F%2Fapi.example.com%2Finvoices
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6ImJhdGNoLWtleS0xIn0.eyJpc3MiOiJiYXRjaC1leHBvcnRlci1leGFtcGxlIn0.c2ln

No browser, no code, no refresh token is expected (RFC 6749 §4.4.3). The client authenticates directly, here with a signed JWT whose iss and sub are its client_id and whose aud is this AS (RFC 7523 §3). Cache the token until shortly before expires_in rather than requesting one per call. For workloads that already hold a platform identity, workload identity federation replaces the client secret entirely.

Message 6: refresh, with rotation

POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA-example
&client_id=reports-web-example

With rotation the response carries a new refresh_token and the old one stops working. If the old one is presented again, the AS treats it as theft and revokes the whole family. For public clients, RFC 9700 §4.14.2 requires refresh tokens to be either sender-constrained or rotated, and OAuth 2.1 carries that forward (draft §10, §4.3.3). The scope of a new refresh token must equal the old one unless the client asked to reduce it.

Messages 7 and 8: calling the API

GET /invoices?status=open HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImFzLWtleS0yMDI2LTEwIn0.eyJpc3MiOi...sig

The header form is the one to use (RFC 6750 §2.1). Tokens in the URL query are forbidden in OAuth 2.1 and discouraged by RFC 9700 §4.3.2 because URLs end up in logs and Referer headers. A rejection looks like:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="invoices", error="invalid_token",
  error_description="token expired"

Error codes are invalid_request (400), invalid_token (401) and insufficient_scope (403) (RFC 6750 §3.1).

The JWT access token, decoded

{ "typ": "at+jwt", "alg": "ES256", "kid": "as-key-2026-10" }
{
  "iss": "https://as.example.com",
  "sub": "5ba552d67-example",
  "aud": "https://api.example.com/invoices",
  "client_id": "reports-web-example",
  "scope": "invoices.read",
  "exp": 1793887931,
  "iat": 1793887331,
  "jti": "dbe39bf3a3ba4238a513f51d6e1691c4-example"
}
Claim Meaning Spec
typ: at+jwt Distinguishes access tokens from ID tokens, so one cannot be used as the other. RFC 9068 §2.1
iss, exp, iat, jti Issuer, expiry, issue time, unique ID; all required. RFC 9068 §2.2
aud The resource indicator of this API. RFC 9068 §2.2, RFC 8707
sub The user, or the client itself for client credentials. RFC 9068 §2.2
client_id Which client obtained the token. RFC 9068 §2.2
scope What the token permits. RFC 9068 §2.2.3

Opaque tokens are checked with the introspection endpoint instead: POST /introspect with token=..., authenticated as the API, returns {"active":true,"scope":"invoices.read","client_id":"reports-web-example","aud":"https://api.example.com/invoices","exp":1793887931} (RFC 7662 §2.1–2.2).

Sender-constrained tokens with DPoP

With DPoP, the client sends a fresh proof JWT signed by its own key on every request, and the access token is bound to that key's thumbprint (cnf.jkt, RFC 9449 §6.1):

GET /invoices HTTP/1.1
Host: api.example.com
Authorization: DPoP eyJ0eXAiOiJhdCtqd3Qi...sig
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIn19.eyJqdGkiOiJlMWozVl9iS2ljOC1MQUVCIiwiaHRtIjoiR0VUIiwiaHR1IjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vaW52b2ljZXMifQ.c2ln

The proof's header has typ: dpop+jwt, alg and the public jwk; its payload has jti, htm (method), htu (URL), iat and, at an API, ath (hash of the access token) (RFC 9449 §4.2). A stolen DPoP-bound token is useless without the private key. Mutual TLS binding (RFC 8705 §3) achieves the same with a client certificate.

Who checks what

Authorization server

  1. redirect_uri exactly matches a registered value (RFC 9700 §2.1; 2.1 §4.1.1).
  2. PKCE: BASE64URL(SHA256(code_verifier)) equals the stored code_challenge, and plain is refused (RFC 7636 §4.6; 2.1 §10). Refusing requests without PKCE also blocks the PKCE downgrade.
  3. Codes are single-use, short-lived and bound to the client they were issued to (RFC 6749 §4.1.2, §4.1.3).

Client

  1. state matches, and iss matches when present (RFC 6749 §10.12, RFC 9207 §2.4).
  2. Read the granted scope in the token response, and store tokens server-side or in a backend-for-frontend, never in localStorage.

API (resource server), from RFC 9068 §4:

  1. typ is at+jwt; iss exactly matches the AS; aud contains this API's identifier.
  2. Signature verifies with the AS's published keys; alg is on your allow-list and never none.
  3. exp has not passed (small skew only), then enforce scope per endpoint. If the token is DPoP-bound, run the twelve proof checks in RFC 9449 §4.3, including ath.

Common implementation mistakes, with the CVEs and techniques behind them

Mistake What goes wrong Real examples
API accepts any token, or skips validation when a lookup fails Fabricated bearer tokens reach protected tools CVE-2026-59822 (LiteLLM MCP gateway, exploited)
Auth bypass rules matched against the wrong part of the URL A crafted query string skips authentication CVE-2025-54576 (OAuth2-Proxy)
Token endpoint URL built from tenant-controlled input The client secret is posted to an attacker's host CVE-2026-89111 (SAP Cloud SDK for Python)
Token cache keyed loosely One client's token attached to another client's request CVE-2025-26620 (Duende)
Signature and audience checks with gaps Forged or misdirected tokens accepted CVE-2026-55040 (SharePoint), CVE-2025-27371
Authorization policy evaluated incorrectly Scope or permission escalation CVE-2025-14778 (Keycloak UMA), technique: scope escalation through delegation
Long-lived, unbound refresh tokens Stolen refresh token works for weeks refresh token abuse, token replay at an unbound endpoint
Users approve a malicious client The protocol works perfectly for the attacker OAuth consent phishing, device code phishing

The last row is the hard one: consent and device-code phishing exploit OAuth working as designed. The defenses are admin-consent policies, publisher verification, blocking the device authorization grant where it is not needed, and short token lifetimes.

Try it

Paste an access token into the JWT decoder and check typ, iss, aud, exp and scope against the table above. If your authorization server also publishes OpenID metadata, run it through the OIDC discovery checker to confirm PKCE S256 support and the issuer string. The protect an API with OAuth scopes recipe turns the resource-server checks into code.

OAuth 2.0, OAuth 2.1, OIDC message flow, OAuth vs OIDC, implementing OAuth 2.0 and OIDC, MCP authorization. Glossary: access token, refresh token, bearer token, scope, token introspection, mTLS.

Frequently asked questions

What is OAuth 2.0, in one sentence?
OAuth 2.0 is the IETF framework (RFC 6749) that lets a client application obtain a scoped, expiring access token from an authorization server and present it to an API, so the application never handles the user's password and the API can check exactly what the client was allowed to do.
What messages make up the OAuth authorization code flow?
A browser redirect to the authorization endpoint carrying response_type=code, client_id, redirect_uri, scope, state and a PKCE code_challenge; a redirect back to the client with a one-time code; a back-channel POST to the token endpoint with the code and the PKCE code_verifier; a JSON token response; and then API calls carrying the access token in the Authorization header.
What does OAuth 2.1 change on the wire?
The draft OAuth 2.1 (draft-ietf-oauth-v2-1-17, October 2026) makes PKCE required by default, removes the implicit and resource owner password grants and the PKCE plain method, requires exact string matching of redirect URIs, forbids bearer tokens in query strings, requires public-client refresh tokens to be sender-constrained or one-time-use, and drops redirect_uri from the code exchange request.
What must an API check on every access token?
For JWT access tokens, RFC 9068 section 4 requires the resource server to check that typ is at+jwt, that iss exactly matches the authorization server, that aud names this resource server, that the signature verifies with the authorization server's keys and alg is not none, and that exp has not passed. Then it applies the scopes. For opaque tokens, the API calls the introspection endpoint defined in RFC 7662 and checks active, aud and scope.
Last reviewed By SWI Community TeamSuggest a correctionHow we research