Skip to content

oauth: H-1 — refuse to start when gating + cluster_secret without require_email_verified#105

Open
BorisTyshkevich wants to merge 1 commit intofeature/oauth-spec-compliance-hardeningfrom
feature/oauth-require-email-verified
Open

oauth: H-1 — refuse to start when gating + cluster_secret without require_email_verified#105
BorisTyshkevich wants to merge 1 commit intofeature/oauth-spec-compliance-hardeningfrom
feature/oauth-require-email-verified

Conversation

@BorisTyshkevich
Copy link
Copy Markdown
Collaborator

Summary

H-1 from the OAuth security review: refuse to start in gating mode when clickhouse.cluster_secret is set but oauth.require_email_verified is false.

This is the second of a 3-PR stack:

  1. PR oauth: spec-compliance hardening + HKDF + C-1 forward-mode validation #104 — spec-compliance hardening + HKDF + C-1 (base of this PR)
  2. This PR (PR-B) — H-1 require_email_verified gate
  3. PR-C (after this lands) — H-2 refresh-token reuse detection (#103)

Note on PR base: This PR is stacked on top of #104. Until #104 merges, the diff shown is against feature/oauth-spec-compliance-hardening. Once #104 lands, GitHub will auto-rebase this onto main and the diff will narrow to the H-1 commit alone.

Threat model

In gating mode with clickhouse.cluster_secret configured, the path at pkg/server/server_client.go:303-311 feeds oauthClaims.Email verbatim into ClickHouse's initial_user. ClickHouse trusts the impersonation because the cluster_secret authenticates the peer, not the end user.

Without oauth.require_email_verified=true, any IdP-issued token with email_verified=false lets the bearer impersonate any ClickHouse user just by typing their email at registration:

  • Auth0 Database Connection that forgot to require verification
  • Self-hosted OIDC without verification gating
  • Federated partner IdP with weak verification

Auth0's hosted social-login providers (Google, GitHub) emit email_verified=true reliably, but a misconfigured custom DB connection or a permissive partner IdP can issue email_verified=false tokens.

Mitigation

validateOAuthRuntimeConfig in cmd/altinity-mcp/main.go refuses to boot when IsGatingMode() && cluster_secret != "" && !RequireEmailVerified. Operators must explicitly opt in to the verified-email check, and the binary won't start without it. Auth0 + Google Social emit email_verified=true on every token, so this is safe to enable in our demo deployments.

The RequireEmailVerified field already existed in OAuthConfig (used by validateOAuthIdentityPolicy); this PR only adds the startup check that makes it mandatory under the dangerous combination.

Live deployment

Already running on otel-mcp.demo.altinity.cloud since the H-1 deploy (image feature-strict-schema-14da408). currentUser() returns btyshkevich@altinity.com confirming the gating-mode + cluster_secret + email-verified chain works correctly with verified Auth0 + Google Social tokens.

Test plan

  • New test in cmd/altinity-mcp/oauth_server_test.go exercises validateOAuthRuntimeConfig rejection with the dangerous combination
  • Live on otel-mcp; smoke test through claude.ai connector returns the impersonated email
  • CI green on the stacked diff
  • Reviewer eyes on the startup-error message wording

🤖 Generated with Claude Code

…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>
@BorisTyshkevich BorisTyshkevich force-pushed the feature/oauth-require-email-verified branch from 804528d to 5302328 Compare May 7, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant