fix: resolve OIDC consent flow 500 errors on NC 32
Root cause: ConsentController::grant() only passed client_id and scope in the post-consent redirect, relying on PHP session fallback for state, response_type, redirect_uri etc. On NC 32 (PHP 8.4), session values were intermittently lost between session->close() and the subsequent GET request, causing 500 errors from trim(null) / matchRedirectUri(null). OIDC app fixes: - Pass all OAuth params in consent redirect URL (eliminates session race) - Add null safety guard in authorize endpoint (400 instead of 500) Test infra fixes: - Wait for OIDC redirect chain to settle before handling consent screen (fixes "Execution context was destroyed" Playwright errors) - Capture nextcloud.log in CI failure artifacts for PHP error debugging Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -206,7 +206,9 @@ jobs:
|
||||
|
||||
- name: Collect service logs on failure
|
||||
if: failure()
|
||||
run: docker compose --profile ${{ matrix.profile }} logs --tail=500 > /tmp/docker-compose-logs.txt 2>&1
|
||||
run: |
|
||||
docker compose --profile ${{ matrix.profile }} logs --tail=500 > /tmp/docker-compose-logs.txt 2>&1
|
||||
docker compose exec -T app cat /var/www/html/data/nextcloud.log 2>/dev/null | tail -100 > /tmp/nextcloud-app.log 2>&1 || true
|
||||
|
||||
- name: Upload debug artifacts
|
||||
if: failure()
|
||||
@@ -216,5 +218,6 @@ jobs:
|
||||
path: |
|
||||
/tmp/*.png
|
||||
/tmp/docker-compose-logs.txt
|
||||
/tmp/nextcloud-app.log
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
+76
-27
@@ -382,27 +382,34 @@ async def nc_mcp_oauth_client_with_elicitation(
|
||||
await page.wait_for_load_state("networkidle", timeout=60000)
|
||||
logger.info(" ✓ Login completed")
|
||||
|
||||
# Wait for the OIDC redirect chain to settle before handling consent.
|
||||
logger.info(" Waiting for OIDC redirect chain to settle...")
|
||||
settle_start = time.time()
|
||||
while time.time() - settle_start < 15:
|
||||
current_url = page.url
|
||||
if "/consent" in current_url or "/callback" in current_url:
|
||||
break
|
||||
await anyio.sleep(0.5)
|
||||
|
||||
# Handle consent screen if present
|
||||
try:
|
||||
logger.info(f" Current URL before consent: {page.url}")
|
||||
consent_handled = await _handle_oauth_consent_screen(page, username)
|
||||
if consent_handled:
|
||||
logger.info(" ✓ Consent granted")
|
||||
else:
|
||||
logger.warning(" ⚠ No consent screen detected")
|
||||
# Take screenshot for debugging
|
||||
screenshot_path = f"/tmp/elicitation_no_consent_{uuid.uuid4()}.png"
|
||||
if "/consent" in page.url:
|
||||
await page.wait_for_load_state("networkidle", timeout=10000)
|
||||
try:
|
||||
logger.info(f" Current URL before consent: {page.url}")
|
||||
consent_handled = await _handle_oauth_consent_screen(page, username)
|
||||
if consent_handled:
|
||||
logger.info(" ✓ Consent granted")
|
||||
else:
|
||||
logger.warning(" ⚠ Consent handler returned False")
|
||||
except Exception as e:
|
||||
logger.warning(f" ⚠ Consent screen handling failed: {e}")
|
||||
screenshot_path = (
|
||||
f"/tmp/elicitation_consent_error_{uuid.uuid4()}.png"
|
||||
)
|
||||
await page.screenshot(path=screenshot_path)
|
||||
logger.info(f" Screenshot saved: {screenshot_path}")
|
||||
# Log page title for debugging
|
||||
page_title = await page.title()
|
||||
logger.info(f" Page title: {page_title}")
|
||||
except Exception as e:
|
||||
logger.warning(f" ⚠ Consent screen handling failed: {e}")
|
||||
# Take screenshot for debugging
|
||||
screenshot_path = f"/tmp/elicitation_consent_error_{uuid.uuid4()}.png"
|
||||
await page.screenshot(path=screenshot_path)
|
||||
logger.info(f" Screenshot saved: {screenshot_path}")
|
||||
else:
|
||||
logger.debug(f" No consent screen (URL: {page.url})")
|
||||
|
||||
# Wait for OAuth callback URL to be reached
|
||||
# The MCP server's callback endpoint will handle token exchange
|
||||
@@ -1843,11 +1850,24 @@ async def playwright_oauth_token(
|
||||
current_url = page.url
|
||||
logger.info(f"After login, current URL: {current_url}")
|
||||
|
||||
# Wait for the OIDC redirect chain to settle before handling consent.
|
||||
# After login, the flow goes: /apps/oidc/redirect (JS page) → JS navigates
|
||||
# to /authorize → 303 to /consent. networkidle fires after the JS page
|
||||
# loads but before the JS navigation starts.
|
||||
logger.info("Waiting for OIDC redirect chain to settle...")
|
||||
settle_start = time.time()
|
||||
while time.time() - settle_start < 15:
|
||||
current_url = page.url
|
||||
if "/consent" in current_url or "localhost:8081" in current_url:
|
||||
break
|
||||
await anyio.sleep(0.5)
|
||||
|
||||
# Handle consent screen if present
|
||||
try:
|
||||
if "/consent" in page.url:
|
||||
await page.wait_for_load_state("networkidle", timeout=10000)
|
||||
await _handle_oauth_consent_screen(page, username)
|
||||
except Exception as e:
|
||||
logger.debug(f"No consent screen or already authorized: {e}")
|
||||
else:
|
||||
logger.debug(f"No consent screen (URL: {page.url})")
|
||||
|
||||
# Wait for callback server to receive the auth code
|
||||
# Browser will be redirected to localhost:8081 which will capture the code
|
||||
@@ -2138,11 +2158,21 @@ async def _get_oauth_token_with_scopes(
|
||||
current_url = page.url
|
||||
logger.info(f"After login, current URL: {current_url}")
|
||||
|
||||
# Wait for the OIDC redirect chain to settle before handling consent.
|
||||
logger.info(f"Waiting for OIDC redirect chain to settle for {username}...")
|
||||
settle_start = time.time()
|
||||
while time.time() - settle_start < 15:
|
||||
current_url = page.url
|
||||
if "/consent" in current_url or "localhost:8081" in current_url:
|
||||
break
|
||||
await anyio.sleep(0.5)
|
||||
|
||||
# Handle consent screen if present
|
||||
try:
|
||||
if "/consent" in page.url:
|
||||
await page.wait_for_load_state("networkidle", timeout=10000)
|
||||
await _handle_oauth_consent_screen(page, username)
|
||||
except Exception as e:
|
||||
logger.debug(f"No consent screen or already authorized: {e}")
|
||||
else:
|
||||
logger.debug(f"No consent screen for {username} (URL: {page.url})")
|
||||
|
||||
# Wait for callback server to receive the auth code
|
||||
logger.info(f"Waiting for auth code with state: {state[:16]}...")
|
||||
@@ -2510,11 +2540,30 @@ async def _get_oauth_token_for_user(
|
||||
await page.wait_for_load_state("networkidle", timeout=30000)
|
||||
current_url = page.url
|
||||
|
||||
# Wait for the OIDC redirect chain to settle before handling consent.
|
||||
# After login, the flow goes: /apps/oidc/redirect (JS page) → JS navigates
|
||||
# to /authorize → 303 to /consent. networkidle fires after the JS page
|
||||
# loads but before the JS navigation starts, so we must wait for the URL
|
||||
# to reach either the consent page or the callback.
|
||||
logger.info(f"Waiting for OIDC redirect chain to settle for {username}...")
|
||||
settle_start = time.time()
|
||||
while time.time() - settle_start < 15:
|
||||
current_url = page.url
|
||||
if "/consent" in current_url or "localhost:8081" in current_url:
|
||||
break
|
||||
await anyio.sleep(0.5)
|
||||
else:
|
||||
logger.warning(
|
||||
f"OIDC redirect chain did not settle for {username}, "
|
||||
f"current URL: {page.url}"
|
||||
)
|
||||
|
||||
# Handle consent screen if present
|
||||
try:
|
||||
if "/consent" in page.url:
|
||||
await page.wait_for_load_state("networkidle", timeout=10000)
|
||||
await _handle_oauth_consent_screen(page, username)
|
||||
except Exception as e:
|
||||
logger.debug(f"No consent screen or already authorized for {username}: {e}")
|
||||
else:
|
||||
logger.debug(f"No consent screen for {username} (URL: {page.url})")
|
||||
|
||||
# Wait for callback server to receive the auth code
|
||||
# Browser will be redirected to localhost:8081 which will capture the code
|
||||
|
||||
Vendored
+1
-1
Submodule third_party/oidc updated: d371a4a797...669785db58
Reference in New Issue
Block a user