Skip to content

Authorization lifecycle

When a client sends an authorization request to /auth/authorize, Goiabada runs a series of checks before issuing a response. This page describes every check, the order they happen in, and how parameters like prompt, max_age, and acr_values interact.

Every authorization request passes through these stages:

/auth/authorize
├─ 1. Validate request (client, redirect_uri, scopes, PKCE, prompt, ...)
├─ 2. Route based on prompt and session state
│ ├─ prompt=none ──► Silent checks ──► Issue response or error
│ ├─ prompt=login ──► Force login ──► /auth/pwd
│ └─ Normal flow ──► SSO or login
├─ 3. Authenticate (password, optionally OTP)
├─ 4. Post-auth checks (scope filtering, session mgmt, consent)
└─ 5. Issue authorization code ──► Redirect to client

These checks run first, before any session or authentication logic.

What’s checked On failure
client_id exists and is enabled Error page (not a redirect)
redirect_uri is registered for this client Error page (not a redirect)
response_type is present and valid invalid_request or unsupported_response_type
Scopes are valid (OIDC scopes or existing resource:permission pairs) invalid_scope
PKCE is correct (required by default; configurable per client or globally) invalid_request
Response mode is valid (query, fragment, or form_post) invalid_request
prompt values are valid and not conflicting invalid_request

Key details:

  • Scopes: openid, profile, email, address, phone, groups, attributes, and offline_access are always allowed. Custom scopes must be in resource:permission format. Scope validation (does it exist?) happens here; scope authorization (is this user allowed?) happens later in post-authentication checks.
  • PKCE: Required by default (S256 only). Can be made optional globally in Settings or per client. When optional, PKCE parameters are still validated if provided. See PKCE for details.
  • Prompt: none, login, and consent are supported. none cannot be combined with other values. Validation happens before prompt-specific logic, so prompt=none login returns invalid_request, not login_required. See Prompt parameter for details.

After validation, Goiabada chooses a path based on the prompt parameter and session state. The three branches are mutually exclusive.

Branch A: prompt=none (silent authentication)

Section titled “Branch A: prompt=none (silent authentication)”

Goiabada attempts to fulfill the request without any user interaction. It runs these checks in order:

# Check Error on failure
1 User has a session login_required
2 Session has not exceeded idle timeout login_required
3 Session has not exceeded max lifetime login_required
4 max_age is satisfied (if provided) login_required
5 User account is enabled access_denied
6 Session ACR level meets the target interaction_required
7 If target is level2_mandatory: user has OTP enabled interaction_required
8 OTP configuration has not changed since session was created interaction_required
9 User is authorized for at least one requested scope access_denied
10 Consent is available for all effective scopes (if required) consent_required

If all checks pass, the response is issued silently (an authorization code for code flow, or tokens directly for implicit flow) and the user is redirected to the client. The session’s last_accessed timestamp is updated to prevent idle timeout.

If any check fails, the error is redirected to the client and no UI is shown. A client that registered itself is the exception: the redirect is withheld, so the silent request gets no error back and simply never returns. See Prompt parameter for what a client should do about that.

Branch B: prompt=login (force re-authentication)

Section titled “Branch B: prompt=login (force re-authentication)”

Goiabada never uses an existing session to skip authentication here, so the user must enter their credentials again.

After authentication, the flow continues normally through post-authentication checks (including consent, if prompt=login consent was requested).

If the same user signs in, the session that was already in the browser is reused, as long as it is still valid; one that has idled out, reached its maximum lifetime, or fallen outside a requested max_age is simply replaced, with nothing revoked. If a different user signs in, that session is ended and replaced: it is deleted, and the refresh tokens and unredeemed authorization codes that came from it are revoked. See Prompt parameter.

Branch C: Normal flow (no prompt, or prompt=consent only)

Section titled “Branch C: Normal flow (no prompt, or prompt=consent only)”

Goiabada checks for an existing session:

  • Valid session found: the user is not prompted for credentials (SSO). The flow skips to the level 1 completion check, which decides if a level 2 step-up is needed.
  • No valid session, or session expired: the user is directed to the login page.
  • Valid session but user account is disabled: access_denied error redirected to client.

This stage only applies to Branch B and Branch C (not prompt=none, which has no UI).

The user enters their email and password. If credentials are valid, the flow moves to the level 1 completion check.

Level 1 completion: deciding if level 2 is needed

Section titled “Level 1 completion: deciding if level 2 is needed”

Goiabada determines the target ACR level and compares it against the session’s current ACR. Only a session belonging to the user who just authenticated counts here. If the browser still holds a session for a different user, that session contributes no ACR and the target alone decides, exactly as it would for a user with no session at all.

