mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-16 17:46:20 +00:00
* feat(auth): make login rate-limit parameters configurable, fixes #5108 Add auth.local.max_login_attempts (default 5) and auth.local.lockout_seconds (default 300) so operators can tune the per-IP login throttle: raise the ceiling for shared-egress-IP offices behind proxies/NAT, or tighten it for stricter posture. Policy is live-read per call (matching the _local_registration_enabled precedent), so a config reload applies without a Gateway restart; raising the threshold mid-lockout immediately unblocks affected IPs. Review feedback addressed (willem-bd): - Only FileNotFoundError falls back to the hardcoded defaults; a malformed config propagates, mirroring _local_registration_enabled, so an operator who tightened the policy never silently gets the more permissive defaults. - _check_rate_limit looks up the record before resolving the policy, so a clean IP pays zero config reads (get_app_config re-hashes config.yaml per call and login_local is an unauthenticated async endpoint). Bumps config_version to 39 in config.example.yaml and the Helm chart (values.yaml + README example) so the chart drift check stays green. * fix(auth): reject max_login_attempts=1 and honor live lockout_seconds for active lockouts * fix(auth): close live-policy state gaps in login throttle (resurrection, count reset, broken-config verification) * fix(auth): commit evaluated lockout duration on decreases too, preventing raise-resurrection * test(auth): pin broken-config fail-closed sequence through the login route * fix(auth): sweep expired locks by stored sentence and keep policy reads off the event loop * fix(auth): re-read throttle record after the policy-resolution yield point --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
109 lines
5.2 KiB
Python
109 lines
5.2 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."
|
|
),
|
|
)
|
|
max_login_attempts: int = Field(
|
|
default=5,
|
|
ge=2,
|
|
description=(
|
|
"Failed login attempts allowed from one client IP before it is locked out of "
|
|
"POST /api/v1/auth/login/local. Defaults preserve the historical hardcoded policy. "
|
|
"Raise it when many users share an egress IP (corporate proxy / NAT); lower it for "
|
|
"a stricter posture. Minimum 2: one failed attempt must never lock an IP, or a "
|
|
"single typo would block everyone behind a shared egress — the strictest legal "
|
|
"value locks after the second failure. The counter is per-Gateway-worker "
|
|
"(in-process), so effective attempts in multi-worker deployments scale with "
|
|
"worker count."
|
|
),
|
|
)
|
|
lockout_seconds: float = Field(
|
|
default=300.0,
|
|
gt=0,
|
|
allow_inf_nan=False,
|
|
description=("Seconds an IP stays locked out after reaching auth.local.max_login_attempts. Defaults preserve the historical hardcoded policy (5 minutes)."),
|
|
)
|
|
|
|
|
|
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")
|