Cheatsheet

OAuth 2.0 Flows Cheatsheet

This cheatsheet is a lookup for OAuth 2.0 grant types and the exact parameters each request carries. It is for developers who must pick a flow for a web app, mobile app, server job or TV app, and then write the requests by hand. The confusion it clears up is that "OAuth flow" means a grant type, and only a few of the original four are still recommended. For the concepts behind it, see the OAuth 2.0 glossary page.

Quick reference

Which grant type to use

Situation Grant type `grant_type` value Status
Server-side, SPA or mobile app with a user Authorization code, with PKCE `authorization_code` Recommended
Service calling an API as itself Client credentials `client_credentials` Fine for confidential clients
Renewing an expired access token Refresh token `refresh_token` Fine, rotate for public clients
TV, console or CLI with no browser Device authorization (RFC 8628) `urn:ietf:params:oauth:grant-type:device_code` Fine
Exchange one token for another Token exchange (RFC 8693) `urn:ietf:params:oauth:grant-type:token-exchange` Fine
Assertion signed by a trusted issuer JWT bearer (RFC 7523) `urn:ietf:params:oauth:grant-type:jwt-bearer` Fine
Browser gets the token directly Implicit none, `response_type=token` Should not be used (RFC 9700)
App collects the user's password Resource owner password `password` Must not be used (RFC 9700)

RFC 6749 from 2012 defined authorization code, implicit, password and client credentials. RFC 9700 from January 2025 is the current security guidance.

Authorization endpoint request (browser redirect, GET)

Parameter Required Value
`response_type` Yes `code`
`client_id` Yes Your registered client ID
`redirect_uri` Depends Must match a registered URI exactly
`scope` No Space-delimited, so `read write`
`state` Recommended Random value you compare on the callback
`code_challenge` With PKCE Base64url of SHA-256 of the verifier
`code_challenge_method` With PKCE `S256`

Token endpoint request (POST, form-encoded)

Grant Parameters
`authorization_code` `code`, `redirect_uri`, `client_id`, `code_verifier`
`client_credentials` `scope` (optional)
`refresh_token` `refresh_token`, `scope` (optional, can only narrow)
device code `device_code`, `client_id`

The body is application/x-www-form-urlencoded, not JSON. A confidential client authenticates with HTTP Basic or by sending client_id and client_secret in the body.

Token endpoint success response

Field Required Notes
`access_token` Yes Opaque string or a JWT
`token_type` Yes Usually `Bearer`, compared case-insensitively
`expires_in` Recommended Lifetime in seconds, for example `3600`
`refresh_token` No Not returned for client credentials
`scope` If different Required when it differs from the request

Error codes

Where `error` Meaning
Token endpoint `invalid_request` Missing or repeated parameter, malformed request
Token endpoint `invalid_client` Client authentication failed
Token endpoint `invalid_grant` Code or refresh token is invalid, expired, revoked, issued to another client, or the `redirect_uri` differs
Token endpoint `unauthorized_client` Client may not use this grant type
Token endpoint `unsupported_grant_type` Server does not support the grant
Token endpoint `invalid_scope` Scope is unknown or exceeds the grant
Authorization endpoint `access_denied` User or server refused
Authorization endpoint `unsupported_response_type` Server cannot issue that response type
Authorization endpoint `server_error`, `temporarily_unavailable` Server-side failure, retry later
Device flow `authorization_pending` User has not finished yet, keep polling
Device flow `slow_down` Add 5 seconds to the polling interval
Device flow `expired_token` The device code expired, start over
Resource server `invalid_token` Sent with HTTP 401 (RFC 6750)
Resource server `insufficient_scope` Sent with HTTP 403 (RFC 6750)

Common patterns

Build the authorization URL with PKCE

import urllib.parse as u
print(u.urlencode({'response_type': 'code', 'client_id': 's6BhdRkqt3',
  'redirect_uri': 'https://app.example.com/cb', 'scope': 'read write',
  'state': 'af0ifjsldkj',
  'code_challenge': 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
  'code_challenge_method': 'S256'}))
response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcb&scope=read+write&state=af0ifjsldkj&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256

Append this after ? on the authorization endpoint. Use urlencode rather than string concatenation so the redirect URI is percent-encoded.

