fix: pass all OAuth params in consent redirect to prevent session race#630
fix: pass all OAuth params in consent redirect to prevent session race#630cbcoutinho wants to merge 2 commits into
Conversation
…sent The consent flow redirects back to /apps/oidc/authorize with only client_id and scope in the URL. Version 1.16.2 changed the session fallback to only trigger when client_id is empty, which means state, response_type, and redirect_uri are lost after consent, causing a 500. Restore the 1.16.1 behavior of falling back to session when ANY critical parameter is missing, but with per-parameter guards so URL-provided values are preserved. This fixes both: - Consent redirect (has client_id but missing state/response_type/redirect_uri) - Implicit flow (missing client_id, as in Guacamole case from H2CK#626) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The consent grant redirect only passed client_id and scope, relying on session fallback in the authorize endpoint. This caused intermittent 500 errors on NC 32 (PHP 8.4) when session values were lost between session->close() and the subsequent GET request — a race condition. Now ConsentController::grant() includes all OAuth params (state, response_type, redirect_uri, nonce, resource, code_challenge, code_challenge_method) in the redirect URL, eliminating the session dependency entirely. Also adds a guard in LoginRedirectorController::authorize() that returns a 400 error if critical params are still missing after session fallback, preventing unhandled 500s from trim(null) or matchRedirectUri(null). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
H2CK
left a comment
There was a problem hiding this comment.
Hi. Parts of the changes will open again the recently closed bugs. Please have a look at the comments in LoginRedirectController. The changes in ConsentController are in general fine.
| // Guard: if critical OAuth params are still missing after session fallback, | ||
| // return a meaningful error instead of letting downstream code crash with a 500 | ||
| // (e.g. matchRedirectUri(null, ...) or trim(null) in PHP 8.4). | ||
| if (empty($state) || empty($response_type) || empty($redirect_uri)) { |
There was a problem hiding this comment.
This will break implicit flow implementation of guacamole. See #626.
The state might be missing without any impact.
| $response_type = $this->session->get('oidc_response_type'); | ||
| $redirect_uri = $this->session->get('oidc_redirect_uri'); | ||
| $scope = $this->session->get('oidc_scope'); | ||
| if (empty($client_id) || empty($state) || empty($response_type) || empty($redirect_uri)) { |
There was a problem hiding this comment.
This again allows a partial fallback (per missing attribute) to the session which leads to mixed oidc parameters and can cause problems as described in #621. Session fallback should only happen if client id is missing and then all parameters should be read from session. Otherwise parameters will be mixed if multiple client perform authentication.
…for missing OAuth params
* fix: Preserve OIDC authorization parameters across SAML session regeneration When a user authenticates via a SAML backend, Nextcloud regenerates the PHP session (as protection against session fixation attacks). This destroys the OIDC authorization parameters (client_id, state, redirect_uri, etc.) that were stored in the session before the login redirect. After SAML login, PageController::index() reads null values and Redirect.vue redirects to /apps/oidc/authorize without parameters, causing: ClientMapper::getByIdentifier(): Argument #1 must be of type string, null given. Fix: Pass all OIDC parameters as URL query parameters in the redirect_url so they survive session regeneration. The session fallback is kept for backwards compatibility with non-SAML login flows. Changes: - LoginRedirectorController: Include params in linkToRoute() call - PageController: Read from request first, fall back to session - templates/main.php: Pass params as data-* attributes on the DOM - Redirect.vue: Read data-* attributes and append to authorize URL Fixes #628 * fix: Address review feedback - Wrap URL parameters in array_filter() to omit null values - Use all-or-nothing approach for URL vs session parameters in PageController to avoid mixing data from different sources Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Integrate consent flow fixes from #630 and add guard clause for missing OAuth params * fix: Revert LoginRedirectorController session fallback to original logic Revert the granular per-parameter fallback and guard clause as requested in review — these changes reintroduced previously fixed bugs. The original session fallback (triggered only when client_id is empty) is restored. All scenarios tested successfully: - SAML backend login (first attempt, no retry needed) - Direct Nextcloud login - PKCE flow (code_challenge preserved across SAML redirect) - Consent flow (ConsentController now passes all params via URL) --------- Co-authored-by: Thomas Schlüter <thomas.schlueter@mdz-wi.de> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
The relevant changes of this PR have now been introduced with #631. Please have a look at the master branch. The described bug should be solved and the introduced guard was adapted to still allows the use of the Guacamole oidc client. If anything is missing please update the PR accordingly. |
|
Changes to fix the bug are already contained in > 1.16.3 |
Summary
client_idandscope, eliminating the session dependency that causes intermittent 500 errorsLoginRedirectorController::authorize()to return a 400 error when critical OAuth params are missing after session fallbackProblem
After a user grants consent,
ConsentController::grant()redirects back to the authorize endpoint with onlyclient_idandscopein the URL. The authorize endpoint then relies on PHP session fallback to recoverstate,response_type,redirect_uri, and other critical OAuth parameters.This works most of the time, but fails intermittently because:
$this->session->close()is called before the redirect (line 212)/authorizeopens the session againmatchRedirectUri(null, ...)→ TypeErrortrim(null)→ E_DEPRECATED in PHP 8.4 (potentially promoted to error)This is especially prevalent on Nextcloud 32 with PHP 8.4, where
trim(null)deprecation handling is stricter.History
The consent feature was introduced in v1.11.0 (1262cfe, PR #586). The bug has been present since then —
ConsentController::grant()has always only passedclient_idandscopein the redirect URL.Two previous fixes attempted to address symptoms of this bug:
authorize()fromif (empty($client_id))toif (empty($client_id) || empty($state) || empty($response_type) || empty($redirect_uri)), so the fallback would trigger when consent only passed 2 params. However, this broke the non-consent flow wherestate/response_type/redirect_uriare legitimately present as URL params.if (empty($client_id)), which restored the non-consent flow but left the consent race condition unfixed.Neither fix addressed the root cause:
ConsentController::grant()not passing the full set of OAuth parameters. This PR fixes that directly.Affected versions
v1.11.0 through v1.16.2 (current).
Solution
ConsentController::grant() — pass all params in URL
array_filter()omits null values so optional params (resource, code_challenge) don't appear as empty strings in the URL.Since all critical params are now in the URL, the session fallback in
authorize()(guarded byif (empty($client_id))) is no longer needed for the consent flow — but remains functional for the initial login redirect flow where it's still required.LoginRedirectorController::authorize() — null safety guard (defense in depth)
After the session fallback block, a guard checks if critical params are still missing and returns a meaningful 400 error instead of letting downstream code crash with a 500:
Test plan
This PR was generated with the help of AI, and reviewed by a Human