privileged.id

Tool · OAuth 2.0

OAuth flow visualiser

Step through the authorization code flow with PKCE, the client credentials grant and the device authorization grant, one HTTP message at a time, with what each step protects against.

Runs in your browser. Nothing you enter is sent or stored.Reviewed

Authorization code + PKCE. Any client that acts for a signed-in user: web apps, single-page apps, mobile and desktop apps. The default choice (RFC 9700 §2.1.1).
Sequence diagram: Authorization code + PKCE, 12 steps between Browser (user), Client app, Authorization server, Resource server (API)Browser (user)Client appAuthorizationserverResource server(API)1. User clicks “Connect”1User clicks “Connect”2. Make verifier, challenge, state2Make verifier, challenge, state3. 302 to the authorization endpoint3302 to the authorization endpoint4. GET /authorize4GET /authorize5. Sign in and consent5Sign in and consent6. 302 back with code6302 back with code7. Browser delivers the code7Browser delivers the code8. POST /token with code_verifier8POST /token with code_verifier9. Check verifier against challenge9Check verifier against challenge10. Tokens10Tokens11. Call the API with the token11Call the API with the token12. 200 OK12200 OK
Client credentials. Machine-to-machine calls where no user is involved: a back-end job calling an internal API. The client acts on its own behalf.
Sequence diagram: Client credentials, 5 steps between Client (service), Authorization server, Resource server (API)Client (service)AuthorizationserverResource server(API)1. POST /token, client authenticates1POST /token, client authenticates2. Authenticate client, check scope2Authenticate client, check scope3. Access token (no refresh token)3Access token (no refresh token)4. Call the API4Call the API5. 201 Created5201 Created
Device authorization. Devices with no browser or no easy keyboard: TVs, CLI tools, consoles. The user approves on a second device.
Sequence diagram: Device authorization, 8 steps between Device (TV / CLI), User's phone or laptop, Authorization server, Resource server (API)Device(TV / CLI)User's phone orlaptopAuthorizationserverResource server(API)1. POST /device_authorization1POST /device_authorization2. device_code + user_code2device_code + user_code3. Show code and URL3Show code and URL4. Sign in, enter code, approve4Sign in, enter code, approve5. Poll /token5Poll /token6. authorization_pending6authorization_pending7. Tokens, once approved7Tokens, once approved8. Call the API8Call the API

Every step, message by message

The same flows written out in full. PKCE values here are the published example from RFC 7636 Appendix B; the interactive version above generates fresh ones in your browser. Host names are examples. Line breaks inside URLs and form bodies are for display only.

Authorization code + PKCE

