Add a web application¶
The authorization code flow with PKCE, end to end. This is the flow for anything with a person in front of it: a single-page app, a server-rendered site, a mobile app.
1. Register the client¶
curl -X POST http://localhost:8000/admin/clients \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clientId": "library",
"name": "Library",
"clientType": "public",
"allowedGrants": ["authorization_code", "refresh_token"],
"redirectUris": ["https://library.example.org/callback"],
"postLogoutRedirectUris": ["https://library.example.org/"],
"backchannelLogoutUri": "https://library.example.org/backchannel-logout",
"skipConsent": false,
"grantableScopeIds": ["<scope uuid>", "..."]
}'
| Choice | Guidance |
|---|---|
clientType |
public for anything running in a browser or on a phone — it cannot keep a secret, and PKCE is what protects it. confidential only for a backend. |
redirectUris |
Matched exactly. No wildcards, no prefix matching. |
skipConsent |
true only for applications your organization owns. See Consent. |
grantableScopeIds |
What this application may request. It still only receives what the person actually holds. |
2. Send the person to IDEN¶
Generate a PKCE pair — a random code_verifier, and its SHA-256 hash, base64url-encoded without
padding, as the code_challenge.
GET /oauth2/authorize
?response_type=code
&client_id=library
&redirect_uri=https://library.example.org/callback
&scope=openid profile email offline_access library:loans:read
&state=<random, tied to this browser session>
&nonce=<random, checked in the ID token later>
&code_challenge=<the hash>
&code_challenge_method=S256
state is not optional
It is what ties the response back to the request this browser started. Without checking it, an attacker can hand a victim's browser their own authorization code and have the victim's account linked to it. Generate it randomly, store it against the session, and compare on return.
IDEN either redirects straight back with a code — that is single sign-on — or sends the person to the sign-in page first.
3. Exchange the code¶
curl -X POST http://localhost:8000/oauth2/token \
-d grant_type=authorization_code \
-d code=$CODE \
-d redirect_uri=https://library.example.org/callback \
-d code_verifier=$VERIFIER \
-d client_id=library
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 600,
"scope": "openid profile email offline_access library:loans:read",
"refresh_token": "3sT...",
"id_token": "eyJ..."
}
The code is single use and lives 60 seconds. Presenting it twice fails — and IDEN locks the row while redeeming it, so two simultaneous attempts cannot both succeed.
4. Validate the ID token¶
Before trusting anything in it:
| Check | Why |
|---|---|
| Signature, against JWKS | Otherwise anyone can write one |
iss matches your IDEN issuer |
Otherwise another provider's token is accepted |
aud equals your client_id |
Otherwise a token for a different application is accepted |
nonce matches what you sent |
Ties it to your request |
exp is in the future |
— |
Then read sub — the stable identifier for this person. Not the email; people change those.
Store sid too, if you plan to support single sign-out.
5. Call APIs with the access token¶
Never send the ID token instead. See Protect an API for the other side.
6. Refresh before it expires¶
curl -X POST http://localhost:8000/oauth2/token \
-d grant_type=refresh_token \
-d refresh_token=$REFRESH \
-d client_id=library
You get a new refresh token. Store it and discard the old one — reusing a spent token is treated as theft after the grace window.
Checking silently whether someone is still signed in¶
For a single-page app, repeat step 2 in a hidden iframe with prompt=none. Either a code comes back,
or an error does — nothing is ever shown to the user.
| Error | Meaning |
|---|---|
login_required |
Not signed in, or the session no longer satisfies what you asked for |
consent_required |
Signed in, but has not agreed to these permissions |
account_selection_required |
Needs to choose an account |
Demanding a fresh sign-in¶
For a sensitive screen, add max_age=300 — or prompt=login to force it outright. See
Assurance.
Mistakes worth avoiding¶
Sending the access token to your own frontend as proof of identity
It says what may be done, not who is doing it. Use the ID token.
Skipping state because PKCE is enabled
They defend against different things. PKCE stops a stolen code being exchanged; state stops a
code being injected into your callback.
Storing tokens in localStorage
Any script on the page can read them. Prefer memory plus a short refresh, or a backend that holds them.
Treating email as the identifier
It changes, and IDEN marks it unverified when it does. sub never changes.