Skip to content

Commit b57e129

Browse files
authored
Don't auto-generate code_verifier by default. (#48)
1 parent e0d21ef commit b57e129

File tree

2 files changed

+83
-14
lines changed

2 files changed

+83
-14
lines changed

google_auth_oauthlib/flow.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ class Flow(object):
9595

9696
def __init__(
9797
self, oauth2session, client_type, client_config,
98-
redirect_uri=None, code_verifier=None):
98+
redirect_uri=None, code_verifier=None,
99+
autogenerate_code_verifier=False):
99100
"""
100101
Args:
101102
oauth2session (requests_oauthlib.OAuth2Session):
@@ -108,8 +109,9 @@ def __init__(
108109
creation time. Otherwise, it will need to be set using
109110
:attr:`redirect_uri`.
110111
code_verifier (str): random string of 43-128 chars used to verify
111-
the key exchange.using PKCE. Auto-generated if not provided.
112-
112+
the key exchange.using PKCE.
113+
autogenerate_code_verifier (bool): If true, auto-generate a
114+
code_verifier.
113115
.. _client secrets:
114116
https://developers.google.com/api-client-library/python/guide
115117
/aaa_client_secrets
@@ -122,6 +124,7 @@ def __init__(
122124
"""requests_oauthlib.OAuth2Session: The OAuth 2.0 session."""
123125
self.redirect_uri = redirect_uri
124126
self.code_verifier = code_verifier
127+
self.autogenerate_code_verifier = autogenerate_code_verifier
125128

126129
@classmethod
127130
def from_client_config(cls, client_config, scopes, **kwargs):
@@ -155,12 +158,25 @@ def from_client_config(cls, client_config, scopes, **kwargs):
155158
raise ValueError(
156159
'Client secrets must be for a web or installed app.')
157160

161+
# these args cannot be passed to requests_oauthlib.OAuth2Session
162+
code_verifier = kwargs.pop('code_verifier', None)
163+
autogenerate_code_verifier = kwargs.pop(
164+
'autogenerate_code_verifier', None)
165+
158166
session, client_config = (
159167
google_auth_oauthlib.helpers.session_from_client_config(
160168
client_config, scopes, **kwargs))
161169

162170
redirect_uri = kwargs.get('redirect_uri', None)
163-
return cls(session, client_type, client_config, redirect_uri)
171+
172+
return cls(
173+
session,
174+
client_type,
175+
client_config,
176+
redirect_uri,
177+
code_verifier,
178+
autogenerate_code_verifier
179+
)
164180

165181
@classmethod
166182
def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
@@ -217,18 +233,20 @@ def authorization_url(self, **kwargs):
217233
specify the ``state`` when constructing the :class:`Flow`.
218234
"""
219235
kwargs.setdefault('access_type', 'offline')
220-
if not self.code_verifier:
236+
if self.autogenerate_code_verifier:
221237
chars = ascii_letters+digits+'-._~'
222238
rnd = SystemRandom()
223239
random_verifier = [rnd.choice(chars) for _ in range(0, 128)]
224240
self.code_verifier = ''.join(random_verifier)
225-
code_hash = hashlib.sha256()
226-
code_hash.update(str.encode(self.code_verifier))
227-
unencoded_challenge = code_hash.digest()
228-
b64_challenge = urlsafe_b64encode(unencoded_challenge)
229-
code_challenge = b64_challenge.decode().split('=')[0]
230-
kwargs.setdefault('code_challenge', code_challenge)
231-
kwargs.setdefault('code_challenge_method', 'S256')
241+
242+
if self.code_verifier:
243+
code_hash = hashlib.sha256()
244+
code_hash.update(str.encode(self.code_verifier))
245+
unencoded_challenge = code_hash.digest()
246+
b64_challenge = urlsafe_b64encode(unencoded_challenge)
247+
code_challenge = b64_challenge.decode().split('=')[0]
248+
kwargs.setdefault('code_challenge', code_challenge)
249+
kwargs.setdefault('code_challenge_method', 'S256')
232250
url, state = self.oauth2session.authorization_url(
233251
self.client_config['auth_uri'], **kwargs)
234252

tests/test_flow.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,23 @@ def test_redirect_uri(self, instance):
8686
mock.sentinel.redirect_uri)
8787

8888
def test_authorization_url(self, instance):
89+
scope = 'scope_one'
90+
instance.oauth2session.scope = [scope]
91+
authorization_url_patch = mock.patch.object(
92+
instance.oauth2session, 'authorization_url',
93+
wraps=instance.oauth2session.authorization_url)
94+
95+
with authorization_url_patch as authorization_url_spy:
96+
url, _ = instance.authorization_url(prompt='consent')
97+
98+
assert CLIENT_SECRETS_INFO['web']['auth_uri'] in url
99+
assert scope in url
100+
authorization_url_spy.assert_called_with(
101+
CLIENT_SECRETS_INFO['web']['auth_uri'],
102+
access_type='offline',
103+
prompt='consent')
104+
105+
def test_authorization_url_code_verifier(self, instance):
89106
scope = 'scope_one'
90107
instance.oauth2session.scope = [scope]
91108
instance.code_verifier = 'amanaplanacanalpanama'
@@ -124,9 +141,11 @@ def test_authorization_url_access_type(self, instance):
124141
code_challenge='2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q',
125142
code_challenge_method='S256')
126143

127-
def test_authorization_url_generated_verifier(self, instance):
144+
def test_authorization_url_generated_verifier(self):
128145
scope = 'scope_one'
129-
instance.oauth2session.scope = [scope]
146+
instance = flow.Flow.from_client_config(
147+
CLIENT_SECRETS_INFO, scopes=[scope],
148+
autogenerate_code_verifier=True)
130149
authorization_url_path = mock.patch.object(
131150
instance.oauth2session, 'authorization_url',
132151
wraps=instance.oauth2session.authorization_url)
@@ -242,6 +261,38 @@ def test_run_local_server(
242261
auth_redirect_url = urllib.parse.urljoin(
243262
'http://localhost:60452',
244263
self.REDIRECT_REQUEST_PATH)
264+
265+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
266+
future = pool.submit(partial(
267+
instance.run_local_server, port=60452))
268+
269+
while not future.done():
270+
try:
271+
requests.get(auth_redirect_url)
272+
except requests.ConnectionError: # pragma: NO COVER
273+
pass
274+
275+
credentials = future.result()
276+
277+
assert credentials.token == mock.sentinel.access_token
278+
assert credentials._refresh_token == mock.sentinel.refresh_token
279+
assert credentials.id_token == mock.sentinel.id_token
280+
assert webbrowser_mock.open.called
281+
282+
expected_auth_response = auth_redirect_url.replace('http', 'https')
283+
mock_fetch_token.assert_called_with(
284+
CLIENT_SECRETS_INFO['web']['token_uri'],
285+
client_secret=CLIENT_SECRETS_INFO['web']['client_secret'],
286+
authorization_response=expected_auth_response,
287+
code_verifier=None)
288+
289+
@pytest.mark.webtest
290+
@mock.patch('google_auth_oauthlib.flow.webbrowser', autospec=True)
291+
def test_run_local_server_code_verifier(
292+
self, webbrowser_mock, instance, mock_fetch_token):
293+
auth_redirect_url = urllib.parse.urljoin(
294+
'http://localhost:60452',
295+
self.REDIRECT_REQUEST_PATH)
245296
instance.code_verifier = 'amanaplanacanalpanama'
246297

247298
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:

0 commit comments

Comments
 (0)