Any client that acts for a signed-in user: web apps, single-page apps, mobile and desktop apps. The default choice (RFC 9700 §2.1.1).

  1. User clicks “Connect” (Browser (user) → Client app)

    GET /connect HTTP/1.1
    Host: client.example.org

    The user asks the client to do something that needs their data at the API. RFC 6749 §1.2

  2. Make verifier, challenge, state (Client app)

    code_verifier  = dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
    code_challenge = BASE64URL(SHA-256(code_verifier))
                   = E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    state          = af0ifjsldkj

    The verifier is a high-entropy random string (43–128 characters) kept on the client. Only its SHA-256 hash travels in the browser. state is a one-time value bound to this browser session. Risk: Never reuse a verifier or send method 'plain': S256 is the only method that keeps the verifier out of the authorization request (RFC 9700 §2.1.1). RFC 7636 §4.1–4.2

  3. 302 to the authorization endpoint (Client app → Browser (user))

    HTTP/1.1 302 Found
    Location: https://as.example.com/authorize
      ?response_type=code
      &client_id=s6BhdRkqt3
      &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
      &scope=openid%20orders.read
      &state=af0ifjsldkj
      &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
      &code_challenge_method=S256

    Line breaks are for display only. The request asks for a code (not a token), names the registered redirect URI and the scopes, and carries the PKCE challenge. RFC 6749 §4.1.1

  4. GET /authorize (Browser (user) → Authorization server)

    GET /authorize?response_type=code&client_id=s6BhdRkqt3&… HTTP/1.1
    Host: as.example.com

    The authorization server checks client_id and compares redirect_uri to the registered value by exact string match. Risk: Pattern or prefix matching of redirect URIs lets an attacker steer the code to a URL they control (RFC 9700 §4.1). RFC 9700 §2.1

  5. Sign in and consent (Authorization server → Browser (user))

    (login page, MFA, consent screen:
     “client.example.org wants to read your orders”)

    The user authenticates to the authorization server, never to the client: the client never sees the password. The consent screen lists what the scopes allow. RFC 6749 §4.1 (B)

  6. 302 back with code (Authorization server → Browser (user))

    HTTP/1.1 302 Found
    Location: https://client.example.org/cb
      ?code=SplxlOBeZQQYbYS6WxSbIA
      &state=af0ifjsldkj
      &iss=https%3A%2F%2Fas.example.com

    The code is short-lived and single-use. The iss parameter (RFC 9207) tells the client which authorization server answered, a defence against mix-up attacks when a client talks to several. Risk: The code passes through the browser, its history and possibly logs. On its own it is useless: redeeming it needs the code_verifier. RFC 6749 §4.1.2

  7. Browser delivers the code (Browser (user) → Client app)

    GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj&iss=https%3A%2F%2Fas.example.com HTTP/1.1
    Host: client.example.org

    The client checks that state matches the value it stored for this browser session, and that iss is the server it sent the user to. Risk: Skipping the state (or PKCE) check allows CSRF: an attacker logs the victim into the attacker's account (RFC 9700 §4.7). RFC 9700 §4.7

  8. POST /token with code_verifier (Client app → Authorization server)

    POST /token HTTP/1.1
    Host: as.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code
    &code=SplxlOBeZQQYbYS6WxSbIA
    &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
    &client_id=s6BhdRkqt3
    &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

    A back-channel request, client to server. A confidential client also authenticates here (client secret, private_key_jwt or mTLS); a public client cannot hold a secret, so PKCE is what protects it. RFC 7636 §4.5

  9. Check verifier against challenge (Authorization server)

    BASE64URL(SHA-256("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"))
      == "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"  ✓
    code unused, unexpired, issued to s6BhdRkqt3  ✓
    redirect_uri identical to the authorization request  ✓

    Only the party that started the flow knows the verifier, so a stolen or injected code cannot be redeemed by anyone else. Risk: Servers MUST reject a code_verifier when the authorization request had no code_challenge, or attackers can downgrade (RFC 9700 §4.8). RFC 7636 §4.6

  10. Tokens (Authorization server → Client app)

    HTTP/1.1 200 OK
    Content-Type: application/json
    Cache-Control: no-store
    
    {
      "access_token": "2YotnFZFEjr1zCsicMWpAA",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
      "id_token": "eyJhbGciOiJSUzI1NiIs…",
      "scope": "openid orders.read"
    }

    An ID token appears because the openid scope made this an OpenID Connect request: it tells the client who signed in. The access token is for the API. Risk: Refresh tokens for public clients must be sender-constrained or rotated on every use (RFC 9700 §4.14.2). RFC 6749 §5.1

  11. Call the API with the token (Client app → Resource server (API))

    GET /orders HTTP/1.1
    Host: api.example.com
    Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA

    Bearer tokens go in the Authorization header, not in the URL. Risk: Anyone holding a bearer token can use it. DPoP or mTLS binding makes a stolen token useless without the client's key (RFC 9700 §2.2). RFC 6750 §2.1

  12. 200 OK (Resource server (API) → Client app)

    HTTP/1.1 200 OK
    Content-Type: application/json
    
    [{"order": 1042, "status": "shipped"}]

    Before answering, the API validates the token (signature or introspection, issuer, audience, expiry) and checks the scope covers this request. RFC 9068 §4

Client credentials

Machine-to-machine calls where no user is involved: a back-end job calling an internal API. The client acts on its own behalf.

  1. POST /token, client authenticates (Client (service) → Authorization server)

    POST /token HTTP/1.1
    Host: as.example.com
    Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=client_credentials
    &scope=reports.write

    The client proves its identity directly. Basic authentication with a shared secret is shown; private_key_jwt (RFC 7523) or mutual TLS (RFC 8705) avoid sending a reusable secret. Risk: A client secret in source code, CI logs or a container image is a standing credential for everything the client can reach. RFC 6749 §4.4.2

  2. Authenticate client, check scope (Authorization server)

    client s6BhdRkqt3 authenticated  ✓
    scope reports.write allowed for this client  ✓

    The grant is only for confidential clients: anyone could otherwise request tokens as the client. RFC 6749 §4.4

  3. Access token (no refresh token) (Authorization server → Client (service))

    HTTP/1.1 200 OK
    Content-Type: application/json
    Cache-Control: no-store
    
    {
      "access_token": "2YotnFZFEjr1zCsicMWpAA",
      "token_type": "Bearer",
      "expires_in": 3600
    }

    A refresh token SHOULD NOT be included: the client can simply authenticate again. Cache the token until shortly before it expires instead of fetching one per call. RFC 6749 §4.4.3

  4. Call the API (Client (service) → Resource server (API))

    POST /reports HTTP/1.1
    Host: api.example.com
    Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
    Content-Type: application/json
    
    {"period": "2026-Q3"}

    The API sees a token whose subject is the client itself, not a user. RFC 6750 §2.1

  5. 201 Created (Resource server (API) → Client (service))

    HTTP/1.1 201 Created
    Location: /reports/77

    Scope the client to exactly the APIs and operations it needs: a service identity is a privileged account like any other. SP 800-53 AC-6

