Skip to content

Commit 640cf6c

Browse files
Support client-side opt-in of Refresh Token Rotation in Snowflake OAuth (#2294)
1 parent ce85800 commit 640cf6c

File tree

4 files changed

+75
-0
lines changed

4 files changed

+75
-0
lines changed

src/snowflake/connector/auth/oauth_code.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def __init__(
5858
token_cache: TokenCache | None = None,
5959
refresh_token_enabled: bool = False,
6060
external_browser_timeout: int | None = None,
61+
enable_single_use_refresh_tokens: bool = False,
6162
**kwargs,
6263
) -> None:
6364
super().__init__(
@@ -81,6 +82,7 @@ def __init__(
8182
logger.debug("oauth pkce is going to be used")
8283
self._verifier: str | None = None
8384
self._external_browser_timeout = external_browser_timeout
85+
self._enable_single_use_refresh_tokens = enable_single_use_refresh_tokens
8486

8587
def _get_oauth_type_id(self) -> str:
8688
return OAUTH_TYPE_AUTHORIZATION_CODE
@@ -296,6 +298,8 @@ def _do_token_request(
296298
"code": code,
297299
"redirect_uri": callback_server.url,
298300
}
301+
if self._enable_single_use_refresh_tokens:
302+
fields["enable_single_use_refresh_tokens"] = "true"
299303
if self._pkce_enabled:
300304
assert self._verifier is not None
301305
fields["code_verifier"] = self._verifier

src/snowflake/connector/connection.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,11 @@ def _get_private_bytes_from_file(
355355
True,
356356
bool,
357357
), # SNOW-XXXXX: remove the check_arrow_conversion_error_on_every_column flag
358+
# Client-side opt-in to single-use refresh tokens.
359+
"oauth_enable_single_use_refresh_tokens": (
360+
False,
361+
bool,
362+
),
358363
}
359364

360365
APPLICATION_RE = re.compile(r"[\w\d_]+")
@@ -1236,6 +1241,7 @@ def __open_connection(self):
12361241
),
12371242
refresh_token_enabled=features.refresh_token_enabled,
12381243
external_browser_timeout=self._external_browser_timeout,
1244+
enable_single_use_refresh_tokens=self._oauth_enable_single_use_refresh_tokens,
12391245
)
12401246
elif self._authenticator == OAUTH_CLIENT_CREDENTIALS:
12411247
self._check_experimental_authentication_flag()

test/unit/test_auth_oauth_auth_code.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
44
#
55

6+
from unittest.mock import patch
7+
8+
import pytest
9+
610
from snowflake.connector.auth import AuthByOauthCode
11+
from snowflake.connector.network import OAUTH_AUTHORIZATION_CODE
712

813

914
def test_auth_oauth_auth_code_oauth_type():
@@ -20,3 +25,42 @@ def test_auth_oauth_auth_code_oauth_type():
2025
body = {"data": {}}
2126
auth.update_body(body)
2227
assert body["data"]["OAUTH_TYPE"] == "authorization_code"
28+
29+
30+
@pytest.mark.parametrize("rtr_enabled", [True, False])
31+
def test_auth_oauth_auth_code_single_use_refresh_tokens(rtr_enabled: bool):
32+
"""Verifies that the enable_single_use_refresh_tokens option is plumbed into the authz code request."""
33+
auth = AuthByOauthCode(
34+
"app",
35+
"clientId",
36+
"clientSecret",
37+
"auth_url",
38+
"tokenRequestUrl",
39+
"http://127.0.0.1:8080",
40+
"scope",
41+
pkce_enabled=False,
42+
enable_single_use_refresh_tokens=rtr_enabled,
43+
)
44+
45+
def fake_get_request_token_response(_, fields: dict[str, str]):
46+
if rtr_enabled:
47+
assert fields.get("enable_single_use_refresh_tokens") == "true"
48+
else:
49+
assert "enable_single_use_refresh_tokens" not in fields
50+
return ("access_token", "refresh_token")
51+
52+
with patch(
53+
"snowflake.connector.auth.AuthByOauthCode._do_authorization_request",
54+
return_value="abc",
55+
):
56+
with patch(
57+
"snowflake.connector.auth.AuthByOauthCode._get_request_token_response",
58+
side_effect=fake_get_request_token_response,
59+
):
60+
auth.prepare(
61+
conn=None,
62+
authenticator=OAUTH_AUTHORIZATION_CODE,
63+
service_name=None,
64+
account="acc",
65+
user="user",
66+
)

test/unit/test_connection.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,3 +687,24 @@ def test_toml_connection_params_are_plumbed_into_authbyworkloadidentity(
687687
== "api://0b2f151f-09a2-46eb-ad5a-39d5ebef917b"
688688
)
689689
assert conn.auth_class.token == "my_token"
690+
691+
692+
@pytest.mark.parametrize("rtr_enabled", [True, False])
693+
def test_single_use_refresh_tokens_option_is_plumbed_into_authbyauthcode(
694+
monkeypatch, rtr_enabled: bool
695+
):
696+
with monkeypatch.context() as m:
697+
m.setattr(
698+
"snowflake.connector.SnowflakeConnection._authenticate", lambda *_: None
699+
)
700+
m.setenv("SF_ENABLE_EXPERIMENTAL_AUTHENTICATION", "true")
701+
702+
conn = snowflake.connector.connect(
703+
account="my_account_1",
704+
user="user",
705+
oauth_client_id="client_id",
706+
oauth_client_secret="client_secret",
707+
authenticator="OAUTH_AUTHORIZATION_CODE",
708+
oauth_enable_single_use_refresh_tokens=rtr_enabled,
709+
)
710+
assert conn.auth_class._enable_single_use_refresh_tokens == rtr_enabled

0 commit comments

Comments
 (0)