Skip to content

Authorization code with PKCE

The flow for anything with a browser. Two requests, one redirect, and a verifier that makes a stolen code useless.

This is the flow for web applications, desktop applications and single-page applications. Everything with a browser uses it, whether or not it can keep a secret.

The shape#

  1. Generate a verifier and a challenge

    A random string, and its SHA-256, base64url encoded.

  2. Send the browser to the authorize endpoint

    With the challenge, a redirect URI, a state value and your scopes.

  3. The person signs in and consents

    On CEagle accounts, not in your application.

  4. They come back with a code

    To your redirect URI, along with the state you sent.

  5. Exchange the code for tokens

    Server to server, with the verifier.

Step 1: the verifier#

VERIFIER=$(openssl rand -base64 60 | tr -d '\n=+/' | cut -c1-64)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=')

Keep the verifier in the session. Send only the challenge.

Step 2: authorize#

GET/oauth2/authorizePublic
https://account.ceagle.cc/oauth2/authorize
  ?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=https%3A%2F%2Fsnake.example%2Fcallback
  &scope=openid%20profile%20email%20offline_access
  &state=RANDOM
  &nonce=RANDOM
  &code_challenge=CHALLENGE
  &code_challenge_method=S256
Parameters
response_typecodeRequired

Implicit and hybrid flows are not offered.

client_idRequired
redirect_uriRequired

Compared exactly against what you registered.

scope

Space separated. Include openid for an id token, offline_access for a refresh token.

state

Opaque, echoed back untouched. Generate it per attempt and verify it on the way back. That is what stops a cross-site request forgery on your callback.

nonce

Echoed into the id token. Bind it to the session to stop token replay.

code_challenge
code_challenge_methodS256

Plain is not accepted.

promptnone | login | consent

login forces a fresh sign-in. none fails rather than asking, which is how you check for an existing session without interrupting anyone.

How errors come back#

This endpoint answers with a redirect, never with JSON.

An error that can be attributed to a registered redirect URI is delivered there, as error and error_description. One that cannot — an unknown client, an unregistered redirect URI — is rendered as a page instead, because sending it onward would turn the authorization endpoint into an open redirector.

Step 3: exchange#

POST/oauth2/tokenClient

Form-encoded, never JSON.

curl -s -X POST https://account.ceagle.cc/oauth2/token \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri=https://snake.example/callback \
  -d client_id="$CLIENT_ID" \
  -d code_verifier="$VERIFIER"
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 600,
  "refresh_token": "…",
  "id_token": "…",
  "scope": "openid profile email offline_access"
}

Codes are single use and short-lived. A second exchange answers invalid_grant.

Step 4: use it#

curl -s https://account.ceagle.cc/oauth2/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Claims are filtered to what the token's scopes allow. sub is always there; preferred_username and name need profile; email and email_verified need email.

Verifying tokens

Checking an id token yourself, and when to introspect instead.

Read on

A checklist#

Before you ship
state is verifiedRequired

Not just sent. Compare it to what you stored, and reject a mismatch.

nonce is verifiedRequired

Against the id token, if you use one.

the verifier is per attemptRequired

Never reused, never derived from the client id.

redirect URIs are exactRequired

Including the scheme and any trailing slash.

tokens are not in the URLRequired

Never in a query string, a fragment you log, or an analytics event.

Common errors#

StatusCodeMeans
400invalid_grantThe code is used, expired, or was issued to a different client. Also a mismatched verifier or redirect URI.
401invalid_clientClient authentication failed. A confidential client sent the wrong secret, or a public client sent one it should not have.
400invalid_requestSomething is missing. Usually redirect_uri on the exchange, or code_verifier.
400unauthorized_clientThe application is disabled, or its project is archived.
400invalid_scopeA scope that does not exist, or one this application was not granted.
CEagle documentation