The guide

Which flow should I choose?

A "flow" (or "grant") is the way an application obtains its tokens. There are several because applications are very different: a website, a mobile app, a TV, a service without a user… do not have the same constraints or the same secrets to protect.

See also: the flows as sequence diagrams · advanced features explained (PAR, JAR, JARM, DPoP, CIBA) · the OAuth and OpenID Connect timeline

In two questions

  1. Is there a person signing in? No, it is a service talking to another service → Client Credentials.
  2. Yes. Where does the application run?
    • Website, mobile application, single-page application (SPA), desktop application, with a browser → Authorization Code + PKCE. That is the answer in # % of cases.
    • Device with no browser or keyboard (TV, console, connected object, command-line tool) → Device Code.

Everything else exists for historical reasons or specific needs. Refresh Token is not a sign-in flow: it is a complement to keep the session alive.

Why so many flows?

OAuth 2.0 (2012) had to serve both servers able to keep a secret and browser applications that cannot. Hence implicit for SPAs, password for “trusted” applications, and code for servers. Over the years it became clear that tokens in the URL leaked (implicit) and that handing the password to the application defeated the whole point (password). OAuth 2.1 and current security recommendations have therefore brought everything back to code + PKCE, keeping client credentials and device for their own cases. OIDC added the hybrid flow to get the identity straight away.

The flows, one by one

Authorization Code + PKCEThe standard flow for any person signing in.Recommended

Use it when: a person signs in to a website (with or without a server), a SPA, a mobile or desktop application. With a server able to keep a secret, add the client_secret; without one (SPA, mobile), PKCE is enough.

Why it exists: the browser only receives a single-use, short-lived code; the tokens go directly from the application’s server to the OP (“back channel”). PKCE ties the code to whoever asked for it, so an intercepted code is useless to anyone else.

Pitfalls: forgetting PKCE (now required even for confidential clients), not checking state and nonce, accepting overly broad redirect_uris, tokens in localStorage for a SPA.

Walkthrough:

  1. The application generates code_verifier (a random secret), state, nonce, and computes code_challenge = SHA-256(code_verifier).
  2. It sends the person to /authorize?response_type=code&code_challenge=…&state=…&nonce=….
  3. The OP authenticates the person, then sends them back to redirect_uri?code=…&state=….
  4. The application calls /token with the code and the code_verifier (and its secret if it has one).
  5. It receives the ID token, access token (and refresh token), then checks the signature, iss, aud and nonce.
Hybrid (code id_token, code token…)Code plus ID token directly in the redirect.Specific cases

Use it when: the application needs the identity immediately (before the code exchange), for example for server-side session validation or because an older library (ASP.NET, Java EE) configures it by default.

Why it exists: OIDC wanted to let you read the identity straight away while keeping the code to obtain tokens safely. The c_hash and at_hash in the ID token bind the code and the access token to that identity.

Pitfalls: tokens in the URL fragment (visible to history and scripts), unchecked c_hash/at_hash, and a forgotten mandatory nonce. Prefer the plain code flow unless something forces you not to.

Walkthrough:

  1. /authorize?response_type=code id_token&nonce=… (or code token, code id_token token).
  2. The OP returns #code=…&id_token=…&state=… in the fragment (response_mode=fragment by default) or by POST (form_post).
  3. The application checks the ID token and its c_hash.
  4. It exchanges the code for the other tokens, as in the code flow.
Implicit (id_token, id_token token)The tokens arrive directly in the URL.Discouraged

Use it when: almost never for new work. You meet it in old SPAs, or to check that an application really forbids it.

Why it exists: before browsers handled cross-origin calls (CORS) and PKCE, a SPA could not call /token, so it was given the tokens directly.

Pitfalls: the token travels in the URL: history, proxy logs, the Referer header, extensions. No refresh token, no binding to the caller. Removed from OAuth 2.1.

