Aari 09e25b8a32
fix(auth): let deployments close local self-registration (#4311)
* fix(auth): let deployments close local self-registration

The OIDC provisioning policy (allowed_email_domains, require_verified_email,
auto_create_users) is enforced only in the SSO callback via
get_or_provision_oidc_user. POST /api/v1/auth/register creates a local account
without consulting any of it, and nothing can turn that path off, so a
deployment declaring an email-domain allowlist can still be joined by any
address through local registration.

Add auth.local.allow_registration (default true, so existing deployments are
unchanged) and gate /register on it before the account is created. Report the
flag from /setup-status so the login page stops offering a signup entry the
Gateway will reject.

/initialize is deliberately not gated: it is the bootstrap path, guarded by
admin_count == 0, and closing it would leave a fresh install unable to create
its first admin.

An unreadable config.yaml falls back to the pre-gate default (open) rather than
making these two endpoints a hard dependency on the file.

* docs(auth): align registration-gate fallback wording with the FileNotFoundError catch
2026-07-20 23:33:09 +08:00

89 lines
4.1 KiB
Python

"""OIDC / SSO authentication configuration models."""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, Field
class OIDCProviderConfig(BaseModel):
"""Configuration for a single OIDC identity provider (Keycloak, Google, Azure AD, etc.)."""
display_name: str = Field(description="Human-readable name shown on the login button")
issuer: str = Field(description="OIDC issuer URL (e.g. https://keycloak.example.com/realms/deerflow)")
client_id: str = Field(description="OAuth2 client ID assigned by the provider")
client_secret: str | None = Field(default=None, description="OAuth2 client secret ($ENV_VAR references supported)")
redirect_uri: str | None = Field(default=None, description="Callback URL the provider will redirect to after auth")
scopes: list[str] = Field(
default_factory=lambda: ["openid", "email", "profile"],
description="OIDC scopes to request (must include openid)",
)
token_endpoint_auth_method: Literal["client_secret_post", "client_secret_basic", "none"] = Field(
default="client_secret_post",
description="How the client authenticates at the token endpoint",
)
# ── User provisioning ─────────────────────────────────────────────
auto_create_users: bool = Field(
default=True,
description="Automatically create a DeerFlow user on first SSO login",
)
require_verified_email: bool = Field(
default=True,
description="Reject authentication if the provider does not report the email as verified",
)
allowed_email_domains: list[str] = Field(
default_factory=list,
description="If non-empty, only allow users whose email domain is in this list (e.g. ['example.com'])",
)
admin_emails: list[str] = Field(
default_factory=list,
description="Users with these email addresses are automatically granted the admin role on first login",
)
# ── PKCE / nonce ──────────────────────────────────────────────────
pkce_enabled: bool = Field(default=True, description="Enable PKCE (S256) for the authorization code flow")
nonce_enabled: bool = Field(default=True, description="Include and validate the nonce claim in ID tokens")
# ── Endpoint overrides (for providers with non-standard discovery) ─
authorization_endpoint: str | None = Field(default=None)
token_endpoint: str | None = Field(default=None)
userinfo_endpoint: str | None = Field(default=None)
jwks_uri: str | None = Field(default=None)
class OIDCAuthConfig(BaseModel):
"""Top-level OIDC authentication configuration."""
enabled: bool = Field(default=False, description="Enable OIDC SSO authentication")
frontend_base_url: str | None = Field(
default=None,
description="Base URL of the frontend (used for callback redirects when behind a reverse proxy)",
)
providers: dict[str, OIDCProviderConfig] = Field(
default_factory=dict,
description="Map of provider IDs to their configuration (e.g. keycloak, google, azure)",
)
class LocalAuthConfig(BaseModel):
"""Configuration for the built-in email/password authentication provider."""
allow_registration: bool = Field(
default=True,
description=(
"Allow visitors to self-register a local account via POST /api/v1/auth/register. "
"Set to false when accounts are provisioned exclusively through SSO — the OIDC "
"provisioning policy (allowed_email_domains, require_verified_email, auto_create_users) "
"does not apply to local registration."
),
)
class AuthAppConfig(BaseModel):
"""Authentication configuration section for the DeerFlow app config."""
oidc: OIDCAuthConfig = Field(default_factory=OIDCAuthConfig, description="OIDC SSO authentication settings")
local: LocalAuthConfig = Field(default_factory=LocalAuthConfig, description="Built-in email/password authentication settings")