feat: harden Claude MCP OAuth transport
This commit is contained in:
@@ -106,12 +106,12 @@ class Settings(BaseSettings):
|
||||
description="Secret detection mode: off, mask, or block",
|
||||
)
|
||||
|
||||
# OAuth2 configuration (for ChatGPT per-user Gitea authentication)
|
||||
# OAuth2 configuration (for per-client Gitea authentication)
|
||||
oauth_mode: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Enable per-user OAuth2 authentication mode. "
|
||||
"When true, each ChatGPT user authenticates with their own Gitea account. "
|
||||
"When true, each client user authenticates with their own Gitea account. "
|
||||
"GITEA_TOKEN and MCP_API_KEYS are not required in this mode."
|
||||
),
|
||||
)
|
||||
@@ -126,8 +126,9 @@ class Settings(BaseSettings):
|
||||
oauth_expected_audience: str = Field(
|
||||
default="",
|
||||
description=(
|
||||
"Expected OIDC audience for access tokens. "
|
||||
"Defaults to GITEA_OAUTH_CLIENT_ID when unset."
|
||||
"Additional expected OIDC audience for access tokens. The canonical MCP "
|
||||
"resource URL and the Gitea OAuth client id are always accepted; set this "
|
||||
"to require an extra audience value."
|
||||
),
|
||||
)
|
||||
oauth_cache_ttl_seconds: int = Field(
|
||||
@@ -139,6 +140,37 @@ class Settings(BaseSettings):
|
||||
default="https://hiddenden.cafe/docs/mcp-gitea",
|
||||
description="Public documentation URL for OAuth-protected MCP resource behavior",
|
||||
)
|
||||
oauth_state_secret: str = Field(
|
||||
default="",
|
||||
description=(
|
||||
"Server secret used to HMAC-sign the OAuth proxy state parameter. "
|
||||
"Required when OAUTH_MODE=true so callback state is tamper-evident."
|
||||
),
|
||||
)
|
||||
oauth_redirect_allowlist_raw: str = Field(
|
||||
default="",
|
||||
description=(
|
||||
"Comma-separated additional allowed client redirect URIs for the OAuth "
|
||||
"callback proxy. Claude's callback URLs and loopback URIs are always allowed."
|
||||
),
|
||||
alias="OAUTH_REDIRECT_ALLOWLIST",
|
||||
)
|
||||
dcr_enabled: bool = Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Enable RFC 7591 Dynamic Client Registration at /register. Claude's "
|
||||
"connectors register dynamically; disable to require manual client_id/secret."
|
||||
),
|
||||
)
|
||||
dcr_storage_path: Path = Field(
|
||||
default=Path("/var/lib/aegis-mcp/dcr_clients.json"),
|
||||
description="Path to the JSON file that persists dynamically registered clients",
|
||||
)
|
||||
repo_authz_cache_ttl_seconds: int = Field(
|
||||
default=60,
|
||||
description="TTL (seconds) for cached per-user repository permission decisions",
|
||||
ge=1,
|
||||
)
|
||||
|
||||
# Authentication configuration
|
||||
auth_enabled: bool = Field(
|
||||
@@ -269,12 +301,28 @@ class Settings(BaseSettings):
|
||||
"Set ALLOW_INSECURE_BIND=true to explicitly permit this."
|
||||
)
|
||||
|
||||
extra_redirect_uris: list[str] = []
|
||||
if self.oauth_redirect_allowlist_raw.strip():
|
||||
extra_redirect_uris = [
|
||||
value.strip()
|
||||
for value in self.oauth_redirect_allowlist_raw.split(",")
|
||||
if value.strip()
|
||||
]
|
||||
object.__setattr__(self, "_oauth_redirect_allowlist", extra_redirect_uris)
|
||||
|
||||
if self.oauth_mode:
|
||||
# In OAuth mode, per-user Gitea tokens are used; no shared bot token or API keys needed.
|
||||
if not self.gitea_oauth_client_id.strip():
|
||||
raise ValueError("GITEA_OAUTH_CLIENT_ID is required when OAUTH_MODE=true.")
|
||||
if not self.gitea_oauth_client_secret.strip():
|
||||
raise ValueError("GITEA_OAUTH_CLIENT_SECRET is required when OAUTH_MODE=true.")
|
||||
# The proxy state parameter carries the client's redirect_uri across the Gitea
|
||||
# round-trip; it must be HMAC-signed, which requires a server-held secret.
|
||||
if not self.oauth_state_secret.strip():
|
||||
raise ValueError(
|
||||
"OAUTH_STATE_SECRET is required when OAUTH_MODE=true so the OAuth "
|
||||
"proxy state parameter can be HMAC-signed and verified."
|
||||
)
|
||||
else:
|
||||
# Standard API key mode: require bot token and at least one API key.
|
||||
if not self.gitea_token.strip():
|
||||
@@ -308,6 +356,11 @@ class Settings(BaseSettings):
|
||||
"""Get parsed list of repositories allowed for write-mode operations."""
|
||||
return list(getattr(self, "_write_repository_whitelist", []))
|
||||
|
||||
@property
|
||||
def oauth_redirect_allowlist(self) -> list[str]:
|
||||
"""Get parsed list of additional allowed client redirect URIs."""
|
||||
return list(getattr(self, "_oauth_redirect_allowlist", []))
|
||||
|
||||
@property
|
||||
def gitea_base_url(self) -> str:
|
||||
"""Get Gitea base URL as normalized string."""
|
||||
|
||||
Reference in New Issue
Block a user