Walkthrough:

  1. /authorize?response_type=id_token token&nonce=….
  2. The OP returns #id_token=…&access_token=…&state=….
  3. A script on the page reads the fragment and verifies the token.
Client CredentialsNo person: the service authenticates itself.Machine to machine

Use it when: a service, a scheduled job or a daemon calls an API in its own name: synchronisation, nightly job, microservices talking to each other.

Why it exists: many API calls have no person behind them. The client proves its identity (secret, or better: a signed assertion) and receives an access token limited to its scopes.

Pitfalls: no ID token or refresh token (that is normal). Never put this secret in a distributed application (mobile, SPA). Grant the bare minimum of scopes and rotate the secret.

Walkthrough:

  1. POST /token with grant_type=client_credentials, scope=… and the client authentication (Basic secret or in the body).
  2. The OP responds with an access token.
  3. The service sends it to the API: Authorization: Bearer ….
Refresh TokenRenew an access token without asking the person again.Complement

Use it when: you want short-lived access tokens (a few minutes) while keeping the person signed in for a long time. Requested with the offline_access scope.

Why it exists: the longer a token lives, the worse its theft. The refresh token stays on the application side, away from the APIs, and is only used to renew.

Pitfalls: a stolen refresh token means prolonged access: safe storage, rotation (a new one on each use, the old one becomes invalid) and reuse detection.

Walkthrough:

  1. POST /token with grant_type=refresh_token&refresh_token=….
  2. The OP returns a new access token (and often a new refresh token, the old one being revoked).
Device Code (RFC 8628)Sign in on another device.Devices without a browser

Use it when: TV, console, connected object, printer, command-line tool: the device has no browser or convenient keyboard.

Why it exists: typing a password with a remote control is painful and risky. A short code is displayed; the person enters it on their phone or computer, where they authenticate normally.

Pitfalls: short-lived, single-use code, no polling too fast (slow_down), and attempt limits so a code can’t be guessed. Phishing is possible: show clearly what the person is authorising.

Walkthrough:

  1. The device calls /device_authorization and receives device_code, user_code and the verification address.
  2. It displays “go to … and enter ABCD-EFGH”.
  3. The person authenticates and enters the code on their phone.
  4. Meanwhile the device polls /token with the device_code. First authorization_pending, then the tokens.
Resource Owner Password CredentialsThe application receives the password directly.Discouraged

Use it when: to be avoided. Sometimes tolerated for a migration from an old system or automated tests in a controlled environment.

Why it exists: a transition for applications that already collected passwords, without changing their interface.

Pitfalls: the application sees the password: exactly what OIDC tries to avoid. No multi-factor authentication, no SSO, easier phishing. Removed from OAuth 2.1.

Walkthrough:

  1. POST /token with grant_type=password&username=…&password=….
  2. The OP responds with the tokens, or refuses if a second factor is required.

Also in the wild

  • private_key_jwt and JWT Bearer (RFC 7523): a service proves its identity with a signed assertion instead of a shared secret. Ideal for system-to-system exchanges. In ID-Kit on the test provider side; not yet on the test application side.
  • Token Exchange (RFC 8693): exchange one token for another, for an API that calls another API on behalf of the person. In ID-Kit.
  • PAR and JAR (RFC 9126 and 9101): the authorization request is pushed server to server, or signed. In ID-Kit: test provider (PAR and JAR) and test application (PAR).
  • CIBA : authentication happens on another channel, initiated by the server (call center, bank). In ID-Kit (test provider, poll mode).

All the details in pictures: sequence diagrams.

The protections that go with it

  • PKCE: proves that whoever exchanges the code is the one who asked for it.
  • state: ties the response to the request, against CSRF.
  • nonce: ties the ID token to the application’s session, against replay.
  • Exact redirect_uri: the OP only sends the code to a registered address, character for character.

Test each flow. ID-Kit’s test provider can play all these flows, with simulated errors; the test application runs them against your real provider.