fix(login-flow): resolve Login Flow v2 login_url to Nextcloud in external-IdP mode#993
Conversation
…n Flow v2 Adds the missing Login Flow v2 app-password integration tests for the mcp-keycloak service (port 8002). The keycloak lane previously only covered DCR/authorize; it never provisioned an app password or exercised a DAV path. The new fixtures obtain a Keycloak OAuth token (browser auth-code + PKCE) and complete Nextcloud Login Flow v2, logging in as a *local* Nextcloud user via its EMAIL. Nextcloud keys the app password on the loginName (the email), which differs from the canonical UID, so context.py builds DAV paths from the email (/remote.php/dav/files/<email>/) instead of the real home dir. Without PR #980 the WebDAV cycle targets the wrong home and fails (RED); with the current-user-principal discovery fix it resolves the UID and passes (GREEN). This is the keycloak-service counterpart of the existing login_flow WebDAV test. Card: Deck #489 (board 12). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @cbcoutinho's task in 3m 40s —— View job Code Review
Note: couldn't execute SummaryThis is a well-scoped, well-documented fix. The core bug ( Things verified while reading (no issues found)
Minor observations (non-blocking)
Test coverageThe new No blocking issues found. This looks ready to merge once CI is green. |
…rnal-IdP mode The Login Flow v2 login_url origin was rewritten to `NEXTCLOUD_PUBLIC_ISSUER_URL`, which doubles as the OAuth issuer for JWT `iss` validation. In single-IdP (login-flow) mode the issuer IS Nextcloud, so this worked. In external-IdP mode (e.g. Keycloak) the issuer is the IdP, so the browser-facing login URL was rewritten onto the IdP origin (e.g. `http://localhost:8888/login/v2/flow/...`) — which has no `/login/v2` endpoint and 404s, breaking Login Flow v2 for every external-IdP deployment. Add a dedicated `NEXTCLOUD_PUBLIC_URL` (browser-reachable Nextcloud base URL) resolved via a new `Settings.nextcloud_browser_url` property with a backward-compatible fallback chain (`nextcloud_public_url` → `nextcloud_public_issuer_url` → `nextcloud_host`). Login Flow v2 / elicitation rewrites now use it; OAuth issuer / JWT validation stays on `nextcloud_public_issuer_url`. Wire `NEXTCLOUD_PUBLIC_URL=http://localhost:8080` for the `mcp-keycloak` compose service. Existing login-flow deployments that set only `NEXTCLOUD_PUBLIC_ISSUER_URL` are unchanged (the fallback resolves to the same value). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The keycloak lane's Login Flow v2 tests never actually ran the reproduction they claimed: the OAuth-leg fixture requested `talk.*` scopes not registered on the Keycloak client (invalid_scope → Playwright #username timeout), and the Login Flow leg 404'd on the Keycloak-origin login_url (fixed separately via NEXTCLOUD_PUBLIC_URL). Rework: - OAuth leg now uses a Keycloak direct-grant (ROPC) with realm-supported scopes instead of the flaky browser auth-code flow — its identity is irrelevant to the reproduction (the Login Flow v2 app password authenticates DAV). - Session-scope the divergent user + provisioned client so a single live user and one browser login serve the lane (the app-password store is keyed by the shared Keycloak `admin` identity, so per-test users would be deleted while their app password is still cached). - Reframe as end-to-end Keycloak Login Flow v2 WebDAV coverage. These do NOT reproduce #980's wrong-path bug on NC32/33: Nextcloud resolves `/remote.php/dav/files/<email>/` to the real home, so the round-trip succeeds with or without the client-side principal-discovery fix. #980's failure mode needs a backend (e.g. LDAP) where the loginName is not a valid files-path alias; it stays covered by #980's own mocked unit tests. The tests remain valuable: they fill the missing keycloak Login Flow coverage and guard the NEXTCLOUD_PUBLIC_URL fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round-1 review finding: userinfo_routes.py built browser-facing Nextcloud app links (viz tab) from `nextcloud_public_issuer_url or nextcloud_host`, the same pattern migrated elsewhere in this PR. In external-IdP mode the issuer URL is the IdP, so those links would point at the IdP origin instead of Nextcloud. Switch to `settings.nextcloud_browser_url` for parity with the elicitation / provision / auth_tools call sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Round-1 findings addressed in
Left as-is (with reasons):
|
Round-2 nit: the module-level comment above ASTROLABE_SETTINGS_PATH still referenced nextcloud_public_issuer_url / nextcloud_host; the code (and the _astrolabe_settings_url docstring just below) now uses nextcloud_browser_url. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Round-2 nit addressed in |
SonarCloud's quality gate flagged the hardcoded Keycloak client secret, bootstrap password, and ephemeral test-user password in conftest.py (Security Rating D on new code, rule S2068). These are dev-only values that mirror keycloak/realm-export.json + docker-compose.yml (not real secrets). Add the repo's established `# NOSONAR(S2068)` markers, matching the pattern in tests/server/login_flow/test_app_password_loginname_mismatch.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Security Rating D gate was driven by the actual issues (not the credential literals the reviewer speculated about): - S5443 (CRITICAL) conftest.py — hardcoded /tmp screenshot path. Kept (CI uploads /tmp/*.png as debug artifacts) but hoisted to its own line with a # NOSONAR(S5443) marker so the suppression anchors to the flagged literal. - S5332 (MINOR) test_config.py / test_elicitation.py — http:// URLs in the new nextcloud_browser_url fallback-chain assertions. Switched to https:// (the scheme is irrelevant to what those unit tests assert). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
…ia OpenLDAP Adds an `ldap` docker-compose profile + integration lane that provides the live RED→GREEN reproduction of cbcoutinho#980 (DAV paths built from the loginName instead of the canonical Nextcloud UID) that the Keycloak lane (cbcoutinho#993) could not: login-by-email resolves to the real home (email is a path alias) and user_oidc hardcodes loginName == UID. - `openldap` service (vegardit/openldap:2.6.10) seeds user `alice` from ldap/bootstrap.ldif. Nextcloud's user_ldap maps her to a UID derived from the LDAP entryUUID, so `loginName (alice) != UID` AND `/remote.php/dav/files/alice/` does not resolve to her real home. - user_ldap is configured by an app-hook (app-hooks/post-installation/15-setup-ldap-backend.sh), gated on the openldap service so it is a no-op for every other lane — mirroring 15-setup-keycloak-provider.sh. - tests/server/ldap drives the multi-user BasicAuth MCP service (port 8003) as `alice`; the round-trip is xfail(strict=True): it fails on master (bug present) and xpasses once cbcoutinho#980's BaseNextcloudClient._ensure_principal_id discovery lands, at which point the marker should be dropped. - CI matrix gains an `ldap` lane (reuses multi-user-basic + `ldap` profile, no browser); docs added to CLAUDE.md. Verified locally: fresh-install hook auto-configures user_ldap; test xfails on master and xpasses with cbcoutinho#980 cherry-picked. Relates to cbcoutinho#980. Tracked on Deck card cbcoutinho#490. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>



What
Fixes a Login Flow v2 bug in external-IdP (Keycloak/OIDC) deployments and adds the missing end-to-end Keycloak Login Flow v2 WebDAV coverage that surfaced it. Started as an attempt to reproduce #980 in the keycloak lane; that investigation found the reproduction isn't feasible on the CI Nextcloud versions (see below) but uncovered two real bugs on the way.
Bug fixed: Login Flow v2
login_urlrewritten to the IdP originmcp-keycloakrewrote the Login Flow v2login_urlorigin toNEXTCLOUD_PUBLIC_ISSUER_URL. That setting is overloaded — it's the OAuth issuer for JWTissvalidation and was reused as the browser-reachable Nextcloud URL for the Login Flow redirect. When Nextcloud is its own IdP these coincide; with an external IdP the issuer is the IdP, so the browser-facing login URL was rewritten onto the IdP origin (http://localhost:8888/login/v2/flow/...) — which has no/login/v2endpoint and 404s. This breaks Login Flow v2 for any external-IdP deployment.Fix: a dedicated
NEXTCLOUD_PUBLIC_URL(browser-reachable Nextcloud base URL), resolved via a newSettings.nextcloud_browser_urlproperty with a backward-compatible fallback chain (nextcloud_public_url→nextcloud_public_issuer_url→nextcloud_host). Login Flow v2 / elicitation rewrites use it; OAuth issuer / JWT validation stays onnextcloud_public_issuer_url. Wired formcp-keycloakindocker-compose.yml. Existing login-flow deployments that set onlyNEXTCLOUD_PUBLIC_ISSUER_URLare unchanged.Also fixed: keycloak OAuth-leg fixture
The OAuth-leg fixture requested
talk.*scopes not registered on the Keycloak client →invalid_scope→ Keycloak error page → Playwright#usernametimeout. Replaced the flaky browser auth-code flow with a direct-grant (ROPC) token using realm-supported scopes (the OAuth-leg identity is irrelevant — the Login Flow v2 app password authenticates DAV). The divergent user + provisioned session are now session-scoped (one live user, one browser login) since the app-password store is keyed by the shared Keycloakadminidentity.Tests
tests/server/keycloak/test_keycloak_login_flow_webdav.py— end-to-end Keycloak Login Flow v2 WebDAV round-trip (create/write/read/list/delete). Fills the keycloak-lane coverage gap (previously only DCR/authorize) and guards theNEXTCLOUD_PUBLIC_URLfix — without it the session can't provision. Plus aSettings.nextcloud_browser_urlunit test and aNEXTCLOUD_PUBLIC_URL-preference elicitation test. Docs updated inconfiguration.mdandlogin-flow-v2.md.Relationship to #980 (why this is not a live reproduction)
The original goal was to reproduce #980's divergent-principal DAV bug via keycloak login-by-email. That doesn't reproduce on NC32/33, verified against a live stack:
loginName != UID, but Nextcloud resolves/remote.php/dav/files/<email>/to the user's real home (email is a valid path alias), so the WebDAV cycle succeeds with or without fix(client): resolve DAV paths via current-user-principal discovery #980's fix.user_oidc(the Keycloak backend) hardcodesloginName == UID(the sha256 hash) inLoginController.php— no config knob changes this./files/<login>/misses/files/<UID>/). That requires LDAP test infra, out of scope here; fix(client): resolve DAV paths via current-user-principal discovery #980's own mocked unit tests cover the fix.So the keycloak tests are kept as genuine end-to-end coverage (they caught both bugs above), not as a #980 reproduction.
Validation
uv run ruff check/ruff format --check/ty check— cleanuv run pytest tests/unit/— 1927 passed, 1 skippedThis PR was generated with the help of AI, and reviewed by a Human