refactor(config)!: rename OAUTH_SINGLE_AUDIENCE to LOGIN_FLOW, gate on ENABLE_LOGIN_FLOW

The AuthMode.OAUTH_SINGLE_AUDIENCE enum was a vestige of ADR-021's
original design where it co-existed with OAUTH_TOKEN_EXCHANGE. The
un-augmented OAuth bearer pass-through it represented relied on
Nextcloud-side patches to user_oidc (Bearer token validation on
non-OCS endpoints) that were never merged upstream (see
docs/authentication.md, docs/login-flow-v2.md). The working path —
mcp-login-flow profile — sets ENABLE_LOGIN_FLOW=true on top of this
mode so Login Flow v2 acquires per-user Nextcloud app passwords via
a browser flow. With OAUTH_TOKEN_EXCHANGE removed in 57303135, the
_AUDIENCE suffix in the Python name no longer disambiguates anything,
and the enum value diverged from the env-var spelling. ADR-022 (now
accepted) called for this rename as step 1 of consolidation.

- nextcloud_mcp_server/config_validators.py: rename enum to LOGIN_FLOW
  with value "login_flow". The mode_map key is now "login_flow"; the
  MODE_REQUIREMENTS entry requires `enable_login_flow=True`. Added a
  validation gate so MCP_DEPLOYMENT_MODE=login_flow without
  ENABLE_LOGIN_FLOW=true errors with a clear message pointing at
  ADR-022. Default auto-detection fallback returns LOGIN_FLOW.
- nextcloud_mcp_server/app.py: renamed three identifier references and
  switched the "Configuring MCP server for OAuth mode" log line to
  the uniform `mode.value` shape used by the other modes.
- nextcloud_mcp_server/api/management.py: renamed identifier in the
  /api/v1/status mapping. The user-visible "auth_mode": "oauth" string
  is preserved — that's a stable Astrolabe contract.
- nextcloud_mcp_server/config.py: updated Settings docstring.
- tests/unit/test_config_validators.py: renamed class
  TestOAuthSingleAudienceValidation → TestLoginFlowValidation,
  individual test methods, env-var strings; added enable_login_flow=True
  to fixtures expecting success; added a new test
  (test_login_flow_requires_enable_login_flow_flag) that exercises the
  validation gate.
- tests/unit/test_management_status_endpoint.py: renamed identifier.

BREAKING CHANGE: MCP_DEPLOYMENT_MODE=oauth_single_audience is no longer
accepted. Set MCP_DEPLOYMENT_MODE=login_flow (and keep
ENABLE_LOGIN_FLOW=true) for the same deployment. The un-augmented
OAuth path is no longer supported; if you previously ran the broken
path, you can either configure Login Flow v2 (recommended) or switch
to multi_user_basic / single_user_basic.

