This document explains how to configure OAuth 2.0 / OpenID Connect (OIDC) authentication with the Altinity MCP Server.
OAuth 2.0 authorization supports two modes.
Use this when ClickHouse has native OAuth support (Altinity Antalya 25.8+). The MCP server passes the bearer token through; ClickHouse validates it.
- An MCP client authenticates with an Identity Provider (IdP) and obtains a token
- The MCP client sends the token to the MCP server in the
Authorization: Bearer {token}header - The MCP server requires only that a bearer token is present (it does not validate the token locally)
- The MCP server forwards the token to ClickHouse via HTTP headers
- ClickHouse validates the token using
token_processorsand authenticates the user
┌────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐
│ MCP │─────>│ IdP │ │ MCP │ │ ClickHouse │
│ Client │<─────│(Keycloak,│ │ Server │ │ (Antalya) │
│ │ │ Azure AD,│ │ │ │ │
│ │ │ Google) │ │ │ │ │
│ │ └──────────┘ │ │ │ │
│ │ │ │ │ │
│ │──Bearer token─────────>│ │ │ │
│ │ │─Bearer──>│ │ │
│ │ │ token │─────>│ Validates │
│ │ │ │ │ via OIDC/ │
│ │<───────────────────────│<─────────│<─────│ JWKS │
│ │ query results │ │ │ │
└────────┘ └──────────┘ └────────────┘
clickhouse:
host: "clickhouse.example.com"
port: 8123
protocol: http
server:
oauth:
enabled: true
mode: "forward"
gating_secret_key: "CHANGE_ME_TO_A_RANDOM_SECRET"
issuer: "https://accounts.google.com"
client_id: "<YOUR_CLIENT_ID>"
client_secret: "<YOUR_CLIENT_SECRET>"
scopes: ["openid", "email"]In forward mode, the bearer token is automatically forwarded to ClickHouse and static credentials are cleared. No additional flags needed.
Use this when ClickHouse has no OAuth support. The MCP server itself authenticates users via the upstream IdP, mints its own tokens, and connects to ClickHouse with static credentials.
- An MCP client authenticates with an Identity Provider (IdP) via browser login
- The MCP server validates the upstream identity (email domain, hosted domain, email verification)
- The MCP server mints its own signed access and refresh tokens
- The MCP server connects to ClickHouse with its statically configured credentials
This mode works even when ClickHouse has no native OAuth support.
┌────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐
│ MCP │─────>│ IdP │ │ MCP │ │ ClickHouse │
│ Client │<─────│(Keycloak,│ │ Server │ │ │
│ │ │ Azure AD,│ │ │ │ │
│ │ │ Google) │ │ │ │ │
│ │ └──────────┘ │ │ │ │
│ │ │ │ │ │
│ │──Browser login────────>│──Verify──>│ │ │
│ │<─────────MCP token─────│ identity │ │ │
│ │ │ │ │ │
│ │──MCP token────────────>│ │ │ │
│ │ │─Static──>│ │ │
│ │ │ creds │─────>│ Authn via │
│ │<───────────────────────│<─────────│<─────│ config user│
│ │ query results │ │ │ │
└────────┘ └──────────┘ └────────────┘
clickhouse:
host: "clickhouse.example.com"
port: 9000
protocol: tcp
username: "default"
password: "<CLICKHOUSE_PASSWORD>"
server:
oauth:
enabled: true
mode: "gating"
gating_secret_key: "CHANGE_ME_TO_A_RANDOM_SECRET"
issuer: "https://accounts.google.com"
public_auth_server_url: "https://mcp.example.com"
client_id: "<YOUR_CLIENT_ID>"
client_secret: "<YOUR_CLIENT_SECRET>"
scopes: ["openid", "email"]
allowed_email_domains: ["example.com"]- ClickHouse protocol: Forward mode requires
http. Gating mode works with bothhttpand nativetcp. - ClickHouse version: Forward mode requires Altinity Antalya build 25.8+ (or any build that supports
token_processors). Gating mode works with any ClickHouse version. - Identity Provider: Any OAuth 2.0 / OIDC-compliant provider (Keycloak, Azure AD, Google, AWS Cognito, etc.)
gating_secret_key: Required in both modes. Protects stateless client registration, authorization codes, and (in gating mode) refresh tokens.- Frontend / reverse proxy: If published behind a proxy, configure explicit
public_resource_urlandpublic_auth_server_url. See Frontend / Reverse Proxy Requirements.
OAuth-capable MCP clients (e.g., Claude Desktop, Codex) discover authentication automatically:
- Client fetches
/.well-known/oauth-protected-resourcefrom the MCP endpoint - Response points to the authorization server URL
- Client fetches
/.well-known/oauth-authorization-serverfor endpoint metadata - Client dynamically registers via the registration endpoint (PKCE, public client)
- Client initiates authorization code flow with S256 PKCE
- After login, client exchanges the code for access + refresh tokens
- Client uses the access token for MCP requests and refreshes silently when it expires
In gating mode, the token endpoint returns a refresh_token alongside the access_token. Clients can exchange it via grant_type=refresh_token to get a new access token without re-authorizing through the browser.
- TTL: Controlled by
refresh_token_ttl_seconds(default: 30 days) - Rotation: Each refresh returns a new refresh token (the old one remains valid until expiry)
- Stateless: Refresh tokens are encrypted JWE blobs with no server-side state. There is no revocation or reuse detection.
- Forward mode: Does not issue refresh tokens. The upstream IdP controls token lifecycle.
Deployments that require token revocation should use forward mode with an IdP that supports it.
Gating mode can restrict access based on verified identity claims from the upstream IdP:
| Option | Description |
|---|---|
allowed_email_domains |
Only allow users with email addresses in these domains (e.g., ["example.com"]) |
allowed_hosted_domains |
Only allow users from these Google Workspace / hosted domains (checks the hd claim) |
require_email_verified |
Reject users whose email_verified claim is false |
These checks run on every token mint and refresh. Identity claims come from the upstream IdP's signed id_token or userinfo response and cannot be forged by the client.
server:
oauth:
allowed_email_domains: ["altinity.com", "example.com"]
allowed_hosted_domains: ["altinity.com"]
require_email_verified: trueserver:
oauth:
# Enable OAuth 2.0 authentication
enabled: false
# OAuth operating mode:
# - "forward": pass bearer tokens through to ClickHouse for validation
# - "gating": validate upstream identity and mint local MCP tokens
mode: "forward"
# Symmetric secret for stateless OAuth artifacts (client registration,
# authorization codes, refresh tokens). Required whenever OAuth is enabled.
gating_secret_key: ""
# Upstream OAuth/OIDC issuer URL (used for discovery and validation)
issuer: ""
# URL to fetch JWKS for token validation (discovered from issuer if empty)
jwks_url: ""
# Expected audience claim in incoming tokens
audience: ""
# Upstream OAuth client credentials (for browser-login facade)
client_id: ""
client_secret: ""
# Upstream OAuth endpoint URLs (discovered from issuer if empty)
token_url: ""
auth_url: ""
userinfo_url: ""
# OAuth scopes to request from upstream IdP
scopes: ["openid", "profile", "email"]
# Scopes required in incoming tokens (gating mode only)
required_scopes: []
# Allowed upstream IdP issuers for identity tokens during callback exchange
upstream_issuer_allowlist: []
# Identity policy (gating mode)
allowed_email_domains: []
allowed_hosted_domains: []
require_email_verified: false
# Token/code lifetimes
auth_code_ttl_seconds: 300 # 5 minutes
access_token_ttl_seconds: 3600 # 1 hour
refresh_token_ttl_seconds: 2592000 # 30 days (gating mode only)
# Header name for forwarding. Default "Authorization" sends "Bearer {token}".
# Set to a custom name to send the raw token without "Bearer " prefix.
clickhouse_header_name: ""
# Map token claims to ClickHouse HTTP headers (gating mode with claims)
claims_to_headers:
sub: "X-ClickHouse-User"
email: "X-ClickHouse-Email"
# Externally visible URLs (required behind a reverse proxy)
public_resource_url: ""
public_auth_server_url: ""
# Endpoint paths (defaults shown; override for custom proxy layouts)
protected_resource_metadata_path: "/.well-known/oauth-protected-resource"
authorization_server_metadata_path: "/.well-known/oauth-authorization-server"
openid_configuration_path: "/.well-known/openid-configuration"
registration_path: "/register"
authorization_path: "/authorize"
callback_path: "/callback"
token_path: "/token"| Option | Description |
|---|---|
mode |
forward passes tokens to ClickHouse for validation; gating validates upstream identity and mints local tokens |
gating_secret_key |
Symmetric secret for all stateless OAuth artifacts. Required whenever OAuth is enabled |
issuer |
Upstream IdP issuer URL for OIDC discovery and token validation |
public_resource_url |
Externally visible MCP endpoint URL. Required behind a reverse proxy |
public_auth_server_url |
Externally visible OAuth authorization server URL. Required behind a reverse proxy |
refresh_token_ttl_seconds |
Lifetime of stateless refresh tokens in gating mode (default 30 days) |
For direct bearer-token use, a plain reverse proxy is usually enough.
For browser-based MCP login, the frontend must expose two public URL spaces:
- the protected resource, for example
https://PUBLIC_HOST.example.com/http - the OAuth authorization server, for example
https://PUBLIC_HOST.example.com/oauth
The proxy must:
- Forward
HostandAuthorizationheaders unchanged - Disable response buffering for MCP streaming
- Disable request buffering for long-lived POSTs
- Keep long read/send timeouts
- Not normalize or rewrite the configured callback or metadata paths
- Not rely on forwarded-prefix headers; configure the public OAuth URLs explicitly in
altinity-mcp
Example nginx configuration:
location ^~ /http {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
proxy_pass http://ALTINITY_MCP_UPSTREAM;
}Notes:
- Set both
public_resource_urlandpublic_auth_server_urlwhenever OAuth is published behind a proxy. - If an IdP reports
redirect_uri_mismatch, verify the public callback URL seen by the browser exactly matches the URI registered at the IdP.
server:
oauth:
enabled: true
mode: "forward"
gating_secret_key: "CHANGE_ME_TO_A_RANDOM_SECRET"
issuer: "https://accounts.google.com"
audience: "https://PUBLIC_HOST.example.com/http"
public_resource_url: "https://PUBLIC_HOST.example.com/http"
public_auth_server_url: "https://PUBLIC_HOST.example.com/oauth"
client_id: "YOUR_GOOGLE_WEB_CLIENT.apps.googleusercontent.com"
client_secret: "YOUR_GOOGLE_CLIENT_SECRET"
scopes: ["openid", "email"]In forward mode, the bearer token is automatically forwarded to ClickHouse and static credentials are cleared. No additional flags needed.
ClickHouse must be configured with token_processors and a user_directories section that maps tokens to user identities and roles.
<clickhouse>
<token_processors>
<my_oidc_provider>
<type>openid</type>
<configuration_endpoint>https://your-idp.example.com/.well-known/openid-configuration</configuration_endpoint>
<token_cache_lifetime>60</token_cache_lifetime>
</my_oidc_provider>
</token_processors>
<user_directories>
<token>
<processor>my_oidc_provider</processor>
<common_roles>
<default_role />
</common_roles>
</token>
</user_directories>
</clickhouse>Alternatively, you can specify the OIDC endpoints explicitly:
<clickhouse>
<token_processors>
<my_oidc_provider>
<type>OpenID</type>
<userinfo_endpoint>https://your-idp.example.com/userinfo</userinfo_endpoint>
<jwks_uri>https://your-idp.example.com/certs</jwks_uri>
<token_introspection_endpoint>https://your-idp.example.com/token/introspect</token_introspection_endpoint>
<token_cache_lifetime>60</token_cache_lifetime>
</my_oidc_provider>
</token_processors>
<user_directories>
<token>
<processor>my_oidc_provider</processor>
<common_roles>
<default_role />
</common_roles>
</token>
</user_directories>
</clickhouse>Azure AD has a dedicated azure type that requires no explicit endpoint configuration:
<clickhouse>
<token_processors>
<azure_ad>
<type>azure</type>
<token_cache_lifetime>60</token_cache_lifetime>
</azure_ad>
</token_processors>
<user_directories>
<token>
<processor>azure_ad</processor>
<common_roles>
<default_role />
</common_roles>
</token>
</user_directories>
</clickhouse>You must create the roles referenced in common_roles before users can authenticate:
CREATE ROLE OR REPLACE default_role;
GRANT SELECT ON default.* TO default_role;In the Keycloak admin console:
- Create a realm (e.g.,
mcp) - Create a client with:
- Client ID:
clickhouse-mcp - Client Protocol:
openid-connect - Access Type:
confidential(orpublicfor PKCE) - Valid Redirect URIs: your MCP server URL
- Client ID:
- Enable "Standard Flow Enabled" and "Direct Access Grants Enabled"
- Create groups for role mapping (e.g.,
clickhouse-users) - Create users and assign them to groups
- Configure group membership mapper in the client to include groups in tokens
server:
oauth:
enabled: true
mode: "forward"
gating_secret_key: "CHANGE_ME_TO_A_RANDOM_SECRET"
issuer: "http://keycloak:8080/realms/mcp"
audience: "clickhouse-mcp"
client_id: "clickhouse-mcp"
client_secret: "<KEYCLOAK_CLIENT_SECRET>"
scopes: ["openid", "email"]<token_processors>
<keycloak>
<type>OpenID</type>
<userinfo_endpoint>http://keycloak:8080/realms/mcp/protocol/openid-connect/userinfo</userinfo_endpoint>
<jwks_uri>http://keycloak:8080/realms/mcp/protocol/openid-connect/certs</jwks_uri>
<token_cache_lifetime>60</token_cache_lifetime>
</keycloak>
</token_processors>See also: zvonand/grafana-oauth for a complete working example with Keycloak and ClickHouse.
In the Azure Portal:
- Go to Microsoft Entra ID > App registrations > New registration
- Set a name (e.g., "ClickHouse MCP")
- Select the appropriate supported account types
- Add a redirect URI if using authorization code flow
- Go to Certificates & secrets > New client secret
- Copy the secret value (shown only once)
- Add the
openid,profile, andemailpermissions under API permissions
- Tenant ID: found in the Overview tab
- Application (Client) ID: found in the Overview tab
- Token URL:
https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token - Auth URL:
https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize - OIDC Discovery:
https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration
server:
oauth:
enabled: true
mode: "forward"
gating_secret_key: "CHANGE_ME_TO_A_RANDOM_SECRET"
issuer: "https://login.microsoftonline.com/<TENANT_ID>/v2.0"
audience: "<APPLICATION_CLIENT_ID>"
client_id: "<APPLICATION_CLIENT_ID>"
client_secret: "<APPLICATION_CLIENT_SECRET>"
token_url: "https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token"
auth_url: "https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/authorize"
scopes: ["openid", "profile", "email"]Azure AD uses the dedicated azure token processor type:
<token_processors>
<azure_ad>
<type>azure</type>
<token_cache_lifetime>60</token_cache_lifetime>
</azure_ad>
</token_processors>See also: zvonand/grafana-oauth/azure for a complete working example with Azure AD and ClickHouse.
References:
In the Google Cloud Console:
- Go to APIs & Services > Credentials > Create Credentials > OAuth client ID
- Select Web application as the application type
- Set authorized redirect URIs
- Copy the Client ID and Client Secret
server:
oauth:
enabled: true
mode: "forward"
gating_secret_key: "CHANGE_ME_TO_A_RANDOM_SECRET"
issuer: "https://accounts.google.com"
audience: "<GOOGLE_CLIENT_ID>.apps.googleusercontent.com"
client_id: "<GOOGLE_CLIENT_ID>.apps.googleusercontent.com"
client_secret: "<GOOGLE_CLIENT_SECRET>"
token_url: "https://oauth2.googleapis.com/token"
auth_url: "https://accounts.google.com/o/oauth2/v2/auth"
scopes: ["openid", "profile", "email"]Google uses the standard openid token processor type:
<token_processors>
<google>
<type>openid</type>
<configuration_endpoint>https://accounts.google.com/.well-known/openid-configuration</configuration_endpoint>
<token_cache_lifetime>60</token_cache_lifetime>
<username_claim>email</username_claim>
</google>
</token_processors>Default for username_claim is sub, that means IdP users will be shown in clickhouse (processlist, query_log, etc) as numerical ids. To see emails, set <username_claim>email</username_claim>
References:
- Google - OpenID Connect
- Google - Using OAuth 2.0 to Access Google APIs
- Setting up OAuth 2.0 in Google Cloud Console
In the AWS Console:
- Create a new user pool
- Configure sign-in options (email, username)
- Set password policies
- Add an app client with OAuth 2.0 settings enabled
- Under App integration > App clients, create a new app client
- Enable the OAuth 2.0 grant types you need (Authorization Code)
- Set the allowed callback URLs
- Select the OAuth scopes:
openid,profile,email
- User Pool ID: found in the General settings tab
- Region: the AWS region where the user pool is created
- Issuer URL:
https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID} - Token URL:
https://{DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token - Auth URL:
https://{DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/authorize - OIDC Discovery:
https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration
server:
oauth:
enabled: true
mode: "forward"
gating_secret_key: "CHANGE_ME_TO_A_RANDOM_SECRET"
issuer: "https://cognito-idp.<REGION>.amazonaws.com/<USER_POOL_ID>"
audience: "<APP_CLIENT_ID>"
client_id: "<APP_CLIENT_ID>"
client_secret: "<APP_CLIENT_SECRET>"
token_url: "https://<DOMAIN>.auth.<REGION>.amazoncognito.com/oauth2/token"
auth_url: "https://<DOMAIN>.auth.<REGION>.amazoncognito.com/oauth2/authorize"
scopes: ["openid", "profile", "email"]AWS Cognito uses the standard openid token processor type:
<token_processors>
<cognito>
<type>openid</type>
<configuration_endpoint>https://cognito-idp.<REGION>.amazonaws.com/<USER_POOL_ID>/.well-known/openid-configuration</configuration_endpoint>
<token_cache_lifetime>60</token_cache_lifetime>
</cognito>
</token_processors>References:
- Amazon Cognito - Using OIDC identity providers
- Amazon Cognito - How to use OAuth 2.0
- Amazon Cognito - Identity provider endpoints
The Helm chart supports all OAuth configuration options under config.server.oauth:
helm install altinity-mcp ./helm/altinity-mcp \
-f helm/altinity-mcp/values_examples/mcp-oauth-keycloak.yamlExample values files are provided for each provider:
values_examples/mcp-oauth-keycloak.yaml- Keycloak / generic OIDCvalues_examples/mcp-oauth-azure.yaml- Azure AD (Microsoft Entra ID)values_examples/mcp-oauth-google.yaml- Google Cloud Identity
gating_secret_keyprotects all stateless OAuth artifacts (client registrations, authorization codes, refresh tokens). Treat it like a signing key. Rotate it to invalidate all outstanding registrations and tokens.- Forward mode does not validate tokens locally. It checks only that a bearer token is present, then forwards it to ClickHouse. Token validation is ClickHouse's responsibility via
token_processors. - Gating-mode refresh tokens are stateless. There is no server-side state, so individual tokens cannot be revoked. The only way to invalidate all tokens is to rotate
gating_secret_key. Userefresh_token_ttl_secondsto limit exposure. - Opaque bearer tokens are not supported. Inbound OAuth validation on MCP/OpenAPI endpoints requires a signed JWT that can be validated via JWKS. The
userinfoendpoint is used only during browser-login identity lookup, not for runtime token validation. - Token preference during browser login. When both
id_tokenandaccess_tokenare returned by the upstream provider,altinity-mcpprefersid_tokenas the MCP bearer token and falls back toaccess_tokenonly when noid_tokenis available.
Your ClickHouse build does not support token_processors. You need the Altinity Antalya build 25.8+ or a compatible ClickHouse version.
Ensure the issuer in your MCP config matches exactly what your IdP puts in the iss claim. Common issues:
- Trailing slash mismatch (
https://accounts.google.comvshttps://accounts.google.com/) - Missing
/v2.0suffix for Azure AD
In gating mode, also ensure public_auth_server_url is set when issuer is configured. The server mints tokens with public_auth_server_url as the issuer but validates against issuer if public_auth_server_url is empty.
Create the roles referenced in common_roles and grant them the necessary permissions:
CREATE ROLE OR REPLACE default_role;
GRANT SELECT ON *.* TO default_role;