Skip to content

Commit 1a6c193

Browse files
SNOW-2462946: document credential caching (SSO, MFA) in the in-repo docs (snowflakedb#2626)
1 parent 1a0290d commit 1a6c193

File tree

4 files changed

+80
-3
lines changed

4 files changed

+80
-3
lines changed

src/snowflake/connector/auth/_auth.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,16 @@ def read_temporary_credentials(
514514
user: str,
515515
session_parameters: dict[str, Any],
516516
) -> None:
517+
"""Attempt to load cached credentials to skip interactive authentication.
518+
519+
SSO (ID_TOKEN): If present, avoids opening browser for external authentication.
520+
Controlled by client_store_temporary_credential parameter.
521+
522+
MFA (MFA_TOKEN): If present, skips MFA prompt on next connection.
523+
Controlled by client_request_mfa_token parameter.
524+
525+
If cached tokens are expired/invalid, they're deleted and normal auth proceeds.
526+
"""
517527
if session_parameters.get(PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL, False):
518528
self._rest.id_token = self._read_temporary_credential(
519529
host,
@@ -549,6 +559,13 @@ def write_temporary_credentials(
549559
session_parameters: dict[str, Any],
550560
response: dict[str, Any],
551561
) -> None:
562+
"""Cache credentials received from successful authentication for future use.
563+
564+
Tokens are only cached if:
565+
1. Server returned the token in response (server-side caching must be enabled)
566+
2. Client has caching enabled via session parameters
567+
3. User consented to caching (consent_cache_id_token for ID tokens)
568+
"""
552569
if (
553570
self._rest._connection.auth_class.consent_cache_id_token
554571
and session_parameters.get(

src/snowflake/connector/auth/_oauth_base.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535

3636

3737
class _OAuthTokensMixin:
38+
"""Manages OAuth token caching to avoid repeated browser authentication flows.
39+
40+
Access tokens: Short-lived (typically 10 minutes), cached to avoid immediate re-auth.
41+
Refresh tokens: Long-lived (hours/days), used to obtain new access tokens silently.
42+
43+
Tokens are cached per (user, IDP host) to support multiple OAuth providers/accounts.
44+
"""
45+
3846
def __init__(
3947
self,
4048
token_cache: TokenCache | None,
@@ -77,12 +85,18 @@ def _pop_cached_token(self, key: TokenKey | None) -> str | None:
7785
return self._token_cache.retrieve(key)
7886

7987
def _pop_cached_access_token(self) -> bool:
80-
"""Retrieves OAuth access token from the token cache if enabled"""
88+
"""Retrieves OAuth access token from the token cache if enabled, available and still valid.
89+
90+
Returns True if cached token found, allowing authentication to skip OAuth flow.
91+
"""
8192
self._access_token = self._pop_cached_token(self._get_access_token_cache_key())
8293
return self._access_token is not None
8394

8495
def _pop_cached_refresh_token(self) -> bool:
85-
"""Retrieves OAuth refresh token from the token cache if enabled"""
96+
"""Retrieves OAuth refresh token from the token cache (if enabled) to silently obtain new access token.
97+
98+
Returns True if refresh token found, enabling automatic token renewal without user interaction.
99+
"""
86100
if self._refresh_token_enabled:
87101
self._refresh_token = self._pop_cached_token(
88102
self._get_refresh_token_cache_key()

src/snowflake/connector/connection.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,13 @@ def _get_private_bytes_from_file(
280280
"support_negative_year": (True, bool), # snowflake
281281
"log_max_query_length": (LOG_MAX_QUERY_LENGTH, int), # snowflake
282282
"disable_request_pooling": (False, bool), # snowflake
283-
# enable temporary credential file for Linux, default false. Mac/Win will overlook this
283+
# Cache SSO ID tokens to avoid repeated browser popups. Must be enabled on the server-side.
284+
# Storage: keyring (macOS/Windows), file (Linux). Auto-enabled on macOS/Windows.
285+
# Sets session PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL as well
284286
"client_store_temporary_credential": (False, bool),
287+
# Cache MFA tokens to skip MFA prompts on reconnect. Must be enabled on the server-side.
288+
# Storage: keyring (macOS/Windows), file (Linux). Auto-enabled on macOS/Windows.
289+
# In driver, we extract this from session using PARAMETER_CLIENT_REQUEST_MFA_TOKEN.
285290
"client_request_mfa_token": (False, bool),
286291
"use_openssl_only": (
287292
True,
@@ -1397,9 +1402,11 @@ def __open_connection(self):
13971402
backoff_generator=self._backoff_generator,
13981403
)
13991404
elif self._authenticator == EXTERNAL_BROWSER_AUTHENTICATOR:
1405+
# Enable SSO credential caching
14001406
self._session_parameters[
14011407
PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL
14021408
] = (self._client_store_temporary_credential if IS_LINUX else True)
1409+
# Try to load cached ID token to avoid browser popup
14031410
auth.read_temporary_credentials(
14041411
self.host,
14051412
self.user,
@@ -1491,9 +1498,11 @@ def __open_connection(self):
14911498
connection=self,
14921499
)
14931500
elif self._authenticator == USR_PWD_MFA_AUTHENTICATOR:
1501+
# Enable MFA token caching
14941502
self._session_parameters[PARAMETER_CLIENT_REQUEST_MFA_TOKEN] = (
14951503
self._client_request_mfa_token if IS_LINUX else True
14961504
)
1505+
# Try to load cached MFA token to skip MFA prompt
14971506
if self._session_parameters[PARAMETER_CLIENT_REQUEST_MFA_TOKEN]:
14981507
auth.read_temporary_credentials(
14991508
self.host,

src/snowflake/connector/token_cache.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222

2323

2424
class TokenType(Enum):
25+
"""Types of credentials that can be cached to avoid repeated authentication.
26+
27+
- ID_TOKEN: SSO identity token from external browser/Okta authentication
28+
- MFA_TOKEN: Multi-factor authentication token to skip MFA prompts
29+
- OAUTH_ACCESS_TOKEN: Short-lived OAuth access token
30+
- OAUTH_REFRESH_TOKEN: Long-lived OAuth token to obtain new access tokens
31+
"""
32+
2533
ID_TOKEN = "ID_TOKEN"
2634
MFA_TOKEN = "MFA_TOKEN"
2735
OAUTH_ACCESS_TOKEN = "OAUTH_ACCESS_TOKEN"
@@ -57,6 +65,16 @@ def _warn(warning: str) -> None:
5765

5866

5967
class TokenCache(ABC):
68+
"""Secure storage for authentication credentials to avoid repeated login prompts.
69+
70+
Platform-specific implementations:
71+
- macOS/Windows: Uses OS keyring (Keychain/Credential Manager) via 'keyring' library
72+
- Linux: Uses encrypted JSON file in ~/.cache/snowflake/ with 0o600 permissions
73+
- Fallback: NoopTokenCache (no caching) if secure storage unavailable
74+
75+
Tokens are keyed by (host, user, token_type) to support multiple accounts.
76+
"""
77+
6078
@staticmethod
6179
def make(skip_file_permissions_check: bool = False) -> TokenCache:
6280
if IS_MACOS or IS_WINDOWS:
@@ -127,6 +145,17 @@ class _CacheFileWriteError(_FileTokenCacheError):
127145

128146

129147
class FileTokenCache(TokenCache):
148+
"""Linux implementation: stores tokens in JSON file with strict security.
149+
150+
Cache location (in priority order):
151+
1. $SF_TEMPORARY_CREDENTIAL_CACHE_DIR/credential_cache_v1.json
152+
2. $XDG_CACHE_HOME/snowflake/credential_cache_v1.json
153+
3. $HOME/.cache/snowflake/credential_cache_v1.json
154+
155+
Security: File must have 0o600 permissions and be owned by current user.
156+
Uses file locks to prevent concurrent access corruption.
157+
"""
158+
130159
@staticmethod
131160
def make(skip_file_permissions_check: bool = False) -> FileTokenCache | None:
132161
cache_dir = FileTokenCache.find_cache_dir(skip_file_permissions_check)
@@ -364,6 +393,14 @@ def _ensure_permissions(self, fd: int, permissions: int) -> None:
364393

365394

366395
class KeyringTokenCache(TokenCache):
396+
"""macOS/Windows implementation: uses OS-native secure credential storage.
397+
398+
- macOS: Stores tokens in Keychain
399+
- Windows: Stores tokens in Windows Credential Manager
400+
401+
Tokens are stored with service="{HOST}:{USER}:{TOKEN_TYPE}" and username="{USER}".
402+
"""
403+
367404
def __init__(self) -> None:
368405
self.logger = logging.getLogger(__name__)
369406

0 commit comments

Comments
 (0)