Authorization callback outcomes
M7 completes a browser authorization request by navigating the top-level
browser to the client's exact registered redirect_uri. The callback uses an
ordinary GET, and the authorization response is carried in the URI query.
M7 does not use a form-body callback or return tokens to the callback.
This guide defines every supported callback outcome, which conditions remain on M7, and how a client or SDK must handle each result.
Process every callback defensively
For each authorization attempt, retain a high-entropy, single-use state with
the browser transaction. On return:
- Accept the callback only at the exact registered endpoint and process the query response.
- Require a non-empty returned
stateand compare it with the expected value before acting on either success or error. Use a constant-time comparison when your platform provides one. - Require exactly one terminal shape: a non-empty
codeor a non-emptyerror, never both and never neither. - Reject duplicate, conflicting, array-shaped, or otherwise malformed protected fields.
- On success, exchange the code only from the application's trusted token pickup component using the original redirect URI and PKCE verifier.
- On error, do not call
/token. Classify the result usingerroranderror_reason, then expire the authorization transaction. - Treat an unknown
errororerror_reasonas a safe terminal failure. Do not convert an unfamiliar value into success.
A state mismatch takes precedence over every application-level outcome. For
example, a callback that says signup_cancelled but carries the wrong state is
an invalid callback, not a trusted cancellation.
Callback fields
| Field | When present | Stability and client use |
|---|---|---|
code |
Successful authorization only | Opaque, confidential, short-lived, and single-use. Send it only to /token; do not inspect it. |
state |
Every successful callback and every error for which the original state was available | Compare with the expected single-use state before any outcome handling. Do not use it as application data. |
error |
Authorization error only | Stable OAuth error class. Clients may branch on documented values and must safely retain unknown values. |
error_reason |
Normally present on an authorization error | Stable M7 machine-readable reason. Clients may branch on documented values and must safely retain unknown values. |
error_description |
Optional on an error | Safe human-readable explanation. Clients must not parse it or depend on exact wording. |
trace_id |
Optional on a logged error | Opaque support correlation value. It carries no authority or diagnostic meaning; retain it only for support correlation. |
During migration, some cancellation responses retain legacy discriminator text
such as login_cancelled or signup_cancelled in error_description.
Integrations must use error_reason; description text is not a protocol field.
Success
Existing-session, account-chooser, password-login, and federated-login success all use the same callback shape:
https://app.example.com/oauth/callback?code=AUTHORIZATION_CODE&state=STATE
Signup or account activation does not issue a separate kind of authorization
success. After the preserved request resumes and sign-in completes, the client
receives the same code and state callback.
The authorization code is reserved for approximately ten minutes. It is not an access token and must never be used as an API bearer credential. Validate state first, then perform the server-side authorization-code exchange.
Callback errors and required actions
The table lists stable client-visible fields. error_description remains safe
human-facing text and may vary unless an exact value is stated. “Start again”
means create a fresh state, PKCE verifier, challenge, and authorization request;
do not replay the old callback.
| Situation | error |
error_reason |
Retryability and client or SDK action |
|---|---|---|---|
| User leaves password login or the remembered-account chooser | access_denied |
login_cancelled |
Terminal for this attempt. Return to the application; start a new interactive request only if the user chooses. |
| User cancels hosted signup | access_denied |
signup_cancelled |
Terminal for this attempt. Offer sign-in or a fresh authorization request. |
| Public signup is disabled for the application | access_denied |
public_signup_disabled |
Do not repeat signup automatically. Offer an allowed sign-in path or contact the application owner. |
| User cancels the pending-account ceremony | access_denied |
account_pending_cancelled |
Terminal for this attempt. Preserve no authorization code or pickup state. |
| Registration requires administrator approval | access_denied |
registration_pending_approval |
Do not loop signup. Tell the user approval is pending and begin a new request after approval. |
| User cancels account recovery | access_denied |
account_recovery_cancelled |
Terminal for this attempt. Return safely or start a new flow at the user's request. |
| User cancels password recovery | access_denied |
forgot_password_cancelled |
Terminal for this attempt. Return safely or start a new flow at the user's request. |
| User cancels username recovery | access_denied |
forgot_username_cancelled |
Terminal for this attempt. Return safely or start a new flow at the user's request. |
| GitHub, Discord, or X reports user cancellation or denial | access_denied |
federated_login_cancelled |
Terminal for this attempt. Offer another provider or a fresh sign-in request. |
| An unexpected provider, persistence, policy, configuration, or processing failure occurs after callback trust | server_error |
unspecified_error_try_again |
Retryable only through a fresh authorization request, normally after a delay. Preserve trace_id for support. Do not infer the internal cause. |
prompt=none cannot use an eligible current session |
login_required |
session_required |
Start a new interactive authorization without prompt=none when interaction is allowed. |
| Authorization completion code fails validation | invalid_request |
authorization_code_validation_failed |
Start the authorization request again. Do not call /token. |
| Application admission policy denies the account | access_denied |
application_access_denied |
Do not retry automatically. Access or application policy must change first. |
| Signup is not configured correctly for the application | invalid_request |
signup_configuration_error |
Application owner must correct signup configuration before retrying. |
| Application policy disables federated sign-in | access_denied |
federated_login_disabled |
Use an allowed sign-in method; do not retry the disabled provider path. |
| Trusted federated handoff is expired or invalid | invalid_request |
federated_login_session_invalid |
Start authorization again. Never replay the old provider handoff. |
| Client requests anything other than authorization code response type | unsupported_response_type |
response_type_not_supported |
Correct the request to response_type=code. |
| Required state is missing | invalid_request |
state_required |
Correct the client and start again. No state can be echoed in this callback. |
| Requested prompt value is unsupported | invalid_request |
prompt_not_supported |
Correct or omit prompt, then start again. |
max_age is malformed |
invalid_request |
invalid_max_age |
Send a non-negative integer or omit the field, then start again. |
login_hint is malformed |
invalid_request |
invalid_login_hint |
Correct or omit the hint, then start again. |
An error callback never contains an authorization code. A client must not begin token exchange for any error, malformed callback, missing state, or state mismatch.
Opaque server failures and trace IDs
M7 deliberately hides internal system and provider failure labels. A safely returnable unexpected failure has this public shape:
https://app.example.com/oauth/callback?
error=server_error&
error_reason=unspecified_error_try_again&
error_description=An%20unspecified%20error%20occurred.%20Please%20try%20again.&
trace_id=OPAQUE_TRACE_ID&
state=STATE
The example is split across lines for readability; the browser receives one URI. The trace ID lets M7 operators correlate the public failure with a private log event. It must not reveal or be treated as an internal error code. Internal failure reasons, exception details, provider responses, configuration data, and credentials are never part of the public callback contract.
Outcomes that remain on M7
M7 redirects only after it trusts the client and exact registered callback. Some failures therefore remain within the hosted flow:
| Condition | M7 behavior | Client implication |
|---|---|---|
Missing, unknown, or invalid client_id |
Local OAuth-style failure | No client registration is trusted, so M7 does not redirect. |
Missing, malformed, non-HTTPS, or unregistered redirect_uri |
Local OAuth-style failure | Never construct a return target from the rejected value. |
| Browser DPoP proof is invalid | Local safe retry response with a trace ID | M7's hosted browser code may restart the proof ceremony; this is not a client callback. |
| Authorization challenge is expired, reused, or invalid | Local safe retry response with a trace ID | M7's hosted browser code may restart the challenge; this is not a client callback. |
| Hosted login, signup, consent, credentials, or required form input is invalid | Local safe form or JSON error | The user can correct the input without terminating authorization. |
| Authorization request hydration fails before trusted callback state is recovered | Local safe start-again result | Begin a new authorization request; M7 does not redirect using untrusted input. |
| Upstream provider state is missing, invalid, or expired | M7 provider terminal or retry page | Provider input alone cannot establish a relying-party callback. |
| Provider proof, token exchange, or profile lookup fails while trusted provider state remains | M7 retry page with a safe application-return option | Raw provider diagnostics remain private. A chosen return uses the generic server-error callback. |
/token rejects a code, client, PKCE verifier, or grant |
Server-to-server JSON error | This is a token-endpoint outcome, not an authorization callback. |
M7-local JSON fields are implementation details for M7's hosted pages unless a separate public endpoint guide explicitly defines them. Client applications must not call hosted form-processing routes as APIs.
PAR expiry and re-entry
An expired pushed authorization request no longer provides trustworthy callback
state. M7 therefore does not send PAR expiry to the expired request's
redirect_uri.
The hosted flow uses a local result with:
error=invalid_request;error_reason=request_uri_expired;outcome=authorization_reentry_required;- an opaque
trace_id; next_method=GET; and- a trusted
next_uri.
If the OAuth client has a registered initiate_login_uri, M7 uses that URI to
start a new authorization request. Otherwise it uses M7's /par-expired page.
The re-entry URI is not an authorization callback, and it receives no code,
token, PKCE verifier, or expired state.
Upstream provider callbacks are separate
GitHub, Discord, and X return to M7-specific provider callbacks. M7 validates and consumes provider state before deciding whether it has a trusted relying application callback.
- An explicit provider cancellation becomes
access_deniedwitherror_reason=federated_login_cancelledwhen trusted state is available. - An unexpected provider failure becomes the opaque public server-error shape only when trusted state is available.
- Without trusted provider state, M7 remains on a local retry or terminal page.
Provider access tokens, response bodies, profiles, proof material, and internal failure labels are never forwarded to the relying application callback.
Callback URI handling
M7 preserves a registered callback's existing non-reserved query parameters, appends callback fields once using RFC 3986 encoding, and inserts them before an existing fragment:
https://app.example.com/oauth/callback?source=m7&code=AUTHORIZATION_CODE&state=STATE#complete
State and code are opaque. After query decoding, the returned state must equal the original value exactly, including Unicode and reserved characters.
A registered URI must not already contain any reserved callback field:
code, state, error, error_reason, error_description, or trace_id.
M7 rejects such a conflict rather than overwriting or duplicating it.
Keep callback and token errors separate
An authorization callback reports the outcome of browser authorization. The token endpoint reports the outcome of a later server-to-server grant request. Both layers can use OAuth labels, but they are different transactions.
- A callback error means no token exchange should begin.
- A successful callback supplies an authorization code, not a token package.
/tokenerrors such asinvalid_grantorinvalid_clientmust be handled by the token-pickup component and must not be reinterpreted as callback errors.- Access tokens, ID tokens, refresh tokens, binding values, and activation secrets are never fields of the browser callback.
Logging and data handling
Never log or place in analytics, telemetry, support tickets, screenshots, or persistent browser storage:
- the raw callback URI;
- authorization codes or state values;
- access, ID, or refresh tokens;
- PKCE verifiers;
- DPoP proofs, challenges, or keys;
- client secrets or assertions;
- provider credentials, responses, or profiles; or
- internal exception or system-error details.
Log only a safe event classification and the opaque trace ID needed for correlation. When displaying an error to a user, escape all callback values and show the trace ID as a support reference, not as diagnostic detail.