How the target ACR is determined:

  1. If acr_values is in the authorization request, use the first value
  2. Otherwise, use the client’s default ACR level

Decision logic:

Condition Result
Target ACR is higher than session ACR Redirect to level 2 (step-up)
OTP configuration changed since login and target requires level 2 Redirect to level 2
Target ACR is level1, or session ACR is already sufficient Skip to post-authentication checks

See ACR and AMR for details on ACR levels and step-up authentication.

If level 2 is needed, the behavior depends on the target ACR:

Target ACR User has OTP? Result
level2_optional Yes Prompt for OTP code
level2_optional No Skip OTP, proceed to post-authentication
level2_mandatory Yes Prompt for OTP code
level2_mandatory No Prompt user to enroll in OTP, then enter code

After authentication completes (or is skipped via SSO), the following checks run:

  • Existing valid session: the session is “bumped” (last_accessed is updated, and the ACR level is upgraded if the new level is higher). ACR levels never downgrade within a session.
  • No valid session: a new session is created, and old sessions from the same device are cleaned up.

The user’s account is checked again (it may have been disabled between login and this point). If disabled: access_denied error redirected to client.

Goiabada filters the requested scopes based on the user’s permissions:

  • Standard OIDC scopes (openid, profile, email, address, phone, groups, attributes) are always kept.
  • Custom scopes (resource:permission) are only kept if the user has the corresponding permission (directly or through a group).
  • If no scopes remain after filtering: access_denied error redirected to client.

The filtered set (“effective scopes”) is what appears in the resulting tokens. Consent is also evaluated against effective scopes, not the originally requested scopes.

Consent is checked if the client has consent enabled, offline_access is in the effective scopes, or prompt=consent was requested.

Condition Result
prompt=consent is set Always show the consent screen, even if all scopes were previously consented
Client requires consent and user has not consented to all effective scopes Show consent screen
offline_access is requested Show consent screen (to re-confirm refresh token grant)
Client does not require consent, no offline_access, no prompt=consent Skip consent, proceed to code issuance
All effective scopes already consented, no offline_access, no prompt=consent Skip consent, proceed to code issuance

Choosing Cancel on the consent screen ends the authorization. What the user sees next depends on who registered the client:

Client What happens
Created by an administrator The browser is redirected to the client’s redirect URI with error=access_denied, and the client shows its own message
Self-registered The browser stays on Goiabada. A page names the application and the address it asked to send the user to, and the request stops there

The redirect is withheld because anyone can register a client and choose where it points: a user who has just refused an application they did not recognise should not then be delivered to it. The same page is shown whenever Goiabada refuses a self-registered client’s authorization for any reason, prompt=none included, and the client receives nothing. See dynamic client registration for the full rule and what it means if you build one.

The authorization code is generated and delivered to the client using the response mode:

Response mode Delivery
query (default) redirect_uri?code=...&state=...
fragment redirect_uri#code=...&state=...
form_post HTML auto-submitting form with code and state fields

The code is then exchanged for tokens at the token endpoint.

The auth_time claim in the resulting ID token reflects when the authorization code was created, except for prompt=none, which preserves the original authentication time from the session for consistency.

Both can force re-authentication, but they work differently:

Parameter When user must re-authenticate
max_age=N Only if the session started more than N seconds ago
prompt=login Always, regardless of session age
Both present prompt=login takes precedence (the session is never used to skip authentication)

max_age vs session idle timeout vs max lifetime

Section titled “max_age vs session idle timeout vs max lifetime”

All three are time-based session checks, but they measure different things:

Check Measured from Configured by
Idle timeout last_accessed (resets on activity) Global setting: idle timeout in seconds
Max lifetime started (never resets) Global setting: max lifetime in seconds
max_age started (never resets) Per-request parameter

A session is valid only if all three pass. The max_age parameter acts like an additional, per-request max lifetime constraint. See User sessions for details.

Situation Target ACR used
acr_values provided in request First value from the acr_values parameter
acr_values not provided Client’s default ACR level

The acr_values parameter overrides the client’s default, allowing applications to request a higher (or lower) authentication level for specific operations.

Question Determined by
Does the user need to log in? Session validity (idle timeout, max lifetime, max_age), prompt=login
Does the user need OTP? Target ACR level (acr_values or client default), user’s OTP enrollment status, OTP configuration changes
Does the user see a consent screen? Client’s consent setting, offline_access scope, prompt=consent, existing consent coverage
Can the request succeed silently? prompt=none (all of the above must pass without interaction)
What scopes end up in the token? Requested scopes filtered by user’s permissions, then by user consent if applicable