Skip to content

Commit 3684460

Browse files
implement PKCE
1 parent 6d025e5 commit 3684460

File tree

3 files changed

+32
-0
lines changed

3 files changed

+32
-0
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1616
- Added a feature to verify if the connection is still good enough to send queries over.
1717
- Added support for base64-encoded DER private key strings in the `private_key` authentication type.
1818
- Added support for OAuth authorization code flow.
19+
- Added support for PKCE on top of OAuth authorization flow.
1920

2021
- v3.12.4(December 3,2024)
2122
- Fixed a bug where multipart uploads to Azure would be missing their MD5 hashes.

src/snowflake/connector/auth/oauth_code.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
from __future__ import annotations
66

7+
import base64
8+
import hashlib
79
import json
810
import logging
11+
import re
912
import secrets
1013
import socket
1114
import time
@@ -57,6 +60,7 @@ def __init__(
5760
token_request_url: str,
5861
redirect_uri: str,
5962
scope: str,
63+
pkce: bool = False,
6064
**kwargs,
6165
) -> None:
6266
super().__init__(**kwargs)
@@ -74,6 +78,8 @@ def __init__(
7478
logger.debug("chose oauth state: %s", self._state)
7579
self._oauth_token = None
7680
self._protocol = "http"
81+
self.pkce = pkce
82+
self._verifier: str | None = None
7783

7884
def reset_secrets(self) -> None:
7985
self._oauth_token = None
@@ -104,6 +110,19 @@ def construct_url(self) -> str:
104110
"scope": self.scope,
105111
"state": self._state,
106112
}
113+
if self.pkce:
114+
self._verifier = secrets.token_urlsafe(43)
115+
self._verifier = re.sub("[^a-zA-Z0-9]+", "", self._verifier)
116+
# calculate challenge and verifier
117+
challenge = (
118+
base64.urlsafe_b64encode(
119+
hashlib.sha256(self._verifier.encode("utf-8")).digest()
120+
)
121+
.decode("utf-8")
122+
.replace("=", "")
123+
)
124+
params["code_challenge"] = challenge
125+
params["code_challenge_method"] = "S256"
107126
url_params = urllib.parse.urlencode(params)
108127
url = f"{self.authentication_url}?{url_params}"
109128
return url
@@ -186,6 +205,10 @@ def prepare(
186205
}
187206
if self.client_secret:
188207
fields["client_secret"] = self.client_secret
208+
if self.pkce:
209+
assert self._verifier is not None
210+
fields["code_verifier"] = self._verifier
211+
189212
resp = urllib3.PoolManager().request_encode_body( # TODO: use network pool to gain use of proxy settings and so on
190213
"POST",
191214
self.token_request_url,

src/snowflake/connector/connection.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from __future__ import annotations
77

88
import atexit
9+
import collections.abc
910
import logging
1011
import os
1112
import pathlib
@@ -338,6 +339,11 @@ def _get_private_bytes_from_file(
338339
str,
339340
# SNOW-1825621: OAUTH implementation
340341
),
342+
"oauth_security_features": (
343+
("pkce",),
344+
collections.abc.Iterable, # of strings
345+
# SNOW-1825621: OAUTH PKCE
346+
),
341347
}
342348

343349
APPLICATION_RE = re.compile(r"[\w\d_]+")
@@ -1122,6 +1128,7 @@ def __open_connection(self):
11221128
backoff_generator=self._backoff_generator,
11231129
)
11241130
elif self._authenticator == OAUTH_AUTHORIZATION_CODE:
1131+
pkce = "pkce" in map(lambda e: e.lower(), self._oauth_security_features)
11251132
if self._client_id is None:
11261133
Error.errorhandler_wrapper(
11271134
self,
@@ -1154,6 +1161,7 @@ def __open_connection(self):
11541161
),
11551162
redirect_uri=self._oauth_redirect_uri,
11561163
scope=self._oauth_scope.format(role=self._role),
1164+
pkce=pkce,
11571165
)
11581166
elif self._authenticator == USR_PWD_MFA_AUTHENTICATOR:
11591167
self._session_parameters[PARAMETER_CLIENT_REQUEST_MFA_TOKEN] = (

0 commit comments

Comments
 (0)