One Snowflake account needs to execute SQL statements on another Snowflake account using the SQL API via machine-to-machine automation. This is accomplished via an External Access Integration wrapping the target account’s SQL API endpoint. How does OAuth authentication work in this cross-account pattern? What are the supported flows, how does token lifecycle work, and who does the SQL API execute as on the target account?
Summary#
Snowflake supports federated cross-account SQL API invocation through External Access Integrations combined with OAuth-based secrets. The calling account (Account A) creates objects to obtain tokens, while the target account (Account B) creates objects to accept and validate tokens. Any OAuth 2.0-compliant identity provider can issue the tokens — Entra ID (Azure AD) and GCP Service Accounts are shown here as concrete walkthroughs, but the pattern generalizes to any IdP that supports JWT/OAuth (Okta, Auth0, Ping, etc.). Two machine-to-machine OAuth flows are available — Client Credentials (IdP-mediated token exchange) and Self-Signed JWT (service account signs its own token using a private key stored as a Snowflake secret). Alternatives to OAuth (Programmatic Access Tokens, Key-Pair) are also covered, with trade-offs noted.
Architecture Overview#
┌──────────────────────────────────┐ HTTPS (SQL API) ┌──────────────────────────────────┐
│ ACCOUNT A │ ─────────────────────────────► │ ACCOUNT B │
│ (Calling) │ │ (Target) │
│ │ Authorization: Bearer <tok> │ │
│ Objects for OBTAINING tokens: │ X-Snowflake-Authorization- │ Objects for ACCEPTING tokens: │
│ ┌─────────────────────┐ │ Token-Type: OAUTH │ ┌────────────────────────────┐ │
│ │ Network Rule │ │ │ │ Security Integration │ │
│ │ (EGRESS to Acct B) │ │ │ │ TYPE = EXTERNAL_OAUTH │ │
│ └─────────────────────┘ │ │ │ (validates token, maps to │ │
│ ┌─────────────────────┐ │ │ │ Snowflake user/role) │ │
│ │ Security Integration│ │ │ └────────────────────────────┘ │
│ │ TYPE = API_AUTH │ │ │ │
│ │ (IdP client config) │ │ │ Executes SQL as mapped user │
│ └─────────────────────┘ │ │ with specified role/warehouse │
│ ┌─────────────────────┐ │ │ │
│ │ Secret │ │ │ Logged in QUERY_HISTORY and │
│ │ (token state or key)│ │ │ LOGIN_HISTORY │
│ └─────────────────────┘ │ │ │
│ ┌─────────────────────┐ │ │ │
│ │ External Access │ │ │ │
│ │ Integration │ │ │ │
│ └─────────────────────┘ │ │ │
│ ┌─────────────────────┐ │ │ │
│ │ Python UDF / SP │────────┘ │ │
│ │ (calls SQL API) │ │ │
│ └─────────────────────┘ │ │
└──────────────────────────────────┘ └──────────────────────────────────┘
│
▼
┌────────────────────┐
│ Identity Provider │
│ (Entra ID, GCP, │
│ Okta — any OAuth │
│ 2.0 IdP) │
└────────────────────┘Two Sides of the Integration#
| Account | Integration Type | Purpose |
|---|---|---|
| Account A (Calling) | TYPE = API_AUTHENTICATION | Stores IdP client credentials and endpoints used to obtain OAuth tokens |
| Account A (Calling) | Secret (OAUTH2 or GENERIC_STRING) | Holds token state (client credentials reference) or private key material for JWT signing |
| Account A (Calling) | Network Rule + External Access Integration | Enables EGRESS to Account B and binds secret to UDFs |
| Account B (Target) | TYPE = EXTERNAL_OAUTH | Accepts and validates inbound OAuth/JWT tokens, maps them to a Snowflake user and role |
A Python UDF or stored procedure in Account A references the external access integration and secret, obtains a valid token at runtime, and sends it as a Bearer token to Account B’s SQL API endpoint (https://<account_b>.snowflakecomputing.com/api/v2/statements).
Authentication Approach Ranking#
OAuth is the recommended authentication mechanism for this pattern. When OAuth is not feasible, two alternatives exist with progressively weaker security postures:
| Rank | Approach | Role Scoping | Token Expiry | Credential Type | Notes |
|---|---|---|---|---|---|
| 1 (Recommended) | OAuth (Client Credentials or Self-Signed JWT) | Yes — token can be scoped to a specific Snowflake role via claims or scope mapping | Yes — access tokens are short-lived (minutes to hours) | Dynamic | Tokens are ephemeral and automatically rotated. |
| 2 | Programmatic Access Token (PAT) | Yes — PAT can be scoped to a specific role at creation time | Yes — configurable expiry at creation | Semi-static | PATs support role scoping and have an expiry date. |
| 3 (Last Resort) | Key-Pair (RSA Private Key) | No — the private key authenticates as the user; role is selected at query time | No — the private key has no intrinsic expiry | Static | Long-lived static credential. Use only when OAuth and PAT are not viable. |
OAuth Flow Options (Machine-to-Machine)#
The examples below use Entra ID and GCP as representative IdPs. The same patterns apply to any OAuth 2.0-compliant identity provider.
Option A: Client Credentials Flow#
This flow has no interactive step and is suited for service-to-service automation. The IdP issues tokens in exchange for client credentials (client ID + client secret). No custom JWT code is required — Snowflake handles the token exchange natively. The calling UDF simply calls _snowflake.get_oauth_access_token('cred'), and Snowflake manages the full lifecycle.
- Identity Provider: Any OAuth 2.0 IdP that supports the
client_credentialsgrant (e.g., Entra ID, Okta) - How it works:
- An application registration in the IdP is configured with application permissions (not delegated)
- A security integration in Account A uses
OAUTH_GRANT = 'CLIENT_CREDENTIALS' - A secret of TYPE = OAUTH2 in Account A references the security integration with
OAUTH_SCOPES - An External OAuth security integration in Account B validates inbound tokens and maps the service principal to a Snowflake user
- At runtime,
_snowflake.get_oauth_access_token('cred')exchanges the client ID and client secret directly for an access token
- Token lifecycle: Access tokens are obtained on demand. Snowflake caches the token and refreshes it automatically before expiry.
- Identity context: The token represents the application (service principal) in the IdP.
Option B: Self-Signed JWT Flow (Service Account with Private Key)#
Some identity providers — such as GCP — do not offer a client_credentials token endpoint that Snowflake can call natively. Instead, they provide only a private key for the service account. Custom glue code in the UDF constructs and signs a JWT at runtime. The signed JWT is then presented directly to Account B as a Bearer token.
- Identity Provider: Any IdP that publishes a JWKS endpoint for service account public keys (e.g., GCP Service Accounts)
- How it works:
- Create a service account in the IdP and generate a private key
- Obtain the JWKS URL where the IdP publishes the corresponding public key
- From the JWKS response, note the
kid(Key ID) - Store the private key as a SECRET (TYPE = GENERIC_STRING) in Account A
- A Python UDF in Account A signs a JWT with custom claims
- An External OAuth security integration in Account B validates the JWT signature via the JWKS URL
JWT Structure:
| Field | Value |
|---|---|
| Header | kid = Key ID from JWKS URL response |
| iss | Service account unique identifier (must match EXTERNAL_OAUTH_ISSUER in Account B) |
| aud | Audience string (must match EXTERNAL_OAUTH_AUDIENCE_LIST in Account B) |
| scp | session:role:<SNOWFLAKE_ROLE> — maps to the Snowflake role |
| name | Service account email — maps to Snowflake login_name |
| iat | Issued-at timestamp |
| exp | Expiration (typically 30-60 minutes) |
- Token lifecycle: The UDF signs a new JWT on each invocation (or caches it for its validity window). No refresh token involved.
- Key advantage: No OAuth token exchange at runtime. The signed JWT is presented directly, reducing network hops.
Token Retrieval at Runtime#
The token retrieval mechanism differs depending on the OAuth flow:
Client Credentials (e.g., Entra ID) — Snowflake handles everything natively:
- The UDF calls
_snowflake.get_oauth_access_token('cred') - Snowflake checks if a valid (non-expired) access token is cached
- If expired, Snowflake exchanges the client ID and secret for a new token
- The UDF includes the token in the
Authorization: Bearer <token>header
Self-Signed JWT (e.g., GCP) — Custom glue code is required:
- The UDF retrieves the private key via
_snowflake.get_generic_secret_string('cred') - The UDF constructs a JWT payload with required claims
- The UDF signs the JWT using
pyjwtwith thekidfrom the JWKS endpoint - The signed JWT is included directly as the Bearer token
In all cases, the user executing the UDF in Account A does not need to authenticate interactively. The identity under which the SQL API executes on Account B is determined by the token claims.
How to Do It with Entra ID (Azure AD)#
Entra ID uses the Client Credentials flow. Because Entra ID provides a standard OAuth token endpoint, Snowflake handles the entire token lifecycle natively.
Account B — Accept Entra ID Tokens#
CREATE SECURITY INTEGRATION entra_external_oauth
TYPE = EXTERNAL_OAUTH
ENABLED = TRUE
EXTERNAL_OAUTH_TYPE = AZURE
EXTERNAL_OAUTH_ISSUER = 'https://sts.windows.net/<tenant_id>/'
EXTERNAL_OAUTH_JWS_KEYS_URL = 'https://login.microsoftonline.com/<tenant_id>/discovery/v2.0/keys'
EXTERNAL_OAUTH_AUDIENCE_LIST = ('api://<app_id>')
EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM = 'upn'
EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE = 'login_name'
EXTERNAL_OAUTH_ANY_ROLE_MODE = 'ENABLE';Account A — Client Credentials (Entra ID)#
-- Step 1: Network Rule
CREATE OR REPLACE NETWORK RULE target_account_rule
MODE = EGRESS
TYPE = HOST_PORT
VALUE_LIST = ('<account_b_identifier>.snowflakecomputing.com:443');
-- Step 2: Security Integration
CREATE OR REPLACE SECURITY INTEGRATION target_cc_oauth
TYPE = API_AUTHENTICATION
AUTH_TYPE = OAUTH2
OAUTH_CLIENT_ID = '<entra_app_client_id>'
OAUTH_CLIENT_SECRET = '<entra_app_client_secret>'
OAUTH_TOKEN_ENDPOINT = 'https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token'
OAUTH_GRANT = 'CLIENT_CREDENTIALS'
OAUTH_ALLOWED_SCOPES = ('api://<app_id>/.default')
ENABLED = TRUE;
-- Step 3: Secret
CREATE OR REPLACE SECRET target_cc_token
TYPE = OAUTH2
API_AUTHENTICATION = target_cc_oauth
OAUTH_SCOPES = ('api://<app_id>/.default');
-- Step 4: External Access Integration
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION target_cc_integration
ALLOWED_NETWORK_RULES = (target_account_rule)
ALLOWED_AUTHENTICATION_SECRETS = (target_cc_token)
ENABLED = TRUE;
-- Step 5: UDF
CREATE OR REPLACE FUNCTION exec_remote_sql(sqltext STRING, warehouse STRING, role_name STRING)
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = 3.11
HANDLER = 'run_remote'
EXTERNAL_ACCESS_INTEGRATIONS = (target_cc_integration)
PACKAGES = ('snowflake-snowpark-python', 'requests')
SECRETS = ('cred' = target_cc_token)
AS
$$
import _snowflake
import requests
def run_remote(sqltext, warehouse, role_name):
token = _snowflake.get_oauth_access_token('cred')
url = 'https://<account_b_identifier>.snowflakecomputing.com/api/v2/statements'
headers = {
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
"Accept": "application/json",
"Snowflake-Account": "<account_b_identifier>",
"X-Snowflake-Authorization-Token-Type": "OAUTH"
}
body = {
"statement": sqltext,
"timeout": 60,
"warehouse": warehouse.upper(),
"role": role_name.upper()
}
response = requests.post(url, json=body, headers=headers)
return response.json()
$$;
-- Usage
SELECT exec_remote_sql(
'SELECT CURRENT_USER(), CURRENT_ROLE(), CURRENT_TIMESTAMP()',
'COMPUTE_WH',
'TARGET_ROLE'
);How to Do It with a GCP Service Account#
GCP Service Accounts use the Self-Signed JWT flow. Unlike Entra ID, GCP does not provide a client_credentials token endpoint — instead, GCP provides only a private key. The UDF contains custom glue code to construct and sign a JWT at runtime.
GCP Setup Steps#
- In GCP Console, go to IAM & Admin, then Service Accounts, and create a service account
- Click into the service account, go to Keys, then Add Key, and select Create new key (JSON)
- Note the service account email (e.g.,
mysa@myproject.iam.gserviceaccount.com) - Retrieve the JWKS URL:
https://www.googleapis.com/service_accounts/v1/jwk/mysa@myproject.iam.gserviceaccount.com - From the JWKS response, note the
kidvalue for the active key
Account B — Accept GCP Service Account JWTs#
CREATE OR REPLACE SECURITY INTEGRATION gcp_sa_external_oauth
TYPE = EXTERNAL_OAUTH
ENABLED = TRUE
EXTERNAL_OAUTH_TYPE = CUSTOM
EXTERNAL_OAUTH_ISSUER = '<service_account_unique_id>@developer.gserviceaccount.com'
EXTERNAL_OAUTH_JWS_KEYS_URL = 'https://www.googleapis.com/service_accounts/v1/jwk/<sa_email>'
EXTERNAL_OAUTH_AUDIENCE_LIST = ('https://oauth2.googleapis.com/token')
EXTERNAL_OAUTH_SCOPE_MAPPING_ATTRIBUTE = 'scp'
EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM = 'name'
EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE = 'login_name';
-- Map the service account to a Snowflake user
ALTER USER <snowflake_user> SET login_name = '<sa_email>';
ALTER USER <snowflake_user> SET default_role = <target_role>;
GRANT ROLE <target_role> TO USER <snowflake_user>;Account A — Self-Signed JWT (GCP Service Account)#
CREATE OR REPLACE NETWORK RULE target_account_rule
MODE = EGRESS
TYPE = HOST_PORT
VALUE_LIST = ('<account_b_identifier>.snowflakecomputing.com:443');
CREATE OR REPLACE SECRET sa_private_key
TYPE = GENERIC_STRING
SECRET_STRING = '<pem_private_key_content>';
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION target_jwt_integration
ALLOWED_NETWORK_RULES = (target_account_rule)
ALLOWED_AUTHENTICATION_SECRETS = (sa_private_key)
ENABLED = TRUE;
CREATE OR REPLACE FUNCTION exec_remote_sql_jwt(
sqltext STRING, warehouse STRING, role_name STRING
)
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = 3.11
HANDLER = 'run_remote'
EXTERNAL_ACCESS_INTEGRATIONS = (target_jwt_integration)
PACKAGES = ('snowflake-snowpark-python', 'requests', 'pyjwt', 'cryptography')
SECRETS = ('cred' = sa_private_key)
AS
$$
import _snowflake
import jwt
import requests
from datetime import datetime, timedelta
def run_remote(sqltext, warehouse, role_name):
private_key = _snowflake.get_generic_secret_string('cred').encode()
now = datetime.utcnow()
payload = {
"iss": "<service_account_unique_id>@developer.gserviceaccount.com",
"aud": "https://oauth2.googleapis.com/token",
"scp": "session:role:" + role_name.upper(),
"name": "<sa_email>",
"iat": now,
"exp": now + timedelta(minutes=30)
}
token = jwt.encode(
payload, private_key, algorithm="RS256",
headers={"kid": "<kid_from_jwks_url>"}
)
url = 'https://<account_b_identifier>.snowflakecomputing.com/api/v2/statements'
headers = {
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
"Accept": "application/json",
"Snowflake-Account": "<account_b_identifier>",
"X-Snowflake-Authorization-Token-Type": "OAUTH"
}
body = {
"statement": sqltext,
"timeout": 60,
"warehouse": warehouse.upper(),
"role": role_name.upper()
}
response = requests.post(url, json=body, headers=headers)
return response.json()
$$;
-- Usage
SELECT exec_remote_sql_jwt(
'SELECT CURRENT_USER(), CURRENT_ROLE(), CURRENT_TIMESTAMP()',
'COMPUTE_WH',
'TARGET_ROLE'
);Non-OAuth Alternatives#
Programmatic Access Token (PAT)#
PATs can be generated for a specific user and scoped to a specific role. The PAT is stored as a secret in Account A and included directly in the SQL API request header.
- Role scoping: Yes
- Expiry: Yes (configurable)
- Account B setup: No External OAuth security integration required
-- ACCOUNT A
CREATE OR REPLACE SECRET target_pat_secret
TYPE = GENERIC_STRING
SECRET_STRING = '<programmatic_access_token>';
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION target_pat_integration
ALLOWED_NETWORK_RULES = (target_account_rule)
ALLOWED_AUTHENTICATION_SECRETS = (target_pat_secret)
ENABLED = TRUE;Key-Pair Authentication (Last Resort)#
Key-pair uses an RSA private key to generate a Snowflake-specific JWT. This is the least preferred option.
- Role scoping: No
- Expiry: No
- Trade-off: Static credential, manual rotation, no role downscoping
Comparison of All Approaches#
| Aspect | OAuth | PAT | Key-Pair |
|---|---|---|---|
| Recommendation | Preferred | Acceptable | Last resort |
| Role scoping | Yes | Yes | No |
| Token expiry | Yes (short-lived) | Yes (configurable) | No |
| Credential rotation | Automatic | Manual | Manual |
| Account B security integration | Required | Not required | Not required |
| Runtime IdP dependency | Client Creds: yes; JWT: none | None | None |
Security Considerations#
Network Policies: If Account B enforces network policies, the calling account’s egress IP addresses are Snowflake-managed and not predictable. Private connectivity (PrivateLink) between accounts can address this.
Least Privilege: The role used for SQL API execution is specified in the API request body. Organizations can limit available roles through the OAuth security integration configuration.
Secret Management: Secrets are encrypted using Snowflake’s key hierarchy. RBAC controls which roles can use the secret. Sensitive values are not exposed via DESCRIBE SECRET.
Audit Trail: SQL API requests on Account B are logged in QUERY_HISTORY and LOGIN_HISTORY, attributed to the user/service principal associated with the token.
Related#
- How JWT Tokens Actually Work — the fundamentals of JWT structure, signing, and verification
- Snowflake OAuth & PAT Toolkit — JWT-to-PAT exchange for MCP authentication
- Governing AI Inference in the Data Cloud — identity propagation and OAuth for AI agents
- JWT Playground — decode, edit, and verify JWTs in your browser
