Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/snowflake/connector/auth/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,16 @@ def read_temporary_credentials(
user: str,
session_parameters: dict[str, Any],
) -> None:
"""Attempt to load cached credentials to skip interactive authentication.

SSO (ID_TOKEN): If present, avoids opening browser for external authentication.
Controlled by client_store_temporary_credential parameter.

MFA (MFA_TOKEN): If present, skips MFA prompt on next connection.
Controlled by client_request_mfa_token parameter.

If cached tokens are expired/invalid, they're deleted and normal auth proceeds.
"""
if session_parameters.get(PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL, False):
self._rest.id_token = self._read_temporary_credential(
host,
Expand Down Expand Up @@ -549,6 +559,13 @@ def write_temporary_credentials(
session_parameters: dict[str, Any],
response: dict[str, Any],
) -> None:
"""Cache credentials received from successful authentication for future use.

Tokens are only cached if:
1. Server returned the token in response (server-side caching must be enabled)
2. Client has caching enabled via session parameters
3. User consented to caching (consent_cache_id_token for ID tokens)
"""
if (
self._rest._connection.auth_class.consent_cache_id_token
and session_parameters.get(
Expand Down
18 changes: 16 additions & 2 deletions src/snowflake/connector/auth/_oauth_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@


class _OAuthTokensMixin:
"""Manages OAuth token caching to avoid repeated browser authentication flows.

Access tokens: Short-lived (typically 10 minutes), cached to avoid immediate re-auth.
Refresh tokens: Long-lived (hours/days), used to obtain new access tokens silently.

Tokens are cached per (user, IDP host) to support multiple OAuth providers/accounts.
"""

def __init__(
self,
token_cache: TokenCache | None,
Expand Down Expand Up @@ -77,12 +85,18 @@ def _pop_cached_token(self, key: TokenKey | None) -> str | None:
return self._token_cache.retrieve(key)

def _pop_cached_access_token(self) -> bool:
"""Retrieves OAuth access token from the token cache if enabled"""
"""Retrieves OAuth access token from the token cache if enabled, available and still valid.

Returns True if cached token found, allowing authentication to skip OAuth flow.
"""
self._access_token = self._pop_cached_token(self._get_access_token_cache_key())
return self._access_token is not None

def _pop_cached_refresh_token(self) -> bool:
"""Retrieves OAuth refresh token from the token cache if enabled"""
"""Retrieves OAuth refresh token from the token cache (if enabled) to silently obtain new access token.

Returns True if refresh token found, enabling automatic token renewal without user interaction.
"""
if self._refresh_token_enabled:
self._refresh_token = self._pop_cached_token(
self._get_refresh_token_cache_key()
Expand Down
11 changes: 10 additions & 1 deletion src/snowflake/connector/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,13 @@ def _get_private_bytes_from_file(
"support_negative_year": (True, bool), # snowflake
"log_max_query_length": (LOG_MAX_QUERY_LENGTH, int), # snowflake
"disable_request_pooling": (False, bool), # snowflake
# enable temporary credential file for Linux, default false. Mac/Win will overlook this
# Cache SSO ID tokens to avoid repeated browser popups. Must be enabled on the server-side.
# Storage: keyring (macOS/Windows), file (Linux). Auto-enabled on macOS/Windows.
# Sets session PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL as well
"client_store_temporary_credential": (False, bool),
# Cache MFA tokens to skip MFA prompts on reconnect. Must be enabled on the server-side.
# Storage: keyring (macOS/Windows), file (Linux). Auto-enabled on macOS/Windows.
# In driver, we extract this from session using PARAMETER_CLIENT_REQUEST_MFA_TOKEN.
"client_request_mfa_token": (False, bool),
"use_openssl_only": (
True,
Expand Down Expand Up @@ -1391,9 +1396,11 @@ def __open_connection(self):
backoff_generator=self._backoff_generator,
)
elif self._authenticator == EXTERNAL_BROWSER_AUTHENTICATOR:
# Enable SSO credential caching
self._session_parameters[
PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL
] = (self._client_store_temporary_credential if IS_LINUX else True)
# Try to load cached ID token to avoid browser popup
auth.read_temporary_credentials(
self.host,
self.user,
Expand Down Expand Up @@ -1484,9 +1491,11 @@ def __open_connection(self):
connection=self,
)
elif self._authenticator == USR_PWD_MFA_AUTHENTICATOR:
# Enable MFA token caching
self._session_parameters[PARAMETER_CLIENT_REQUEST_MFA_TOKEN] = (
self._client_request_mfa_token if IS_LINUX else True
)
# Try to load cached MFA token to skip MFA prompt
if self._session_parameters[PARAMETER_CLIENT_REQUEST_MFA_TOKEN]:
auth.read_temporary_credentials(
self.host,
Expand Down
37 changes: 37 additions & 0 deletions src/snowflake/connector/token_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@


class TokenType(Enum):
"""Types of credentials that can be cached to avoid repeated authentication.

- ID_TOKEN: SSO identity token from external browser/Okta authentication
- MFA_TOKEN: Multi-factor authentication token to skip MFA prompts
- OAUTH_ACCESS_TOKEN: Short-lived OAuth access token
- OAUTH_REFRESH_TOKEN: Long-lived OAuth token to obtain new access tokens
"""

ID_TOKEN = "ID_TOKEN"
MFA_TOKEN = "MFA_TOKEN"
OAUTH_ACCESS_TOKEN = "OAUTH_ACCESS_TOKEN"
Expand Down Expand Up @@ -57,6 +65,16 @@ def _warn(warning: str) -> None:


class TokenCache(ABC):
"""Secure storage for authentication credentials to avoid repeated login prompts.

Platform-specific implementations:
- macOS/Windows: Uses OS keyring (Keychain/Credential Manager) via 'keyring' library
- Linux: Uses encrypted JSON file in ~/.cache/snowflake/ with 0o600 permissions
- Fallback: NoopTokenCache (no caching) if secure storage unavailable

Tokens are keyed by (host, user, token_type) to support multiple accounts.
"""

@staticmethod
def make(skip_file_permissions_check: bool = False) -> TokenCache:
if IS_MACOS or IS_WINDOWS:
Expand Down Expand Up @@ -127,6 +145,17 @@ class _CacheFileWriteError(_FileTokenCacheError):


class FileTokenCache(TokenCache):
"""Linux implementation: stores tokens in JSON file with strict security.

Cache location (in priority order):
1. $SF_TEMPORARY_CREDENTIAL_CACHE_DIR/credential_cache_v1.json
2. $XDG_CACHE_HOME/snowflake/credential_cache_v1.json
3. $HOME/.cache/snowflake/credential_cache_v1.json

Security: File must have 0o600 permissions and be owned by current user.
Uses file locks to prevent concurrent access corruption.
"""

@staticmethod
def make(skip_file_permissions_check: bool = False) -> FileTokenCache | None:
cache_dir = FileTokenCache.find_cache_dir(skip_file_permissions_check)
Expand Down Expand Up @@ -364,6 +393,14 @@ def _ensure_permissions(self, fd: int, permissions: int) -> None:


class KeyringTokenCache(TokenCache):
"""macOS/Windows implementation: uses OS-native secure credential storage.

- macOS: Stores tokens in Keychain
- Windows: Stores tokens in Windows Credential Manager

Tokens are stored with service="{HOST}:{USER}:{TOKEN_TYPE}" and username="{USER}".
"""

def __init__(self) -> None:
self.logger = logging.getLogger(__name__)

Expand Down
Loading