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 redirect
│ ├─ 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 checkedOn failure
client_id exists and is enabledError page (not a redirect)
redirect_uri is registered for this clientError page (not a redirect)
response_type is present and validinvalid_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 conflictinginvalid_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:

#CheckError on failure
1User has a sessionlogin_required
2Session has not exceeded idle timeoutlogin_required
3Session has not exceeded max lifetimelogin_required
4max_age is satisfied (if provided)login_required
5User account is enabledaccess_denied
6Session ACR level meets the targetinteraction_required
7If target is level2_mandatory: user has OTP enabledinteraction_required
8OTP configuration has not changed since session was createdinteraction_required
9User is authorized for at least one requested scopeaccess_denied
10Consent 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. No UI is shown.

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

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

Goiabada ignores any existing session entirely and starts a fresh authentication flow. 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).

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.

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:

ConditionResult
Target ACR is higher than session ACRRedirect to level 2 (step-up)
OTP configuration changed since login and target requires level 2Redirect to level 2
Target ACR is level1, or session ACR is already sufficientSkip 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 ACRUser has OTP?Result
level2_optionalYesPrompt for OTP code
level2_optionalNoSkip OTP, proceed to post-authentication
level2_mandatoryYesPrompt for OTP code
level2_mandatoryNoPrompt 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.

ConditionResult
prompt=consent is setAlways show the consent screen, even if all scopes were previously consented
Client requires consent and user has not consented to all effective scopesShow consent screen
offline_access is requestedShow consent screen (to re-confirm refresh token grant)
Client does not require consent, no offline_access, no prompt=consentSkip consent, proceed to code issuance
All effective scopes already consented, no offline_access, no prompt=consentSkip consent, proceed to code issuance

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

Response modeDelivery
query (default)redirect_uri?code=...&state=...
fragmentredirect_uri#code=...&state=...
form_postHTML 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:

ParameterWhen user must re-authenticate
max_age=NOnly if the session started more than N seconds ago
prompt=loginAlways, regardless of session age
Both presentprompt=login takes precedence (session is ignored entirely)

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:

CheckMeasured fromConfigured by
Idle timeoutlast_accessed (resets on activity)Global setting: idle timeout in seconds
Max lifetimestarted (never resets)Global setting: max lifetime in seconds
max_agestarted (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.

SituationTarget ACR used
acr_values provided in requestFirst value from the acr_values parameter
acr_values not providedClient’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.

QuestionDetermined 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