Device authorization

Devices with no browser or no easy keyboard: TVs, CLI tools, consoles. The user approves on a second device.

  1. POST /device_authorization (Device (TV / CLI) → Authorization server)

    POST /device_authorization HTTP/1.1
    Host: server.example.com
    Content-Type: application/x-www-form-urlencoded
    
    client_id=1406020730&scope=example_scope

    The device asks for a pair of codes. It should start only when the user asks, not automatically. RFC 8628 §3.1

  2. device_code + user_code (Authorization server → Device (TV / CLI))

    HTTP/1.1 200 OK
    Content-Type: application/json
    Cache-Control: no-store
    
    {
      "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
      "user_code": "WDJB-MJHT",
      "verification_uri": "https://example.com/device",
      "verification_uri_complete":
          "https://example.com/device?user_code=WDJB-MJHT",
      "expires_in": 1800,
      "interval": 5
    }

    The device_code stays on the device; the short user_code is for the human. Values from the RFC's own example. RFC 8628 §3.2

  3. Show code and URL (Device (TV / CLI) → User's phone or laptop)

    To sign in, visit  example.com/device
    and enter  WDJB-MJHT
    (or scan the QR code)

    The user reads the code off the screen and continues on a phone or laptop. Risk: Attackers can start a device flow themselves and send the victim the code with a pretext (“enter this code to view the document”). RFC 8628 §5.4 recommends telling the user they are authorizing a device and asking them to confirm it is theirs. RFC 8628 §3.3

  4. Sign in, enter code, approve (User's phone or laptop → Authorization server)

    GET /device HTTP/1.1
    Host: example.com
    
    (sign in, MFA, enter WDJB-MJHT, approve access)

    Authentication happens on a device with a full browser, so phishing-resistant methods like passkeys still work. RFC 8628 §3.3

  5. Poll /token (Device (TV / CLI) → Authorization server)

    POST /token HTTP/1.1
    Host: server.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code
    &device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
    &client_id=1406020730

    Meanwhile the device polls, waiting at least `interval` seconds between requests. RFC 8628 §3.4

  6. authorization_pending (Authorization server → Device (TV / CLI))

    HTTP/1.1 400 Bad Request
    Content-Type: application/json
    Cache-Control: no-store
    
    {"error": "authorization_pending"}

    Not yet approved: keep polling. slow_down means add 5 seconds to the interval for this and every later request; access_denied and expired_token end the flow. RFC 8628 §3.5

  7. Tokens, once approved (Authorization server → Device (TV / CLI))

    HTTP/1.1 200 OK
    Content-Type: application/json
    Cache-Control: no-store
    
    {
      "access_token": "2YotnFZFEjr1zCsicMWpAA",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
    }

    The next poll after approval returns a normal token response. RFC 8628 §3.5

  8. Call the API (Device (TV / CLI) → Resource server (API))

    GET /me/library HTTP/1.1
    Host: api.example.com
    Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA

    From here it is ordinary OAuth. RFC 6750 §2.1

Which flow to use

SituationFlowWhy
A user signs in to a web app, single-page app, mobile or desktop appAuthorization code + PKCETokens never pass through the browser's URL; PKCE stops stolen or injected codes. RFC 9700 requires PKCE for public clients and recommends it for confidential ones.
A service calls an API on its own behalfClient credentialsNo user is involved. Use a strong client authentication method and narrow scopes.
A TV, console or command-line tool with no usable browserDevice authorizationThe user approves on a phone or laptop with a full browser.
Anything still using the implicit grantMove to code + PKCERFC 9700 §2.1.2: clients SHOULD NOT use the implicit grant, which puts access tokens in the URL.
Apps collecting the user's password to exchange for tokensMove to code + PKCERFC 9700 §2.4: the resource owner password credentials grant MUST NOT be used.

Sources

  1. RFC 6749: The OAuth 2.0 Authorization Framework, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  2. RFC 7636: Proof Key for Code Exchange by OAuth Public Clients, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  3. RFC 8628: OAuth 2.0 Device Authorization Grant, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  4. RFC 9700: Best Current Practice for OAuth 2.0 Security, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  5. RFC 9207: OAuth 2.0 Authorization Server Issuer Identification, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  6. RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage, IETF. IETF Trust Legal Provisions (brief quotations with attribution).