Dead-code pruning of `oauth_enabled and not enable_login_flow`
branches in app.py (lifespan, background sync) is deferred to a
separate follow-up PR per the consolidation plan in ADR-022.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-12 19:34:13 +02:00
co-authored by Claude Opus 4.7
parent 586859d66a
commit cafd318f36
6 changed files with 90 additions and 50 deletions
+1 -1
View File
@@ -212,7 +212,7 @@ async def get_server_status(request: Request) -> JSONResponse:
# Map deployment mode to auth_mode for API response
# This helps clients (like Astrolabe) determine which auth flow to use
if mode == AuthMode.OAUTH_SINGLE_AUDIENCE:
if mode == AuthMode.LOGIN_FLOW:
auth_mode = "oauth"
elif mode == AuthMode.MULTI_USER_BASIC:
auth_mode = "multi_user_basic"
+8 -5
View File
@@ -1015,8 +1015,11 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
logger.info(f"✅ Configuration validated successfully for {mode.value} mode")
logger.debug(f"Mode details:\n{get_mode_summary(mode)}")
# Derive helper variables for backward compatibility with existing code
oauth_enabled = mode == AuthMode.OAUTH_SINGLE_AUDIENCE
# Derive helper variables for backward compatibility with existing code.
# `oauth_enabled` is True for the LOGIN_FLOW (formerly OAUTH_SINGLE_AUDIENCE)
# multi-user OAuth mode — in this mode the MCP server is an OIDC relying
# party and Login Flow v2 acquires per-user Nextcloud app passwords.
oauth_enabled = mode == AuthMode.LOGIN_FLOW
# Log hybrid authentication status for multi-user BasicAuth with offline access
if mode == AuthMode.MULTI_USER_BASIC and settings.enable_offline_access:
logger.info(
@@ -1166,8 +1169,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
raise
# Create MCP server based on detected mode
if mode == AuthMode.OAUTH_SINGLE_AUDIENCE:
logger.info("Configuring MCP server for OAuth mode")
if mode == AuthMode.LOGIN_FLOW:
logger.info("Configuring MCP server for %s mode", mode.value)
# Asynchronously get the OAuth configuration
(
@@ -1932,7 +1935,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
# Check authentication configuration
# Report the deployment mode, not just whether OAuth is enabled
# This helps clients (like Astrolabe) determine which auth flow to use
if mode == AuthMode.OAUTH_SINGLE_AUDIENCE:
if mode == AuthMode.LOGIN_FLOW:
checks["auth_mode"] = "oauth"
checks["auth_configured"] = "ok"
elif mode == AuthMode.MULTI_USER_BASIC:
+3 -2
View File
@@ -408,9 +408,10 @@ def get_document_processor_config() -> dict[str, Any]:
class Settings:
"""Application settings from environment variables."""
# Deployment mode (ADR-021: explicit mode selection)
# Deployment mode (ADR-021: explicit mode selection; updated by ADR-022)
# Optional: If not set, mode is auto-detected from other settings
# Valid values: single_user_basic, multi_user_basic, oauth_single_audience
# Valid values: single_user_basic, multi_user_basic, login_flow
# (ADR-022: `oauth_single_audience` was renamed to `login_flow`.)
deployment_mode: str | None = None
# OAuth/OIDC settings
+30 -13
View File
@@ -25,7 +25,7 @@ class AuthMode(Enum):
SINGLE_USER_BASIC = "single_user_basic"
MULTI_USER_BASIC = "multi_user_basic"
OAUTH_SINGLE_AUDIENCE = "oauth_single"
LOGIN_FLOW = "login_flow"
@dataclass
@@ -113,8 +113,8 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
"Users provide credentials in request headers. "
"Optional background sync using app passwords stored via Astrolabe.",
),
AuthMode.OAUTH_SINGLE_AUDIENCE: ModeRequirements(
required=["nextcloud_host"],
AuthMode.LOGIN_FLOW: ModeRequirements(
required=["nextcloud_host", "enable_login_flow"],
optional=[
# OAuth credentials (uses DCR if not provided)
"oidc_client_id",
@@ -149,9 +149,13 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
# enables background operations in multi-user modes. No explicit
# enable_offline_access setting required.
},
description="OAuth multi-user deployment with single-audience tokens. "
"Tokens work for both MCP server and Nextcloud APIs (pass-through). "
"Uses Dynamic Client Registration if credentials not provided.",
description="OAuth multi-user deployment using Login Flow v2 to acquire "
"per-user Nextcloud app passwords via a browser flow. The MCP server "
"is an OIDC relying party of a configurable IdP (Nextcloud's built-in "
"OIDC by default; Keycloak, AWS Cognito, etc. via OIDC_DISCOVERY_URL). "
"Uses Dynamic Client Registration if credentials not provided. "
"Replaces the deprecated direct OAuth bearer-token pass-through which "
"required unmerged user_oidc patches (see ADR-022).",
),
}
@@ -159,11 +163,11 @@ MODE_REQUIREMENTS: dict[AuthMode, ModeRequirements] = {
def detect_auth_mode(settings: Settings) -> AuthMode:
"""Detect authentication mode from configuration.
Mode detection priority (ADR-021):
Mode detection priority (ADR-021, updated for ADR-022):
0. Explicit MCP_DEPLOYMENT_MODE (if set) - NEW in ADR-021
1. Multi-user BasicAuth
2. Single-user BasicAuth
3. OAuth single-audience (default OAuth mode)
3. Login Flow v2 (default — was OAuth single-audience pre-ADR-022)
Args:
settings: Application settings
@@ -185,7 +189,7 @@ def detect_auth_mode(settings: Settings) -> AuthMode:
mode_map = {
"single_user_basic": AuthMode.SINGLE_USER_BASIC,
"multi_user_basic": AuthMode.MULTI_USER_BASIC,
"oauth_single_audience": AuthMode.OAUTH_SINGLE_AUDIENCE,
"login_flow": AuthMode.LOGIN_FLOW,
}
if mode_str not in mode_map:
@@ -208,9 +212,11 @@ def detect_auth_mode(settings: Settings) -> AuthMode:
if settings.nextcloud_username and settings.nextcloud_password:
return AuthMode.SINGLE_USER_BASIC
# Default: OAuth single-audience mode
# This is the safest multi-user mode (no credential storage)
return AuthMode.OAUTH_SINGLE_AUDIENCE
# Default: Login Flow v2 multi-user mode (browser-based app-password flow).
# The un-augmented OAuth bearer pass-through it replaced required unmerged
# Nextcloud user_oidc patches (see ADR-022); selecting LOGIN_FLOW without
# ENABLE_LOGIN_FLOW=true fails validation below.
return AuthMode.LOGIN_FLOW
def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
@@ -301,7 +307,18 @@ def validate_configuration(settings: Settings) -> tuple[AuthMode, list[str]]:
f"{settings.nextcloud_host}"
)
if mode == AuthMode.OAUTH_SINGLE_AUDIENCE:
if mode == AuthMode.LOGIN_FLOW:
# ADR-022: LOGIN_FLOW requires the Login Flow v2 layer. The un-augmented
# OAuth bearer pass-through (formerly OAUTH_SINGLE_AUDIENCE without
# ENABLE_LOGIN_FLOW) needed unmerged Nextcloud user_oidc patches and
# is no longer supported.
if not settings.enable_login_flow:
errors.append(
f"[{mode.value}] ENABLE_LOGIN_FLOW=true is required for "
"login_flow mode. The un-augmented OAuth path is no longer "
"supported — see ADR-022."
)
# If OAuth credentials not provided, DCR must be available
# (This is a runtime check, not a config check, so we just warn)
if not settings.oidc_client_id or not settings.oidc_client_secret: