Feature/oauth require email verified#111
Merged
Merged
Conversation
…uire_email_verified In gating + cluster_secret mode, GetClickHouseClientWithOAuth uses `oauthClaims.Email` verbatim as the ClickHouse `initial_user` field (server_client.go:303-311). ClickHouse trusts that impersonation because the cluster_secret authenticates the peer; the username is just metadata. So without `require_email_verified=true`, any IdP- issued token with `email_verified=false` (e.g. an Auth0 Database Connection that forgot to require verification, a self-hosted OIDC, a federated partner IdP) lets the bearer impersonate any provisioned ClickHouse user just by typing their email at registration. Reproducer: Auth0 tenant with both Google Social + a Database Connection that doesn't verify email. Attacker registers `admin@altinity.com` on the Database Connection (no verification sent / required), logs in via that connection, gets an id_token with `email=admin@altinity.com, email_verified=false`. MCP validates signature/iss/aud/exp — all pass. MCP routes the bearer to gating-mode CH using cluster_secret with `initial_user= admin@altinity.com`. CH looks up admin, applies admin's grants. Attacker now runs queries as admin. `allowed_email_domains` alone doesn't fix this — domain restriction allows `admin@altinity.com` regardless of `email_verified`. The actual protection is `require_email_verified=true`. Fix: validateOAuthRuntimeConfig now refuses to start when: - OAuth is enabled in gating mode AND - ClickHouse cluster_secret is set AND - require_email_verified is false Forces operators to opt in explicitly. Auth0 + Google Social deployments can flip the flag with one config line; deployments that intentionally federate with non-verifying IdPs need to redesign the impersonation path (don't use cluster_secret with unverified-email-source IdPs). Tests: 4 new sub-tests in TestValidateOAuthRuntimeConfig covering the unsafe combo, the safe-with-flag combo, the no-cluster-secret case (unaffected), and the forward-mode case (unaffected because forward never uses Email as initial_user). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Under the new gating semantics MCP no longer acts as the OAuth Authorization Server — Auth0 (or another DCR-capable AS) owns DCR, /authorize, /token, refresh rotation, and reuse detection. MCP's job under gating is to validate AS-issued JWTs (signature + RFC 8707 audience byte-equality + expiry) and authorize per-tool scopes. Code changes: - registerOAuthHTTPRoutes: under gating, only /.well-known/oauth-protected-resource is registered. Forward-mode routes unchanged. - handleOAuthProtectedResource: under gating advertises cfg.OAuth.Issuer as the upstream AS; forward unchanged. - handleOAuthCallback / handleOAuthTokenAuthCode: gating mint branches removed; forward path retained. - handleOAuthTokenRefresh and the gating-only mint helpers (gatingIdentity, mintGatingTokenResponse, encodeSelfIssuedAccessToken) deleted. - ValidateOAuthToken: both modes now route through parseAndVerifyExternalJWT (JWKS); parseAndVerifySelfIssuedOAuthToken removed. - validateOAuthClaims: issuer enforcement deferred to parseAndVerifyExternalJWT (UpstreamIssuerAllowlist); kept audience, expiry, scopes, identity-policy checks. Forward mode (Google direct, basic-tier Auth0, IdPs without DCR) is preserved unchanged — this is the supported escape hatch when the upstream AS doesn't support DCR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that gating mode no longer runs an AS leg, six OAuthConfig fields become forward-mode-exclusive: ClientID, ClientSecret, TokenURL, AuthURL, UserInfoURL, PublicAuthServerURL. Under gating these are silent dead weight that confuses operators migrating from the v1 "MCP-as-AS" gating semantics. validateOAuthRuntimeConfig now refuses startup with operator-oriented errors pointing at helm values when any of these fields are set under gating, and requires Issuer + Audience non-empty under gating (RFC 8707 byte-equality target for AS-issued JWTs). Forward-mode validation is untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ures (#109) Removes test surface that exercised the deleted gating-mode AS leg: self-issued access-token mint/decode, gating refresh-token grant, reuse-detection, gating-flow-e2e, and the standalone-claims issuer checks (issuer is now enforced upstream in parseAndVerifyExternalJWT via UpstreamIssuerAllowlist). Adds OAuth.Audience to three TestValidateOAuthRuntimeConfig fixtures that exercise gating mode — the new startup check requires Audience to be non-empty under gating. Forward-mode test coverage is untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflects the post-#109 semantics: gating mode is now a pure OAuth resource server, forward mode is unchanged. Adds: - Mode-taxonomy table + per-mode helm-values examples (live: otel for gating, antalya for forward) - Auth0 setup checklist for gating mode (per-cluster API resource, scopes, token policy, RFC 8707 + DCR notes) - Migration note: forbidden fields under gating that will refuse startup (client_id, client_secret, token_url, auth_url, userinfo_url, public_auth_server_url, refresh_revokes_tracking) - Rewritten Refresh Tokens / Identity Policy / Discovery sections to differentiate gating-mode (Auth0-managed) vs forward-mode (MCP-mediated) flows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auth0 enhanced-security third-party (DCR) clients silently drop non-namespaced custom claims from access tokens. Operators work around this with a post-login Action that re-adds email under a URL-prefixed claim key, e.g. `https://mcp.altinity.cloud/email`. The cluster_secret + Auth.Username impersonation path now accepts either the standard `email` claim, any `*/email` namespaced claim from OAuthClaims.Extra, or finally `sub` as a last-resort fallback. Required to make claude.ai / ChatGPT MCP connectors against an Auth0-fronted gating-mode MCP impersonate the OAuth user (rather than the Google user-id "google-oauth2|...") to ClickHouse. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
oauth: redefine gating mode as pure OAuth resource server (#109)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@BorisTyshkevich please use main as base branch always