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#
Generate a verifier and a challenge
A random string, and its SHA-256, base64url encoded.
Send the browser to the authorize endpoint
With the challenge, a redirect URI, a state value and your scopes.
The person signs in and consents
On CEagle accounts, not in your application.
They come back with a code
To your redirect URI, along with the state you sent.
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#
/oauth2/authorizePublichttps://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=S256response_typecodeRequiredImplicit and hybrid flows are not offered.
client_idRequiredredirect_uriRequiredCompared exactly against what you registered.
scopeSpace separated. Include
openidfor an id token,offline_accessfor a refresh token.stateOpaque, 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.
nonceEchoed into the id token. Bind it to the session to stop token replay.
code_challengecode_challenge_methodS256Plain is not accepted.
promptnone | login | consentloginforces a fresh sign-in.nonefails 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#
/oauth2/tokenClientForm-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"curl -s -X POST https://account.ceagle.cc/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri=https://snake.example/callback \
-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.
Checking an id token yourself, and when to introspect instead.
A checklist#
state is verifiedRequiredNot just sent. Compare it to what you stored, and reject a mismatch.
nonce is verifiedRequiredAgainst the id token, if you use one.
the verifier is per attemptRequiredNever reused, never derived from the client id.
redirect URIs are exactRequiredIncluding the scheme and any trailing slash.
tokens are not in the URLRequiredNever in a query string, a fragment you log, or an analytics event.
Common errors#
| Status | Code | Means |
|---|---|---|
| 400 | invalid_grant | The code is used, expired, or was issued to a different client. Also a mismatched verifier or redirect URI. |
| 401 | invalid_client | Client authentication failed. A confidential client sent the wrong secret, or a public client sent one it should not have. |
| 400 | invalid_request | Something is missing. Usually redirect_uri on the exchange, or code_verifier. |
| 400 | unauthorized_client | The application is disabled, or its project is archived. |
| 400 | invalid_scope | A scope that does not exist, or one this application was not granted. |