Code: github.com/sfc-gh-kkeller/snowflake-oauth-pat-toolkit — Python scripts for PAT lifecycle management, JWT generation, and OAuth testing.
This is an educational proof of technology demonstrating the authentication patterns discussed in Governing AI Inference in the Data Cloud.
The Problem#
You have an AI agent, an MCP server, or a service that needs to authenticate to Snowflake’s REST API. The options:
- Username/password — cannot be used programmatically in many setups, requires MFA, not suitable for automation.
- Key-pair authentication — works for service accounts, but requires managing RSA keys on every client.
- OAuth browser flow — great for humans, unusable for headless agents.
- Programmatic Access Tokens (PATs) — short-lived Bearer tokens, role-restricted, rotatable. Perfect for MCP. But how do you get one without a browser?
The answer: exchange a JWT from your identity provider for a Snowflake session, then use that session to create a short-lived PAT. The PAT becomes the Bearer token for MCP authentication. When it expires, rotate it — no browser, no MFA, no static credentials.
This toolkit implements that full flow.
Architecture#
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User / App │────▶│ OIDC IdP │────▶│ JWT Token │
│ │ │ (Entra ID, │ │ │
│ │ │ Okta, etc) │ │ │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌────────────────────────────┘
│ External OAuth (JWT → Snowflake session)
▼
┌──────────────┐
│ Snowflake │
│ │
│ ALTER USER │
│ ADD PAT │──── creates short-lived PAT (1 day)
│ ROTATE PAT │──── rotates existing PAT
│ │
└──────┬───────┘
│
│ PAT secret
▼
┌──────────────┐
│ MCP Server │
│ │
│ Bearer: │
│ <PAT_SECRET>│
│ │
│ Header: │
│ X-Snowflake-│
│ Authorization│
│ -Token-Type:│
│ PROGRAMMATIC│
│ _ACCESS_ │
│ TOKEN │
└──────────────┘The key insight: PATs cannot create or modify other PATs for the same user. All PAT lifecycle operations (create, rotate, remove) must use JWT/OAuth authentication. This is a security constraint by design — it prevents a leaked PAT from being used to mint more PATs.
The PAT Lifecycle: One PAT Per User#
The toolkit uses a standardised PAT name (MCP_PAT by default) per user. This prevents PAT accumulation (Snowflake allows a maximum of 15 PATs per user) and simplifies lifecycle management:
| Situation | Action | SQL |
|---|---|---|
| No PAT exists | Create | ALTER USER ADD PAT MCP_PAT DAYS_TO_EXPIRY=1 |
| PAT exists, still valid | Rotate | ALTER USER ROTATE PAT MCP_PAT EXPIRE_ROTATED_TOKEN_AFTER_HOURS=0 |
| Expired rotated tokens remain | Cleanup | ALTER USER REMOVE PAT <old_pat> |
The flow is idempotent: call exchange_token() any time, and you get back a valid PAT secret. If the PAT already exists, it gets rotated. If expired tokens are piling up, they get cleaned automatically.
How It Works#
Step 1: Generate or Obtain a JWT#
In production, this comes from your OIDC identity provider (Entra ID, Okta, Ping, etc.) via External OAuth. For testing, the toolkit includes a JWT generator that simulates an IdP:
from pat_mcp_poc.token_generator import generate_access_token
# Simulates what your IdP would return
jwt_token = generate_access_token(
username="kevin@example.com",
role="ANALYST_ROLE",
expires_minutes=60
)The JWT is signed with your RSA private key. Snowflake validates it against the public key registered in your External OAuth security integration.
Step 2: Exchange JWT for a PAT#
from pat_mcp_poc.pat_manager import exchange_token
# Authenticate to Snowflake with the JWT,
# then create or rotate a PAT
pat_secret = exchange_token(jwt_token)Under the hood:
- The JWT is sent to Snowflake’s login endpoint via External OAuth
- Snowflake validates the JWT and creates a session
- The session runs
ALTER USER ADD PAT MCP_PAT DAYS_TO_EXPIRY=1 ROLE_RESTRICTION='ANALYST_ROLE' - If
MCP_PATalready exists, it runsALTER USER ROTATE PAT MCP_PATinstead - The new PAT secret is returned
- Expired rotated tokens are cleaned up automatically
Step 3: Use the PAT for MCP Authentication#
import requests
headers = {
"Authorization": f"Bearer {pat_secret}",
"X-Snowflake-Authorization-Token-Type": "PROGRAMMATIC_ACCESS_TOKEN",
}
# Authenticate to any Snowflake REST endpoint or MCP server
response = requests.get(
"https://myaccount.snowflakecomputing.com/api/v2/cortex/...",
headers=headers
)The PAT is role-restricted — it can only operate within the role specified at creation time. It expires after 1 day (configurable). No browser, no MFA, no interactive flow.
Step 4: Rotate on Next Exchange#
When the PAT approaches expiry, call exchange_token() again. The existing PAT gets rotated — a new secret is issued, the old one is invalidated immediately (EXPIRE_ROTATED_TOKEN_AFTER_HOURS=0). The PAT name stays the same.
Configuration#
All secrets live in a single credentials.json (git-ignored):
{
"snowflake": {
"account": "myorg-myaccount",
"base_url": "https://myorg-myaccount.snowflakecomputing.com",
"login_name": "kevin@example.com",
"default_role": "ANALYST_ROLE"
},
"oauth_external": {
"issuer": "https://login.microsoftonline.com/tenant-id/v2.0",
"audience": "https://myorg-myaccount.snowflakecomputing.com"
},
"pat": {
"pat_name": "MCP_PAT",
"days_to_expiry": 1
},
"rsa_keys": {
"private_key": "-----BEGIN RSA PRIVATE KEY-----\n...",
"public_key": "-----BEGIN PUBLIC KEY-----\n..."
}
}A centralised config.py module provides typed access:
from config import get_account, get_login_name, get_pat_name, get_rsa_private_key
account = get_account() # "myorg-myaccount"
pat_name = get_pat_name() # "MCP_PAT"
key = get_rsa_private_key() # bytesWhy This Matters for MCP#
MCP (Model Context Protocol) servers need a Bearer token to authenticate requests from AI agents. The question is: where does that token come from, and how is it managed?
The naive approach is a static PAT — create one, paste it in the agent config, forget about it. This works until the PAT expires, or someone commits it to git, or an admin needs to audit which tokens are active.
The toolkit approach treats PATs as ephemeral, rotatable credentials derived from your organisation’s existing identity infrastructure:
- Identity flows through your IdP. The JWT comes from Entra ID, Okta, or whatever you already use. No new identity system.
- PATs are short-lived. 1-day expiry by default. A leaked PAT is useless tomorrow.
- Rotation is automatic. Call
exchange_token()on a schedule. The old secret dies immediately. - Cleanup is built in. Expired rotated tokens are removed to prevent accumulation.
- Audit trail is complete. Every PAT creation and rotation is logged in Snowflake’s
ACCOUNT_USAGE.QUERY_HISTORY.
This is the Pattern 3 (JWT → PAT Exchange) described in Governing AI Inference in the Data Cloud.
OAuth Token Testing: Continuous Refresh Pattern#
The toolkit also includes a separate flow for testing Snowflake’s internal OAuth with continuous refresh token rotation. This is the pattern you would use for long-running applications (dashboards, ETL services, monitoring tools) that need to maintain a Snowflake session indefinitely without user interaction.
The Problem with OAuth Tokens#
OAuth access tokens expire. Typically after 10 minutes for Snowflake’s internal OAuth. When they expire, the application needs a new one. The user is not there to re-authenticate — the app is running unattended at 3 AM.
The solution is a refresh token. When the initial OAuth authorization happens (browser-based, one time), Snowflake returns both an access token and a refresh token. The access token is short-lived. The refresh token is long-lived and can be exchanged for a new access token without user interaction.
Refresh Token Rotation#
Snowflake rotates refresh tokens on every use. When you exchange a refresh token for a new access token, you also get a new refresh token. The old refresh token is invalidated. This is a security feature — a stolen refresh token can only be used once before the legitimate client detects the theft (its next refresh fails).
The flow:
┌──────────┐ ┌───────────────┐ ┌──────────────┐
│ User │────▶│ Browser Auth │────▶│ Access Token │ (short-lived)
│ (once) │ │ (OAuth PKCE) │ │ + Refresh │ (long-lived)
└──────────┘ └───────────────┘ └──────┬───────┘
│
┌─────────────────────────────────────┘
│ Every 10 minutes (automatic, no user):
▼
┌──────────────┐ ┌───────────────────────┐
│ Old Refresh │────▶│ New Access Token │
│ Token │ │ + New Refresh Token │
└──────────────┘ │ (old refresh dies) │
└───────────────────────┘
│
└──── repeat foreverWhat the Toolkit Tests#
pixi run python oauth_test.pyThe oauth_test.py script exercises the full cycle:
- Authorization code flow — opens a browser for the initial OAuth consent
- Refresh token caching — stores the refresh token locally for reuse across script runs
- Refresh token rotation — exchanges the refresh token, verifies a new one is returned
- Token comparison — confirms the new access token and refresh token differ from the old ones
- Validation — verifies the new tokens actually work against Snowflake’s API
This is useful for validating that your Snowflake OAuth security integration is configured correctly — especially the refresh token lifetime, rotation behaviour, and role restrictions — before building it into a production application.
When to Use Which Pattern#
| Pattern | Best For | User Interaction | Token Lifetime |
|---|---|---|---|
| OAuth + Continuous Refresh | Long-running apps, dashboards, ETL | Once (initial browser auth) | Indefinite (refresh keeps it alive) |
| JWT → PAT Exchange | MCP servers, AI agents, headless automation | Never (JWT from IdP) | Short (1-day PAT, rotated) |
| Static PAT | Quick testing, dev environments | Once (create in UI) | Fixed (days to expiry) |
The OAuth refresh pattern is for applications that start with a human present and then run unattended. The JWT → PAT pattern is for services that never have a human — the identity comes from the infrastructure (IdP, workload identity, service account).
Common Errors and Fixes#
| Error | Cause | Fix |
|---|---|---|
099420: OAuth sessions require role restriction | PATs from OAuth must be role-scoped | The toolkit auto-detects the current role and applies it |
099413: PATs cannot modify PATs for same user | Using a PAT to create/rotate another PAT | Use JWT/OAuth authentication for all PAT operations |
credentials.json not found | Missing config | Copy credentials.json.example to credentials.json |
| PAT not working for MCP | Missing header | Include X-Snowflake-Authorization-Token-Type: PROGRAMMATIC_ACCESS_TOKEN |
Quick Start#
# Clone
git clone https://github.com/sfc-gh-kkeller/snowflake-oauth-pat-toolkit.git
cd snowflake-oauth-pat-toolkit/refresh_token
# Configure
cp credentials.json.example credentials.json
# Edit credentials.json with your account details and RSA keys
# Install
pixi install
# Run the interactive demo
pixi run python pat_mcp_poc/pat_mcp_poc.pyKey Takeaways#
JWT → PAT exchange is the production pattern for MCP auth. Static PATs are a shortcut. Exchanging from your IdP’s JWT gives you short-lived, auditable, rotatable credentials.
One PAT per user, standardised name. Prevents accumulation, simplifies rotation, makes cleanup deterministic.
PATs cannot mint PATs. This is a security feature. All lifecycle operations require JWT/OAuth — a leaked PAT cannot escalate.
Role restriction is mandatory for OAuth-derived PATs. The PAT can only operate within the role specified at creation. Least privilege by design.
Rotation kills the old secret immediately.
EXPIRE_ROTATED_TOKEN_AFTER_HOURS=0means a rotated PAT secret is dead on arrival. No grace period, no overlap window.
This toolkit implements the token exchange patterns from Governing AI Inference in the Data Cloud. See also Cortex Proxy which uses PATs for authenticating AI coding agents to Snowflake Cortex.
