Skip to main content
  1. Posts/

Snowflake OAuth & PAT Toolkit: JWT-to-PAT Exchange for MCP Authentication

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:

  1. Username/password — cannot be used programmatically in many setups, requires MFA, not suitable for automation.
  2. Key-pair authentication — works for service accounts, but requires managing RSA keys on every client.
  3. OAuth browser flow — great for humans, unusable for headless agents.
  4. 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:

SituationActionSQL
No PAT existsCreateALTER USER ADD PAT MCP_PAT DAYS_TO_EXPIRY=1
PAT exists, still validRotateALTER USER ROTATE PAT MCP_PAT EXPIRE_ROTATED_TOKEN_AFTER_HOURS=0
Expired rotated tokens remainCleanupALTER 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:

  1. The JWT is sent to Snowflake’s login endpoint via External OAuth
  2. Snowflake validates the JWT and creates a session
  3. The session runs ALTER USER ADD PAT MCP_PAT DAYS_TO_EXPIRY=1 ROLE_RESTRICTION='ANALYST_ROLE'
  4. If MCP_PAT already exists, it runs ALTER USER ROTATE PAT MCP_PAT instead
  5. The new PAT secret is returned
  6. 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()      # bytes

Why 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 forever

What the Toolkit Tests
#

pixi run python oauth_test.py

The oauth_test.py script exercises the full cycle:

  1. Authorization code flow — opens a browser for the initial OAuth consent
  2. Refresh token caching — stores the refresh token locally for reuse across script runs
  3. Refresh token rotation — exchanges the refresh token, verifies a new one is returned
  4. Token comparison — confirms the new access token and refresh token differ from the old ones
  5. 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
#

PatternBest ForUser InteractionToken Lifetime
OAuth + Continuous RefreshLong-running apps, dashboards, ETLOnce (initial browser auth)Indefinite (refresh keeps it alive)
JWT → PAT ExchangeMCP servers, AI agents, headless automationNever (JWT from IdP)Short (1-day PAT, rotated)
Static PATQuick testing, dev environmentsOnce (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
#

ErrorCauseFix
099420: OAuth sessions require role restrictionPATs from OAuth must be role-scopedThe toolkit auto-detects the current role and applies it
099413: PATs cannot modify PATs for same userUsing a PAT to create/rotate another PATUse JWT/OAuth authentication for all PAT operations
credentials.json not foundMissing configCopy credentials.json.example to credentials.json
PAT not working for MCPMissing headerInclude 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.py

Key Takeaways
#

  1. 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.

  2. One PAT per user, standardised name. Prevents accumulation, simplifies rotation, makes cleanup deterministic.

  3. PATs cannot mint PATs. This is a security feature. All lifecycle operations require JWT/OAuth — a leaked PAT cannot escalate.

  4. Role restriction is mandatory for OAuth-derived PATs. The PAT can only operate within the role specified at creation. Least privilege by design.

  5. Rotation kills the old secret immediately. EXPIRE_ROTATED_TOKEN_AFTER_HOURS=0 means 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.

Kevin Keller
Author
Kevin Keller
Personal blog about AI, Observability & Data Sovereignty. Snowflake-related articles explore the art of the possible and are not official Snowflake solutions or endorsed by Snowflake unless explicitly stated. Opinions are my own. Content is meant as educational inspiration, not production guidance.
Share this article

Related