Skip to main content
  1. Posts/

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

Recommended path: External OAuth tokens work directly with Snowflake’s managed MCP servers — no PAT exchange needed. External OAuth is the simpler and recommended approach.

Why this article is still valuable: The JWT-to-PAT exchange pattern below is a concrete example of credential vending for AI agents — the broader problem of how a human user’s identity gets transformed into a short-lived, scoped credential that an agent can use on their behalf. This is the same pattern that platforms like Immuta implement as just-in-time role vending, and that the Cloud Security Alliance’s delegation chain framework requires at every hop. Understanding the mechanics — token exchange, scope attenuation, lifecycle management, audit trail — is relevant regardless of which specific protocol you use.

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 exploring the credential vending patterns discussed in Governing AI Inference in the Data Cloud.


The Core Problem: Passing User Identity Through AI Agents
#

When you build an AI agent — whether it’s a chat interface, a TUI tool, a web app, or an API service — that agent needs to talk to Snowflake on behalf of a specific user. The fundamental challenge: how does the human user’s identity flow through the agent to Snowflake?

┌──────────┐     ┌──────────────────┐     ┌──────────────┐
│  Human   │────►│  Agent / App     │────►│  Snowflake   │
│  User    │     │  (TUI, Web UI,   │     │              │
│          │     │   API, MCP)      │     │  "Who is     │
│  Has an  │     │                  │     │   calling?"  │
│  identity│     │  Needs to pass   │     │              │
│  (SSO)   │     │  that identity   │     │  Must map to │
│          │     │  through         │     │  a Snowflake │
│          │     │                  │     │  user + role │
└──────────┘     └──────────────────┘     └──────────────┘

This matters because Snowflake’s access control, row-level security, masking policies, and audit trail all depend on who is executing the query. If the agent connects with a shared service account, you lose all of that. Every query looks like it came from the same user, and you can’t enforce per-user data governance.

The goal: the agent executes SQL as the actual human user, with their roles and permissions, even though the user never directly logs into Snowflake.

Why This Is Hard
#

Snowflake offers several authentication methods, but none of them are straightforward for agent identity passthrough:

MethodWorks for Agents?Identity Passthrough?Problem
Username/passwordPartiallyYes (user’s own)Requires MFA, can’t be automated safely, password management nightmare
Key-pair (RSA)YesNo — tied to one userEvery user needs a key pair deployed to every agent. Doesn’t scale.
OAuth browser flowNoYesRequires a browser. Agents are headless.
OAuth refresh tokensPartiallyYesSnowflake’s internal OAuth rotates refresh tokens — each use invalidates the previous one. Hard to manage across distributed agents.
Programmatic Access Tokens (PATs)YesYesShort-lived, role-scoped, rotatable. But how do you get one without a browser?

The answer is a token exchange: the user authenticates once with their identity provider (Entra ID, Okta, etc.), gets a JWT, and the agent exchanges that JWT for a short-lived Snowflake PAT. The PAT carries the user’s identity and can be used as a Bearer token for Snowflake’s REST API — including MCP server authentication.

This toolkit implements that full flow.

What This Toolkit Demonstrates
#

  1. JWT → Snowflake session — Exchange an OIDC JWT for a Snowflake session via External OAuth
  2. Session → PAT — Use the session to create a short-lived PAT scoped to a specific role
  3. PAT → MCP — Use the PAT as a Bearer token for MCP server authentication
  4. PAT rotation — Automatically rotate the PAT before expiry, no user interaction needed
  5. OAuth refresh token testing — Explore Snowflake’s internal OAuth flow with continuous token rotation

The result: the agent authenticates to Snowflake as the actual user, with their roles and permissions, using a short-lived credential that auto-rotates. No shared service account. No static keys. Full audit trail.


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