Skip to Content
MuoCeon Docs
v1oauth context

Authorization

POST /v1/oauth/preauthorize and POST /v1/oauth/authorize — the two-step internal endpoints behind the consent page.

These two endpoints are internal interfaces used by the consent UI, not something third-party clients call. Third parties use the standard authorization code flow, described in Authorization code flow.

They turn "the user has seen and approved this application's permissions" into a fact the server can verify: preauthorize validates the identity assertion and the redirect address and issues a consent ticket, then authorize consumes the assertion and ticket and either issues a code or denies.

POST /preauthorize

Request

ParameterTypeRequiredNotes
id_assertionstring✅JWE issued by identity assertion
client_idstring✅Client requesting authorization
redirect_uristringOptionalRedirect address used by the client
scopestringOptionalScope being requested; the client's registered set when omitted

Response

{
  "success": true,
  "can_skip_consent": false,
  "client_name": "Example App",
  "client_type": "third_party",
  "client_logo_uri": null,
  "client_uri": null,
  "client_policy_uri": null,
  "scopes": ["openid", "profile"],
  "valid_redirect_uri": "https://app.example.com/callback",
  "consent_ticket": "ct_..."
}
FieldNotes
can_skip_consentWhether the consent screen may be skipped; it does not mean the ticket may be skipped
client_typefirst_party or third_party
client_*_uriPresentation material, nullable; the page shows or hides each item individually
scopesScopes to display, computed by the server and written into the ticket
valid_redirect_uriThe validated redirect address
consent_ticketOpaque ticket, valid 30 minutes, redeemable once

Validation order

  1. Decrypt the identity assertion, check audience, expiry and session binding;
  2. consume the assertion (one assertion yields at most one ticket);
  3. confirm the client exists and the redirect address is registered;
  4. issue the ticket.

POST /authorize

Request

ParameterTypeRequiredNotes
id_assertionstring✅Assertion signed at the moment of the click
consent_ticketstring✅Ticket returned by preauthorize
decisionallow | denyOptionalDefaults to allow
client_idstringOptionalClient identifier
redirect_uristringOptionalRedirect address
response_typestringOptionalNormally code
scopestringOptionalRequested scope
statestringOptionalEchoed back verbatim
code_challenge / code_challenge_methodstringOptionalPKCE
acr_valuesstringOptionalTarget authentication strength
max_agenumber | stringOptionalMaximum allowed authentication age
authorization_detailsunknownOptionalRFC 9396 fine-grained authorization

Undeclared extension parameters are preserved and passed to the authorization server library rather than silently dropped.

Response

{ "success": true, "redirect_to": "https://app.example.com/callback?code=...&state=..." }

With decision=deny, redirect_to points at the callback with error=access_denied.

JSON instead of a 302

The caller is the auth frontend, and it must stay on its own page while deciding when to navigate: a 302 handed straight to the browser would leave the frontend without the "authorization succeeded" result and unable to clean up before navigating. The practical problem is cross-origin: a frontend calling this endpoint with fetch cannot read Location, so a 302 is simply unusable.

Why the ticket is mandatory

Without itConsequence
No ticket/authorize degrades into "an assertion buys a code", so anyone with a valid session can mint codes and the consent page becomes decoration
Ticket without dataThe account, client, displayed scopes and validated redirect address all live in the server-side record; the caller only returns an opaque string, so the comparison basis never depends on caller input

On this page