Authorization code flow
From the browser redirect to the authorization code: parameters, what the consent UI does, callback validation.
Third-party applications have exactly one path to tokens: authorization code + PKCE. This page covers the first half — how to send the user to the consent UI and how to receive the code safely.
Sequence
User browser Consent UI (id) Authorization server (server)
│ GET /authorize │ │
├─────────────────────>│ session check (sign in first if needed)
│ │ issue one-time id_assertion │
│ │ POST /oauth/preauthorize │
│ ├─────────────────────────────>│
│ │<── app info + consent_ticket ──┤
│ render app name and permission list │
│ click "Allow" │ issue a fresh id_assertion │
│ │ POST /oauth/authorize │
│ ├─────────────────────────────>│
│ │<── redirect location (JSON) ───┤
│ 302 redirect_uri │ │
│<─────────────────────┤ code + state + iss │Steps 2 to 5 happen entirely inside MuoCeon's own pages and services. Third parties cannot influence or replicate them. They are described here so you understand why some parameters stop working and where errors actually originate.
Authorization request parameters
The browser is redirected to authorization_endpoint (the consent UI) with:
| Parameter | Required | Notes |
|---|---|---|
client_id | ✅ | The client identifier obtained at registration |
redirect_uri | Always recommended | Must exactly match a registered value (loopback ignores the port). When omitted, falls back only if the client has exactly one usable address |
response_type | ✅ | Must be code; any other value is rejected |
scope | Optional | Space-separated; when omitted, the client's registered default set applies |
state | Strongly recommended | Returned verbatim; use it for anti-CSRF |
code_challenge | ✅ | PKCE challenge (S256) |
code_challenge_method | ✅ | Must be S256; plain is rejected |
acr_values | Optional | Required authentication strength, ordered by preference; see Step-up authentication |
max_age | Optional | Maximum accepted authentication age in seconds; exceeding it forces re-authentication |
authorization_details | Optional (first-party) | RFC 9396 JSON string; see Authorization details |
Example:
https://id.example.com/authorize
?client_id=app_9f3c
&redirect_uri=https%3A%2F%2Fyour-app.example.com%2Fcallback
&response_type=code
&scope=openid%20profile%20email%20offline_access
&state=k3Jd9x
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256What happens inside the consent UI
Understanding these four steps explains most "inexplicable" errors.
- Session check: when not signed in, the user is sent to the sign-in page and returns with all original parameters intact.
- Pre-validation: the consent UI submits
client_id,redirect_uri,scopeand friends to/v1/oauth/preauthorize. The server validates the client and redirect URI, then returns display material (app name, permission list) and an opaqueconsent_ticketvalid for 30 minutes. The ticket's contents are invisible to clients. - Decision submission: when the user clicks "Allow" or "Deny", the consent UI issues a fresh identity assertion and submits it together with the ticket to
/v1/oauth/authorize. The server consumes both the ticket and the assertion (assertions are single-use; replays are rejected). - Returning the redirect location:
/authorizereturns JSON containing the target address, and the frontend performs the redirect — it is not a 302. This is an internal endpoint for MuoCeon's own frontend; third parties must not call it.
The ticket is not consumed when authentication strength is insufficient
If the user's current strength is below the requested acr_values (or exceeds max_age), /authorize
returns 401 INSUFFICIENT_USER_AUTHENTICATION and does not consume the ticket. The consent UI then
sends the user to the step-up page and retries with the original parameters afterwards. To third parties
this is fully transparent — the callback simply arrives later.
Callback parameters
Success:
https://your-app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=k3Jd9x&iss=https%3A%2F%2Fid.example.com%2Fv1%2FoauthDenial:
https://your-app.example.com/callback?error=access_denied&state=k3Jd9x&iss=https%3A%2F%2Fid.example.com%2Fv1%2FoauthThree mandatory checks:
statematches the value stored when the flow started.issmatches the trusted issuer (RFC 9207). The server declaresauthorization_response_iss_parameter_supported: true, so both success and error responses carryiss; a missingissis an anomaly.- The
codeis exchanged immediately — codes are single-use with a minute-scale lifetime; do not persist them for async processing.
Common rejection causes
| Symptom | Cause |
|---|---|
| Consent page errors before any callback | Unknown client_id, redirect_uri mismatch, response_type other than code, missing PKCE or a method other than S256 |
Callback carries error=access_denied | The user denied |
invalid_grant on exchange | Code already used or expired, or redirect_uri / code_verifier mismatch |
| Repeatedly sent to the step-up page | acr_values requires more than the user can reach, or max_age is too small |
What not to do
- Do not implement the implicit flow (
response_type=token): the server does not support it. - Do not keep
client_secretin a browser; browser-side clients should register asweb_client(public, no secret). - Do not put business data in
state— it is a one-time anti-CSRF random value, discarded after the callback.