Skip to Content
MuoCeon Docs
Third-party OAuth IntegrationAuthorization flows

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:

ParameterRequiredNotes
client_id✅The client identifier obtained at registration
redirect_uriAlways recommendedMust 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
scopeOptionalSpace-separated; when omitted, the client's registered default set applies
stateStrongly recommendedReturned verbatim; use it for anti-CSRF
code_challenge✅PKCE challenge (S256)
code_challenge_method✅Must be S256; plain is rejected
acr_valuesOptionalRequired authentication strength, ordered by preference; see Step-up authentication
max_ageOptionalMaximum accepted authentication age in seconds; exceeding it forces re-authentication
authorization_detailsOptional (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=S256

Understanding these four steps explains most "inexplicable" errors.

  1. Session check: when not signed in, the user is sent to the sign-in page and returns with all original parameters intact.
  2. Pre-validation: the consent UI submits client_id, redirect_uri, scope and friends to /v1/oauth/preauthorize. The server validates the client and redirect URI, then returns display material (app name, permission list) and an opaque consent_ticket valid for 30 minutes. The ticket's contents are invisible to clients.
  3. 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).
  4. Returning the redirect location: /authorize returns 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%2Foauth

Denial:

https://your-app.example.com/callback?error=access_denied&state=k3Jd9x&iss=https%3A%2F%2Fid.example.com%2Fv1%2Foauth

Three mandatory checks:

  1. state matches the value stored when the flow started.
  2. iss matches the trusted issuer (RFC 9207). The server declares authorization_response_iss_parameter_supported: true, so both success and error responses carry iss; a missing iss is an anomaly.
  3. The code is exchanged immediately — codes are single-use with a minute-scale lifetime; do not persist them for async processing.

Common rejection causes

SymptomCause
Consent page errors before any callbackUnknown client_id, redirect_uri mismatch, response_type other than code, missing PKCE or a method other than S256
Callback carries error=access_deniedThe user denied
invalid_grant on exchangeCode already used or expired, or redirect_uri / code_verifier mismatch
Repeatedly sent to the step-up pageacr_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_secret in a browser; browser-side clients should register as web_client (public, no secret).
  • Do not put business data in state — it is a one-time anti-CSRF random value, discarded after the callback.

On this page