Skip to main content
  1. Posts/

Federated Queries Between Snowflake Accounts Using Service Accounts and OAuth

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
#

AccountIntegration TypePurpose
Account A (Calling)TYPE = API_AUTHENTICATIONStores 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 IntegrationEnables EGRESS to Account B and binds secret to UDFs
Account B (Target)TYPE = EXTERNAL_OAUTHAccepts 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:

RankApproachRole ScopingToken ExpiryCredential TypeNotes
1 (Recommended)OAuth (Client Credentials or Self-Signed JWT)Yes — token can be scoped to a specific Snowflake role via claims or scope mappingYes — access tokens are short-lived (minutes to hours)DynamicTokens are ephemeral and automatically rotated.
2Programmatic Access Token (PAT)Yes — PAT can be scoped to a specific role at creation timeYes — configurable expiry at creationSemi-staticPATs 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 timeNo — the private key has no intrinsic expiryStaticLong-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_credentials grant (e.g., Entra ID, Okta)
  • How it works:
    1. An application registration in the IdP is configured with application permissions (not delegated)
    2. A security integration in Account A uses OAUTH_GRANT = 'CLIENT_CREDENTIALS'
    3. A secret of TYPE = OAUTH2 in Account A references the security integration with OAUTH_SCOPES
    4. An External OAuth security integration in Account B validates inbound tokens and maps the service principal to a Snowflake user
    5. 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:
    1. Create a service account in the IdP and generate a private key
    2. Obtain the JWKS URL where the IdP publishes the corresponding public key
    3. From the JWKS response, note the kid (Key ID)
    4. Store the private key as a SECRET (TYPE = GENERIC_STRING) in Account A
    5. A Python UDF in Account A signs a JWT with custom claims
    6. An External OAuth security integration in Account B validates the JWT signature via the JWKS URL

JWT Structure:

FieldValue
Headerkid = Key ID from JWKS URL response
issService account unique identifier (must match EXTERNAL_OAUTH_ISSUER in Account B)
audAudience string (must match EXTERNAL_OAUTH_AUDIENCE_LIST in Account B)
scpsession:role:<SNOWFLAKE_ROLE> — maps to the Snowflake role
nameService account email — maps to Snowflake login_name
iatIssued-at timestamp
expExpiration (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:

  1. The UDF calls _snowflake.get_oauth_access_token('cred')
  2. Snowflake checks if a valid (non-expired) access token is cached
  3. If expired, Snowflake exchanges the client ID and secret for a new token
  4. The UDF includes the token in the Authorization: Bearer <token> header

Self-Signed JWT (e.g., GCP) — Custom glue code is required:

  1. The UDF retrieves the private key via _snowflake.get_generic_secret_string('cred')
  2. The UDF constructs a JWT payload with required claims
  3. The UDF signs the JWT using pyjwt with the kid from the JWKS endpoint
  4. 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
#

  1. In GCP Console, go to IAM & Admin, then Service Accounts, and create a service account
  2. Click into the service account, go to Keys, then Add Key, and select Create new key (JSON)
  3. Note the service account email (e.g., mysa@myproject.iam.gserviceaccount.com)
  4. Retrieve the JWKS URL: https://www.googleapis.com/service_accounts/v1/jwk/mysa@myproject.iam.gserviceaccount.com
  5. From the JWKS response, note the kid value 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
#

AspectOAuthPATKey-Pair
RecommendationPreferredAcceptableLast resort
Role scopingYesYesNo
Token expiryYes (short-lived)Yes (configurable)No
Credential rotationAutomaticManualManual
Account B security integrationRequiredNot requiredNot required
Runtime IdP dependencyClient Creds: yes; JWT: noneNoneNone

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#

Documentation References
#

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