Generate a PKCE verifier and challenge

import base64, hashlib, secrets
verifier = secrets.token_urlsafe(32)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode('ascii')).digest()).rstrip(b'=').decode()
print(len(verifier), len(challenge))
rfc = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
print(base64.urlsafe_b64encode(hashlib.sha256(rfc.encode()).digest()).rstrip(b'=').decode())
43 43
E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

The verifier must be 43 to 128 characters from the unreserved set (RFC 7636). The second line is the RFC 7636 test vector. Keep the verifier in the session and send it only to the token endpoint.

Read the callback

import urllib.parse as u
cb = 'https://app.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj'
print(u.parse_qs(u.urlparse(cb).query))
{'code': ['SplxlOBeZQQYbYS6WxSbIA'], 'state': ['af0ifjsldkj']}

Compare state to the value you stored before redirecting, and reject the request on any mismatch.

Client credentials request on the wire

curl -u 's6BhdRkqt3:7Fjfp0ZBr1KtDRbnfVdmIw' -d grant_type=client_credentials -d scope=read https://auth.example.com/token

This request was sent to a local listener, which captured:

POST /token HTTP/1.1
Host: 127.0.0.1:8765
Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
User-Agent: curl/8.5.0
Accept: */*
Content-Length: 40
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=read

The Basic value is base64 of client_id:client_secret. RFC 6749 says a refresh token should not be included in this response.

Refresh, exchange and revoke bodies

from urllib.parse import urlencode as e
print(e({'grant_type': 'refresh_token', 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA', 'scope': 'read'}))
print(e({'grant_type': 'urn:ietf:params:oauth:grant-type:device_code', 'device_code': 'GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS', 'client_id': '459691054427'}))
print(e({'token': '45ghiukldjahdnhzdauz', 'token_type_hint': 'refresh_token'}))
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA&scope=read
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS&client_id=459691054427
token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token

The third body goes to the revocation endpoint from RFC 7009. Grant type URNs contain colons, so they must be percent-encoded in form bodies.

Poll the device flow correctly

interval = 5
for err in ['authorization_pending', 'slow_down', 'authorization_pending']:
    if err == 'slow_down':
        interval += 5
    print(err, '-> wait', interval, 'seconds')
authorization_pending -> wait 5 seconds
slow_down -> wait 10 seconds
authorization_pending -> wait 10 seconds

Use 5 seconds when the server gives no interval. After slow_down, the longer interval stays for every later request.

Pitfalls

  • Sending JSON to the token endpoint: RFC 6749 requires a form-encoded body. A JSON body usually returns invalid_request. Send Content-Type: application/x-www-form-urlencoded.
  • Reusing an authorization code: a code is single-use and short-lived, and RFC 6749 says a replay should revoke tokens already issued from it. A second exchange returns invalid_grant.
  • invalid_grant from a redirect mismatch: the redirect_uri in the token request must equal the one in the authorization request, character for character. A trailing slash is enough to fail.
  • Putting the access token in the URL: RFC 6750 allows ?access_token= but says it should not be used when a header is possible, because URLs end up in logs. Send Authorization: Bearer <token>.
  • Wildcard redirect URIs: RFC 9700 requires exact string matching, with a port exception for native app loopback addresses. Prefix matching lets an attacker steal the code.
  • Skipping PKCE on confidential clients: RFC 9700 requires PKCE for public clients and recommends it for confidential ones too. Use S256; plain is only for clients that cannot hash.
  • Embedding a client secret in a SPA or mobile app: anyone can extract it, so the client is public. Register it as public and rely on PKCE.
  • Long-lived refresh tokens with no protection: RFC 9700 says refresh tokens for public clients must be sender-constrained or rotated, so each use returns a new one.
  • Treating scope as a list with commas: scopes are separated by a single space. In a query string that space appears as + or %20.

Related ZipKit tools

  • JWT Decoder — inspects access and ID tokens when the server issues JWTs
  • URL Encode / Decode — percent-encodes redirect URIs and grant type URNs
  • Query String ↔ JSON — turns a callback or authorization query string into an object and back
  • Base64 Encode / Decode — builds and checks the Basic authorization value

Related cheatsheets