JSON Web Tokens are everywhere. Every OAuth flow, every API authentication layer, every modern SSO system uses them. But most developers treat them as opaque blobs — copy a token, paste it into a header, move on. This article breaks open the black box, then shows how Snowflake uses JWTs for External OAuth.
After reading this, try decoding and editing JWTs yourself in the JWT Playground.
What Is a JWT?#
A JWT is a compact, URL-safe string that carries claims (statements about a user or entity) in a way that can be cryptographically verified. It’s defined in RFC 7519.
A JWT looks like this:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyOjQyIn0.signature...Three parts separated by dots: Header, Payload, and Signature.
┌────────────────┐ ┌────────────────────────┐ ┌──────────────────┐
│ HEADER │ . │ PAYLOAD │ . │ SIGNATURE │
│ │ │ │ │ │
│ Algorithm │ │ Claims about the user │ │ Proof that the │
│ Token type │ │ Issuer, expiry, etc. │ │ header+payload │
│ │ │ │ │ haven't been │
│ Base64URL │ │ Base64URL encoded │ │ tampered with │
│ encoded │ │ │ │ │
└────────────────┘ └────────────────────────┘ └──────────────────┘Anatomy: Decoding Each Part#
Header#
The header declares the algorithm and token type:
{
"alg": "RS256",
"typ": "JWT"
}Common algorithms:
- HS256 — HMAC with SHA-256. Symmetric: same secret signs and verifies.
- RS256 — RSA with SHA-256. Asymmetric: private key signs, public key verifies. This is what Snowflake uses.
- ES256 — ECDSA with P-256. Asymmetric, smaller keys than RSA.
Payload#
The payload contains claims — key-value pairs about the subject:
{
"iss": "https://auth.example.com",
"aud": "https://myapp.snowflakecomputing.com",
"sub": "kevin@example.com",
"scp": "session:role:analyst",
"iat": 1711926000,
"exp": 1711929600
}Important: the payload is encoded, not encrypted. Anyone who has the token can decode the payload. Never put secrets in the payload.
Signature#
The signature prevents tampering. For RS256:
RSA-SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
private_key
)If anyone modifies a single character in the header or payload, the signature won’t match. The verifier uses the public key to verify — they never need the private key.
Standard Claims#
| Claim | Name | Purpose | Example |
|---|---|---|---|
iss | Issuer | Who created the token | "https://auth.example.com" |
sub | Subject | Who the token is about | "kevin@example.com" |
aud | Audience | Who should accept it | "https://myaccount.snowflakecomputing.com" |
exp | Expiration | When the token expires | 1711929600 (Unix timestamp) |
iat | Issued At | When it was created | 1711926000 |
nbf | Not Before | Token not valid before | 1711926000 |
jti | JWT ID | Unique token identifier | "abc-123-def" |
exp is the most critical. Without it, a stolen token works forever.
The Token Lifecycle#
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Client │ │ Auth Server │ │ API / DB │
│ (App) │ │ (IdP) │ │ (Snowflake) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ 1. Authenticate │ │
│ (credentials/SSO) │ │
│────────────────────▶│ │
│ │ │
│ 2. JWT returned │ │
│ (signed with │ │
│ private key) │ │
│◀────────────────────│ │
│ │ │
│ 3. Connect with JWT│ │
│ Authorization: │ │
│ Bearer <JWT> │ │
│─────────────────────┼────────────────────▶│
│ │ │
│ │ 4. Verify JWT │
│ │ - Check signature │
│ │ (public key) │
│ │ - Check exp │
│ │ - Check iss, aud │
│ │ - Map user + role │
│ │ │
│ 5. Access granted │ │
│◀─────────────────────┼────────────────────│Key insight: step 4 is entirely local. The API server (or Snowflake) has the public key and verifies the token without contacting the auth server. This is why JWTs scale — no round-trip, no session store.
How Signing Works: RSA (RS256)#
Snowflake’s External OAuth uses RS256 — asymmetric RSA signing. Here’s what happens:
Token Creation (Auth Server):
- Generate an RSA key pair (private + public)
- Build the header + payload JSON
- Base64URL-encode both
- Sign with the private key:
RSA-SHA256(header.payload, private_key) - Concatenate:
header.payload.signature
Token Verification (Snowflake):
- Receive the JWT
- Split into header, payload, signature
- Recompute:
RSA-SHA256(header.payload, public_key) - Compare with the received signature
- If they match → token is authentic and untampered
The private key never leaves the auth server. Snowflake only needs the public key.
JWTs in OAuth 2.0 / OIDC#
Access Token#
Used to authorize API requests. Short-lived (minutes to hours). Contains scopes that define what the bearer can do.
ID Token#
OIDC-specific. Identifies the user (name, email, etc.). Used by the client app, not sent to APIs.
Refresh Token#
Not a JWT in most implementations — it’s an opaque string. Used to get new access tokens without re-authentication. Long-lived.
Snowflake External OAuth: JWTs in Practice#
Now let’s apply this to Snowflake. External OAuth lets you authenticate to Snowflake using JWTs issued by your own identity provider (Entra ID, Okta, Ping, or a custom server).
The Flow#
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ BI Tool / │ │ Identity │ │ Snowflake │
│ App / Agent │ │ Provider │ │ │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ 1. User logs in │ │
│ (SSO / OAuth) │ │
│────────────────────▶│ │
│ │ │
│ 2. IdP issues JWT │ │
│ with user identity │ │
│ + Snowflake role │ │
│◀────────────────────│ │
│ │ │
│ 3. Connect to Snowflake │
│ authenticator=oauth │
│ token=<JWT> │
│──────────────────────────────────────────▶│
│ │ │
│ │ 4. Snowflake: │
│ │ - Verifies sig │
│ │ (RSA public key) │
│ │ - Maps iss → integ │
│ │ - Maps name → user │
│ │ - Maps scp → role │
│ │ │
│ 5. Session created with mapped role │
│◀──────────────────────────────────────────│What the JWT Contains#
For Snowflake External OAuth, the JWT payload must include:
{
"iss": "https://login.microsoftonline.com/tenant-id/v2.0",
"aud": "https://myaccount.snowflakecomputing.com",
"scp": "session:role:ANALYST_ROLE",
"name": "KEVIN.KELLER@COMPANY.COM",
"iat": 1711926000,
"exp": 1711929600
}| Claim | Maps To | Description |
|---|---|---|
iss | Security Integration external_oauth_issuer | Must match exactly |
aud | Security Integration external_oauth_audience_list | Must match exactly |
scp | Snowflake role via external_oauth_scope_mapping_attribute | e.g., session:role:ANALYST |
name | Snowflake user via external_oauth_token_user_mapping_claim | Must match a Snowflake LOGIN_NAME |
exp | Token expiry | Snowflake rejects expired tokens |
Step 1: Generate an RSA Key Pair#
# Generate a 2048-bit RSA private key
openssl genrsa -out private_key.pem 2048
# Extract the public key
openssl rsa -in private_key.pem -pubout -out public_key.pem
# Get the public key without headers (for Snowflake config)
openssl rsa -in private_key.pem -pubout | grep -v "BEGIN\|END" | tr -d '\n'Step 2: Create the Snowflake Security Integration#
CREATE OR REPLACE SECURITY INTEGRATION external_oauth_demo
TYPE = external_oauth
ENABLED = true
EXTERNAL_OAUTH_TYPE = custom
EXTERNAL_OAUTH_ISSUER = 'https://auth.example.com'
EXTERNAL_OAUTH_AUDIENCE_LIST = ('https://myaccount.snowflakecomputing.com')
EXTERNAL_OAUTH_SCOPE_MAPPING_ATTRIBUTE = 'scp'
EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM = 'name'
EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE = 'login_name'
EXTERNAL_OAUTH_ANY_ROLE_MODE = 'ENABLE'
EXTERNAL_OAUTH_RSA_PUBLIC_KEY = 'MIIBIjANBgk...your-public-key-here...AQAB';Key configuration points:
EXTERNAL_OAUTH_ISSUERmust exactly match theissclaim in your JWTsEXTERNAL_OAUTH_AUDIENCE_LISTmust match theaudclaimEXTERNAL_OAUTH_RSA_PUBLIC_KEYis the public key (PEM format, no headers, single line) that Snowflake uses to verify signaturesEXTERNAL_OAUTH_ANY_ROLE_MODE = 'ENABLE'allows the JWT to specify any role the mapped user has access to
Step 3: Generate a JWT (Python UDF Example)#
You can generate JWTs directly inside Snowflake using a Python UDF — useful for testing and proof of concepts:
CREATE OR REPLACE FUNCTION generate_jwt_token(
user_login VARCHAR,
role_name VARCHAR,
expiry_minutes NUMBER DEFAULT 60
)
RETURNS VARCHAR
LANGUAGE PYTHON
RUNTIME_VERSION = '3.11'
PACKAGES = ('pyjwt', 'cryptography')
HANDLER = 'generate_token'
AS $$
import jwt
from datetime import datetime, timedelta
def generate_token(user_login: str, role_name: str, expiry_minutes: int) -> str:
"""Generate a signed JWT for Snowflake External OAuth testing.
In production, tokens would be issued by your identity provider
(Entra ID, Okta, etc.), not by a Snowflake UDF. This is for
educational purposes and testing only.
"""
# In a real setup, this key lives in your IdP's secure key store
# NEVER hardcode production keys in code
private_key = b"""-----BEGIN RSA PRIVATE KEY-----
... your private key here ...
-----END RSA PRIVATE KEY-----"""
now = datetime.utcnow()
payload = {
"iss": "https://auth.example.com", # Must match security integration
"aud": "https://myaccount.snowflakecomputing.com", # Must match security integration
"scp": f"session:role:{role_name}", # Maps to Snowflake role
"name": user_login.upper(), # Maps to Snowflake LOGIN_NAME
"iat": now, # Issued at
"exp": now + timedelta(minutes=expiry_minutes) # Expiration
}
return jwt.encode(payload, private_key, algorithm="RS256")
$$;Usage:
-- Generate a token for user KEVIN with role ANALYST, valid for 60 minutes
SELECT generate_jwt_token('KEVIN@COMPANY.COM', 'ANALYST', 60);
-- Generate a short-lived token for testing (1 minute)
SELECT generate_jwt_token('KEVIN@COMPANY.COM', 'ANALYST', 1);Step 4: Connect Using the JWT#
import snowflake.connector
conn = snowflake.connector.connect(
account='myaccount',
authenticator='oauth',
token='eyJhbGciOiJSUzI1NiIs...', # The JWT from step 3
warehouse='COMPUTE_WH',
database='MY_DB',
schema='PUBLIC'
)
# The session is now authenticated as the user mapped from the JWT's 'name' claim
# with the role from the 'scp' claim
cur = conn.cursor()
cur.execute("SELECT CURRENT_USER(), CURRENT_ROLE()")
print(cur.fetchone()) # ('KEVIN@COMPANY.COM', 'ANALYST')Step 5: Verify the Integration Works#
-- Check the security integration configuration
DESCRIBE SECURITY INTEGRATION external_oauth_demo;
-- View active OAuth sessions
SELECT * FROM SNOWFLAKE.ACCOUNT_USAGE.SESSIONS
WHERE AUTHENTICATION_METHOD = 'OAUTH'
ORDER BY CREATED_ON DESC
LIMIT 10;
-- Check for failed authentication attempts
SELECT * FROM SNOWFLAKE.ACCOUNT_USAGE.LOGIN_HISTORY
WHERE FIRST_AUTHENTICATION_FACTOR = 'OAUTH_ACCESS_TOKEN'
AND IS_SUCCESS = 'NO'
ORDER BY EVENT_TIMESTAMP DESC;Security Pitfalls#
1. The alg: none Attack#
An attacker sets the header’s alg to "none", removes the signature, and the server accepts it.
Fix: Always validate the algorithm server-side. Snowflake enforces RS256.
2. Weak or Exposed Keys#
Short RSA keys or keys committed to source control. Fix: Use 2048-bit+ RSA keys. Store private keys in a vault (Azure Key Vault, AWS KMS, HashiCorp Vault). Rotate regularly.
3. No Expiration#
A token without exp is valid forever.
Fix: Snowflake rejects tokens without exp. Set short expiry (15-60 minutes for access tokens).
4. Audience Confusion#
A token issued for Service A is used against Service B.
Fix: Snowflake checks aud against EXTERNAL_OAUTH_AUDIENCE_LIST. Always set a specific audience.
5. User Mapping Errors#
The name claim doesn’t match any Snowflake LOGIN_NAME.
Fix: Ensure exact case matching. Use EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE = 'login_name' and verify usernames match.
JWT vs Session Tokens#
| JWT | Session Token | |
|---|---|---|
| State | Stateless (self-contained) | Stateful (server stores session) |
| Revocation | Hard (wait for expiry) | Easy (delete session) |
| Scalability | Excellent (no shared store) | Needs shared store |
| Size | Larger (~800+ bytes) | Small (~32 bytes) |
| Inspection | Payload readable | Opaque |
Try It Yourself#
Decode, edit, and verify JWTs right in your browser — no server needed:
JWT Playground — paste any JWT, see the decoded header and payload, edit claims, and verify signatures. Everything runs locally.
Related#
- Snowflake OAuth & PAT Toolkit — JWT-to-PAT exchange for MCP authentication
- How PrivateLink and DNS Work — networking fundamentals for secure Snowflake connectivity
- Governing AI Inference — identity propagation and OAuth for AI agents
