The /api/v1/chunk-context and /api/v1/pdf-preview handlers in
api/visualization.py forwarded the incoming OAuth bearer directly to
Nextcloud via NextcloudClient.from_token. In multi-user BasicAuth mode
Nextcloud has no validator for those bearers on Notes/WebDAV, so it
treats the request as anonymous and returns 401 — surfaced to the user
as a 500 from /apps/astrolabe/api/chunk-context. Search worked because
it only hits Qdrant.
Architecturally, OAuth is only for Astrolabe→MCP server; MCP server→
Nextcloud always uses the per-user app password stored during provision
(background sync already does this via vector.oauth_sync).
- Resolve the Nextcloud client through get_user_client_basic_auth in
both get_chunk_context and get_pdf_preview, surfacing
NotProvisionedError as a clean 401 instead of opaque 500.
- Apply the same fix to the session-cookie variant in
auth/viz_routes.chunk_context_endpoint for the internal viz UI.
Tests:
- New unit file test_management_chunk_context_endpoint.py, including a
regression guard that asserts get_user_client_basic_auth is awaited
(so reverting to from_token fails without needing a live Nextcloud).
- Updated test_management_pdf_preview_endpoint.py to mock the new auth
path (drops extract_bearer_token / NextcloudClient.from_token patches).
- New integration test test_astrolabe_chunk_context.py drives the full
chain (browser → Astrolabe → MCP → Nextcloud) in multi-user BasicAuth
mode, plus bare-bones 401 checks on the MCP endpoint.
Full unit suite: 546 passed.
Companion PR on astrolabe (cbcoutinho/astrolabe#66) sends the Nextcloud
UID as loginName in the app-password POST body so the stored record is
complete. Submodule bump to that branch will follow once CI reproduces
the failure on the old submodule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs made `uvx --from . nextcloud-mcp-server run` (and any pip install)
unusable outside Docker:
1. Dynaconf was configured with ignore_unknown_envvars=True and relied on
settings.toml to declare the key schema. With no settings.toml in a wheel
install, every env var (NEXTCLOUD_HOST, MCP_DEPLOYMENT_MODE, ...) was
silently dropped. Moved the schema into a Python _DEFAULTS dict passed
directly to Dynaconf, kept settings.toml as an optional external override
(renamed to settings.toml.example, gitignored), and pointed docker-compose
at the example file.
2. Token SQLite DB defaulted to /app/data/tokens.db in multiple places
(auth/storage.py, migrations.py, alembic/env.py, cli.py db subcommands),
which blew up at uvicorn startup with FileNotFoundError on non-Docker
hosts. Replaced with a new config.get_token_db_path() helper that
resolves TOKEN_STORAGE_DB if explicitly set, otherwise allocates a
per-process tempfile cleaned up at interpreter exit via atexit — mirroring
the "ephemeral by default" pattern used for QDRANT_LOCATION=:memory:.
Containers are unaffected: docker-compose services now explicitly set
TOKEN_STORAGE_DB=/app/data/tokens.db (the fourth service that was missing
this pin has been brought in line with the other three).
Verified end-to-end in an isolated /tmp venv: env-var-only startup, Alembic
migrations run against the tempfile, Application startup complete, /health/live
returns 200, tempfile deleted on SIGTERM. Unit tests (464) + ruff + ty pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
External IdPs like AWS Cognito return scopes prefixed with the resource
server identifier (e.g. https://mcp.example.com/notes.read). MCP tools
use bare scope names (notes.read) in @require_scopes decorators. Without
stripping the prefix, scope matching fails and only identity-only tools
(openid/profile/email) are visible — resulting in 4/125 tools shown.
Strip the OIDC_RESOURCE_SERVER_ID prefix in both get_access_token_scopes()
(used by list_tools filtering) and the require_scopes decorator (used at
tool execution time).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AWS Cognito provides refresh tokens automatically with the authorization
code flow but does not list offline_access as a supported scope. Check
the IdP's scopes_supported discovery field before including it in
requests, and always accept refresh tokens from responses regardless.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Flow 2 hardcoded offline_access in the scope string, but providers
like AWS Cognito don't support this scope (they handle refresh tokens
via client config). This caused invalid_scope errors on the Astrolabe
semantic search enablement flow.
Only include offline_access when enable_offline_access is explicitly
set, matching the behavior of DCR scope registration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AWS Cognito access tokens do not include an `aud` claim per RFC 7519 —
they use `client_id` instead. This causes `_has_mcp_audience` to reject
all Cognito-issued tokens with "Missing MCP audience. Got []".
When `aud` is empty, fall back to the `client_id` JWT claim for audience
validation. The MCP server's own client_id will be present there since
the AS proxy exchanges the authorization code using its credentials.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use dynaconf (get_settings()) instead of os.getenv for OIDC_RESOURCE_SERVER_ID
- Re-add Settings field, _field_map entry, and settings.toml default
- Add trailing-slash guard (.rstrip("/")) to prevent double-slash in scopes
- Add double-prefixing guard: skip scopes already carrying the prefix
- Add @pytest.mark.unit to test module
- Add test for already-prefixed scopes
- Document OIDC_RESOURCE_SERVER_ID in docs/configuration.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add offline_access to OIDC standard scopes exclusion list to prevent it
from being incorrectly prefixed, which would break Cognito refresh token
flows. Extract scope transformation into testable _transform_scopes_for_idp()
helper, add debug logging for prefixed scopes, remove unused Settings field
(oauth_routes.py consistently uses os.getenv), and add unit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When OIDC_RESOURCE_SERVER_ID is set, prefix resource scopes with the
identifier when forwarding to the IdP (e.g., calendar.read becomes
https://example.com/calendar.read). Required for IdPs like AWS Cognito
that mandate {resource_server_id}/{scope} format for custom scopes.
OIDC standard scopes (openid, profile, email) are forwarded as-is.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Many identity providers (AWS Cognito, Okta, Azure AD) reject or mishandle
colons in OAuth scope names. This migrates all custom scopes from
`resource:action` to `resource.action` format (e.g., `notes:read` →
`notes.read`), which is universally accepted and aligns with industry
conventions (Microsoft, Google).
Includes Alembic migration 004 for stored scope strings and ADR-024
documenting the rationale and RFC references.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Document wildcard scope policy in ClientRegistry class docstring
- Add hostname None guard and IPv6 loopback (::1) to redirect URI validation
- Simplify redirect URI scheme validation into single guard clause
- Add try/finally cleanup to DCR client deletion test
- Validate 302 Location header in unknown client rejection test
- Add unit tests for IPv6 loopback, malformed URIs, and DCR proxy paths
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the unused ALLOWED_MCP_CLOUD_CLIENTS env var — all clients are
defined via ALLOWED_MCP_CLIENTS or the static well-known defaults.
Add keycloak as an integration test profile in CI now that login-flow
replaces the old bearer token approach for external IdPs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge ALLOWED_MCP_CLOUD_CLIENTS into a single ALLOWED_MCP_CLIENTS env var
that supports both simple client IDs and pipe-separated client_id|redirect_uri
entries. Enforce HTTPS for non-localhost redirect URIs, warn on malformed
entries, and use wildcard scopes for all static clients (upstream IdP enforces
actual scopes). Add deprecation warning for the old env var.
Also fixes DCR proxy error messages to reference only ALLOWED_MCP_CLIENTS and
use "Upstream" instead of "Nextcloud" for IdP-agnostic language. Enables
Login Flow v2 + DCR on the mcp-keycloak docker-compose service.
Adds 17 unit tests for ClientRegistry parsing/validation and 7 keycloak
integration tests for DCR lifecycle, AS metadata, and client authorization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude AI (web) sends a Cognito-issued client_id with an HTTPS redirect
URI, but the client registry only supported localhost redirect URIs via
ALLOWED_MCP_CLIENTS. Add ALLOWED_MCP_CLOUD_CLIENTS env var for web-based
clients with format "client_id|redirect_uri".
Also fix the DCR proxy to return a clear error when the upstream IdP
(e.g. Cognito) doesn't support dynamic client registration, instead of
silently falling back to a Nextcloud-specific endpoint that fails.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both /app/provision and /app/provision/status now require a valid
Nextcloud OIDC bearer token via the Authorization header, reusing the
existing validate_token_and_get_user pattern from the management API.
This eliminates the open redirect vulnerability (only authenticated
Astrolabe users can trigger the flow) and prevents unauthenticated
resource exhaustion via Login Flow v2 session creation.
The authenticated user_id from the token replaces the untrusted
user_id query parameter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Log warning if /app mount not found when sharing poll task group
- Add docstring explaining unconditional task group creation
- Check session expires_at in provision_status to catch stale sessions
- Guard _poll_and_store status writes against cleanup-while-polling race
- Use "error" status (not "expired") when app_password is missing
- Remove hardcoded "Astrolabe Background Sync" user_agent string
- Fix async mock pattern (new_callable=AsyncMock) in test
- Add autouse fixture to clear _provision_sessions between tests
- Add _poll_and_store unit tests: completed, expired, error, cleanup
- Document all status values in provision_status docstring
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Escape HTML in _render_error to prevent XSS from exception messages
- Replace asyncio.create_task/sleep with anyio task group and sleep,
tying poll task lifetime to the app lifespan for proper cleanup
- Extract rewrite_url_origin() utility to fix duplicated URL rewriting
logic and replace urlparse._replace with stable urlunparse API
- Add warning log for insecure HTTP redirect URIs
- Add unit tests for validation, XSS escaping, route handlers, and
URL rewriting (16 new tests in test_provision_routes.py)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nextcloud returns poll/login URLs using its internal hostname (e.g.
http://localhost/login/v2/poll) which is unreachable from the MCP
server container in Docker networks. Rewrite the poll endpoint's
origin to use the configured NEXTCLOUD_HOST so server-side polling
works correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nextcloud may omit default ports in the login_url (e.g. http://app
instead of http://app:80). Extract just scheme+hostname from
NEXTCLOUD_HOST for the URL replacement check.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the popup-based approach with a direct redirect to Nextcloud's
login page. This is more compatible with Playwright E2E tests and
simpler for users. The background polling task still runs server-side
to store the app password when the user grants access.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add /app/provision and /app/provision/status endpoints for browser-based
Login Flow v2 app password provisioning. Used by Astrolabe's "Enable
Semantic Search" to chain OAuth (bearer token) + Login Flow v2 (app
password) in a single user interaction.
The provision page initiates Login Flow v2, opens Nextcloud's login URL
in a popup, polls for completion via background task, and redirects back
to the caller's redirect_uri on success.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add docker compose services (tailscale-mcp + nginx-claude-filter) behind
a claude-funnel profile that expose the login-flow MCP server via
Tailscale Funnel with IP-based access control:
- /mcp endpoint restricted to Claude AI outbound IPs (160.79.104.0/21)
- /oauth/*, /.well-known/*, /app paths open to all IPs (user login flow)
- All other paths return 404
Also add favicon.png served at /favicon.ico for connector directory
discovery (Google favicon service).
Usage:
docker compose --profile login-flow --profile claude-funnel up -d
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Relax redirect_uri validation to accept HTTPS for remote hosts (e.g.,
cloud-hosted MCP clients like Claude AI) while keeping HTTP allowed
for localhost per RFC 8252 loopback exception.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Drop OIDC fork: comment out third_party/oidc mount, use upstream
v1.16.3 from app store (fixes consent redirect race, PR #631)
- Support client_secret_basic auth: add _extract_basic_auth() helper
so TS MCP SDK can authenticate at token endpoint (RFC 6749 §2.3.1)
- Multi-issuer JWT validation: accept tokens with internal Docker
issuer (http://app:80) or public URL (NEXTCLOUD_PUBLIC_ISSUER_URL)
since AS proxy obtains tokens server-to-server
- Introspection fallback: try token introspection when JWT verification
fails, supporting both JWT and opaque token types
- Register all tool scopes in DCR: add semantic:read, collectives:read,
collectives:write to OIDC client allowed_scopes so tokens include
them and semantic search tools are visible to authenticated clients
- Auto-create Astrolabe OAuth client: new app-hook creates OIDC client
and stores credentials in config.php so the "Authorize via OAuth"
button works without manual setup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace `assert entry.code_challenge` with a proper if-guard returning a
500 JSON error in the token endpoint, since Python's -O flag strips
asserts and would silently disable PKCE enforcement.
Invalidate the scope cache immediately after Login Flow v2 provisioning
completes, so users no longer hit ProvisioningRequiredError for up to
5 minutes after successfully authenticating.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Disable Nextcloud's bruteforce protection and rate limiting via a new
post-installation hook, preventing 429 errors during repeated DCR calls
in CI. Add warning-level logging to all 8 error paths in the AS proxy
token endpoint to make login-flow 400 errors diagnosable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 429 retry with exponential backoff to register_client() (fixes CI
oauth matrix failures from parallel DCR requests)
- Make client_id, redirect_uri, and PKCE mandatory at token endpoint
- Add null-checks for discovery_url and OAuth credentials in proxy flows
- Add OIDC discovery document caching with 5-min TTL
- Add per-IP rate limiting on /oauth/register DCR proxy
- Discover DCR endpoint from OIDC discovery instead of hardcoding
- Extract extract_user_id_from_token to auth/token_utils.py (breaks
circular imports between server/ and auth/ layers)
- Add TTL scope cache in scope_authorization.py (avoids DB hit per tool)
- Add defense-in-depth scope validation in storage layer
- Broaden elicitation exception handling with graceful fallback
- Add idempotentHint to nc_auth_check_status, return "pending" status
after accepted elicitation, add polling interval to description
- Change ALL_SUPPORTED_SCOPES from tuple to frozenset for O(1) lookups
- Replace Optional[str] with str | None throughout config.py
- Use default_factory for ProxyCodeEntry/ASProxySession dataclasses
- Add proxy code/session cleanup to background loop
- Fix OIDC verification CI step to only run for oauth/login-flow modes
- Add unit tests for access.py REST endpoints (10 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MCP clients like Claude Code were unable to use tools because tokens
obtained directly from Nextcloud had the wrong audience claim. The MCP
server now acts as its own OAuth Authorization Server, proxying auth
to Nextcloud with its own client_id so tokens have the correct audience.
New endpoints: /.well-known/oauth-authorization-server, /oauth/token,
/oauth/register. Modified /oauth/authorize from pass-through to
intermediary pattern. PRM now points authorization_servers to the MCP
server instead of Nextcloud.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Consolidate MCP session + login flow cleanup into _mcp_session_with_login_flow() helper,
replacing 4 duplicated AsyncExitStack sites in app.py
- Fix get_shared_storage() race condition by using module-level anyio.Lock() init
(reverts regression from ba59763)
- Collapse cosmetic if/else branching in scope_authorization.py
- Consolidate dual password storage paths into single store_app_password_with_scopes() call
- Mark unused request param as _ in list_supported_scopes
- Make ALL_SUPPORTED_SCOPES an immutable tuple; use list() instead of .copy()
- Add hasattr(ctx, "elicit") guard in elicitation.py, narrow except to NotImplementedError
- Add YAML comment explaining --oauth flag for mcp-login-flow service
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix anyio.Lock() created at module import time; use lazy init in
get_shared_storage() to avoid instantiation before event loop exists
- Stop get_login_flow_session from silently swallowing DB exceptions;
re-raise and handle in caller with proper error response
- Update ProvisionAccessResponse and UpdateScopesResponse status field
docs to include all actual values (declined, cancelled, unchanged)
- Narrow except clause in present_login_url to (AttributeError,
NotImplementedError) instead of bare Exception
- Add KeyError handling in LoginFlowV2Client.initiate() and poll() for
clear errors on malformed Nextcloud responses
- Simplify redundant env-var bypass branches in scope_authorization.py
- Extract _maybe_login_flow_cleanup() context manager to replace 4
inline cleanup loop registrations in app.py; move sleep to end of
loop body so cleanup runs once at startup
- Replace fragile string replacement in _rewrite_login_flow_url with
proper urllib.parse URL handling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix circular dependency in scope_authorization: auth tools requiring
only identity scopes (openid/profile/email) now bypass the login flow
provisioning check, so unprovisioned users can call provisioning tools
- Fix no-op detection in nc_auth_update_scopes: NULL scopes (legacy "all")
now correctly map to ALL_SUPPORTED_SCOPES instead of empty set
- Fix get_app_password_with_scopes swallowing exceptions: re-raise instead
of returning None, matching sibling methods
- Add missing audit logging to update_app_password_scopes,
delete_login_flow_session, and delete_expired_login_flow_sessions
- Pin setup-uv to v7.3.1 in CI unit-test job (was v7.3.0)
- Add FastMCP type annotation to register_auth_tools parameter
- Log warning when user accepts elicitation without checking acknowledged box
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Consolidate three independent RefreshTokenStorage lazy singletons into a
single lock-protected get_shared_storage() function, eliminating race
conditions on concurrent first-access. Remove blanket try/except in
_get_stored_scopes so storage errors propagate as proper MCP errors
instead of silently triggering "please provision" messages. Handle
declined/cancelled elicitation results in Login Flow tools by cleaning up
sessions and returning clear status. Add update_app_password_scopes() to
avoid unnecessary decrypt/re-encrypt when only scopes change. Add
unprovisioned-user early exit and no-op detection to nc_auth_update_scopes.
Remove four dead config fields and misleading NEXTCLOUD_PASSWORD deprecation
warning. Add periodic login flow session cleanup task. Generate separate
Fernet keys per service. Add board cleanup in deck integration test. Gate
CI unit tests on linting and skip Astrolabe build for single-user profile.
Fix test markers from oauth to multi_user_basic for astrolabe integration
tests. Update login_flow.py docstrings to document outbound HTTP calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix data loss in nc_auth_update_scopes: remove premature
delete_app_password call; old password stays valid until upsert
replaces it on successful re-provisioning
- Replace assert with proper error return in nc_auth_check_status
- Add lazy singleton for RefreshTokenStorage in auth_tools,
scope_authorization, and context to avoid per-call re-initialization
- Centralize _is_login_flow_mode() to get_settings().enable_login_flow
and remove duplicate definitions and per-call os.getenv reads
- Add dev-only comment to TOKEN_ENCRYPTION_KEY in docker-compose.yml
- Gate OIDC build steps in CI behind matrix.needs-playwright
- Add diagnostic step reporting Playwright skip count in CI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable ruff PLC0415 rule for all source files (tests excluded via
per-file-ignores). Move 136 inline imports to top-level across 33 files.
8 imports suppressed with noqa for legitimate reasons: circular
dependencies (client/__init__.py, context.py), optional dependency
guards (app.py document processors, auth/userinfo_routes.py), and
post-env-setup imports (smithery_main.py).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move httpx import to top-level and use anyio task group for concurrent
validation in cleanup_invalid_app_passwords (storage.py)
- Respect Retry-After header for 429 responses, capped at 300s (oauth_sync.py)
- Soften pre-validation exceptions so transient failures don't crash the
background sync task (oauth_sync.py)
- Replace f-string SQL with blanket DELETE and add returncode checks (conftest.py)
- Extract clear_stale_test_state() helper to deduplicate cleanup logic
in astrolabe background sync tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The revoke test failed because it only completed Step 2 (app password) but
not Step 1 (OAuth authorization). In hybrid mode, Astrolabe requires both
steps for $isFullyConfigured=true, which gates the "Revoke Access" button.
Changes:
- Use complete_astrolabe_authorization() in revoke test for full two-step flow
- Add stale state cleanup (app passwords, bruteforce entries, Astrolabe prefs)
to both enablement and revoke tests
- Add startup cleanup of invalid app passwords in BasicAuth mode
- Pre-validate credentials before entering scanner loop to fail fast
- Handle 401/403/429 in scanner with proper backoff and circuit breaking
- Clean up app passwords in test_users_setup fixture teardown
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add NEXTCLOUD_VERIFY_SSL and NEXTCLOUD_CA_BUNDLE env vars to configure
TLS certificate verification for all outbound Nextcloud connections.
Centralizes SSL config via a new HTTP client factory (http.py) used by
all 27 Nextcloud-bound call sites, including API clients, OIDC endpoints,
OAuth flows, and health checks.
Closes#560
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace direct os.getenv() calls with get_settings().vector_sync_enabled
to ensure consistent behavior with both VECTOR_SYNC_ENABLED (deprecated)
and ENABLE_SEMANTIC_SEARCH environment variables.
Also add webhook management documentation guide.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, the multi-user BasicAuth mode attempted to retrieve app passwords
via OAuth client_credentials grant, which Nextcloud OIDC doesn't support.
This fix implements local storage for app passwords:
- Add app_passwords table via Alembic migration (002)
- Add store/get/delete methods to RefreshTokenStorage
- Add management API endpoints for app password provisioning:
- POST /api/v1/users/{user_id}/app-password
- GET /api/v1/users/{user_id}/app-password
- DELETE /api/v1/users/{user_id}/app-password
- Update oauth_sync.py to read from local storage
- Update Astrolabe to send app passwords to MCP server after validation
- Add app-hook to configure mcp_server_url in Nextcloud
The flow is now:
1. User creates app password in Nextcloud Security settings
2. User enters it in Astrolabe Personal Settings
3. Astrolabe validates against Nextcloud, then sends to MCP server
4. MCP server stores encrypted app password locally
5. Background sync uses locally stored password
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes NC PHP app (Astrolabe) OAuth integration by making token validation
more lenient for management API access.
Problem:
- Astrolabe calls Nextcloud OIDC token endpoint via internal URL (http://localhost)
- Tokens are issued with iss: http://localhost (internal)
- MCP server expects iss: http://localhost:8080 (external)
- Token validation failed with "Invalid issuer"
Solution:
- Add skip_issuer_check parameter to _verify_jwt_signature()
- verify_token_for_management_api() now skips both audience and issuer checks
- Security maintained: signature still verified, authorization checked by API
Also includes related fixes from previous session:
- Update test selectors for Vue 3 UI ("Enable Semantic Search")
- Fix OIDC discovery URL transformation in OAuthController.php
- Add overwrite.cli.url to setup hook for proper external URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Migrate all direct ENABLE_OFFLINE_ACCESS environment variable checks to
use settings.enable_offline_access, which handles both the new
ENABLE_BACKGROUND_OPERATIONS and deprecated ENABLE_OFFLINE_ACCESS vars.
Also fixes JWT issuer validation in Docker by using NEXTCLOUD_PUBLIC_ISSUER_URL
when set, resolving 401 errors caused by internal/external URL mismatch.
Changes:
- app.py: Use settings for offline access checks in setup_oauth_config,
register_oauth_client, and tool registration
- oauth_tools.py: Use settings in provision_nextcloud_access and check_logged_in
- management.py: Use settings in get_user_session
- scope_authorization.py: Use settings in require_scopes decorator
- Remove unused os imports after migration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove URL rewriting logic from MCP server that was converting
public URLs to internal Docker URLs. This was a workaround for
Nextcloud's overwritehost setting forcing URLs to localhost:8080.
Changes:
- Remove OIDC endpoint rewriting in app.py (setup_oauth_config)
- Remove OIDC_JWKS_URI override support (no longer needed)
- Remove URL rewriting in browser_oauth_routes.py
- Remove URL rewriting in token_broker.py
- Update Helm chart values and README
- Add hybrid auth setup unit tests
- Update Astrolabe admin UI for Vue 3
The proper fix is in the previous commit which removes the
overwritehost setting from Nextcloud, allowing it to respect
the Host header from incoming requests.
Implement multi-user BasicAuth pass-through mode (ADR-020) where each
request includes BasicAuth credentials that are forwarded to Nextcloud
APIs without persistent storage.
Changes:
- Add _get_client_from_basic_auth() in context.py to extract credentials
from Authorization header (set by BasicAuthMiddleware)
- Add AstrolabeClient for app password provisioning via Astrolabe API
- Update oauth_sync.py with dual credential support (app passwords first,
then refresh tokens as fallback)
- Simplify oauth_tools.py provisioning logic
- Add integration tests for app password provisioning and multi-user BasicAuth
Features:
- Stateless multi-user mode: credentials passed per-request
- Optional background sync via app passwords (stored in Astrolabe)
- Falls back to refresh tokens if app password not available
- Test coverage for provisioning flow and pass-through mode
Related: ADR-019 (Multi-user BasicAuth), ADR-020 (Deployment Modes)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>