Skip to content

fix: pass all OAuth params in consent redirect to prevent session race#630

Open
cbcoutinho wants to merge 2 commits intoH2CK:masterfrom
cbcoutinho:fix/consent-redirect-session-race
Open

fix: pass all OAuth params in consent redirect to prevent session race#630
cbcoutinho wants to merge 2 commits intoH2CK:masterfrom
cbcoutinho:fix/consent-redirect-session-race

Conversation

@cbcoutinho
Copy link
Contributor

@cbcoutinho cbcoutinho commented Mar 21, 2026

Summary

  • Pass all OAuth parameters in consent redirect URL instead of only client_id and scope, eliminating the session dependency that causes intermittent 500 errors
  • Add null safety guard in LoginRedirectorController::authorize() to return a 400 error when critical OAuth params are missing after session fallback

Problem

After a user grants consent, ConsentController::grant() redirects back to the authorize endpoint with only client_id and scope in the URL. The authorize endpoint then relies on PHP session fallback to recover state, response_type, redirect_uri, and other critical OAuth parameters.

This works most of the time, but fails intermittently because:

  1. $this->session->close() is called before the redirect (line 212)
  2. The browser follows the 303 redirect immediately
  3. The GET request to /authorize opens the session again
  4. If session values are lost (timing-dependent), downstream code crashes:
    • matchRedirectUri(null, ...) → TypeError
    • trim(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 passed client_id and scope in the redirect URL.

Two previous fixes attempted to address symptoms of this bug:

  • v1.16.1 (49fd2f1) — Widened the session fallback condition in authorize() from if (empty($client_id)) to if (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 where state/response_type/redirect_uri are legitimately present as URL params.
  • v1.16.2 (5c5d1e1) — Reverted v1.16.1's condition change back to 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

$authorizeUrl = $this->urlGenerator->linkToRoute('oidc.LoginRedirector.authorize', array_filter([
    'client_id' => $this->session->get('oidc_client_id'),
    'scope' => $grantedScopes,
    'state' => $this->session->get('oidc_state'),
    'response_type' => $this->session->get('oidc_response_type'),
    'redirect_uri' => $this->session->get('oidc_redirect_uri'),
    'nonce' => $this->session->get('oidc_nonce'),
    'resource' => $this->session->get('oidc_resource'),
    'code_challenge' => $this->session->get('oidc_code_challenge'),
    'code_challenge_method' => $this->session->get('oidc_code_challenge_method'),
]));

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 by if (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:

if (empty($state) || empty($response_type) || empty($redirect_uri)) {
    $this->logger->error('Missing critical OAuth params after session fallback: ...');
    return new TemplateResponse('core', '400', [
        'message' => $this->l->t('Authorization session expired. Please try again.'),
    ], 'error');
}

Test plan

  • Verified consent flow works with PKCE (code_challenge params passed correctly)
  • Verified consent flow works without PKCE (optional params omitted via array_filter)
  • Verified session fallback still works for the initial login redirect flow
  • Ran full OAuth test suite (68 tests pass)
  • Tested on Nextcloud 31 and 32

This PR was generated with the help of AI, and reviewed by a Human

cbcoutinho and others added 2 commits March 21, 2026 18:07
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant