Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Commit 3ff4d55

Browse files
author
Jon Wayne Parrott
authored
Refactor common code from google.oauth2.flow to google.oauth2.oauthlib (#106)
1 parent 474c8df commit 3ff4d55

6 files changed

Lines changed: 294 additions & 63 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
google.oauth2.oauthlib module
2+
=============================
3+
4+
.. automodule:: google.oauth2.oauthlib
5+
:members:
6+
:inherited-members:
7+
:show-inheritance:

docs/reference/google.oauth2.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ Submodules
1414
google.oauth2.credentials
1515
google.oauth2.flow
1616
google.oauth2.id_token
17+
google.oauth2.oauthlib
1718
google.oauth2.service_account
1819

google/oauth2/flow.py

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,9 @@
5555

5656
import json
5757

58-
import requests_oauthlib
59-
6058
import google.auth.transport.requests
6159
import google.oauth2.credentials
62-
63-
_REQUIRED_CONFIG_KEYS = frozenset(('auth_uri', 'token_uri', 'client_id'))
60+
import google.oauth2.oauthlib
6461

6562

6663
class Flow(object):
@@ -82,8 +79,32 @@ class Flow(object):
8279
https://console.developers.google.com/apis/credentials
8380
"""
8481

85-
def __init__(self, client_config, scopes, **kwargs):
82+
def __init__(self, oauth2session, client_type, client_config):
8683
"""
84+
Args:
85+
oauth2session (requests_oauthlib.OAuth2Session):
86+
The OAuth 2.0 session from ``requests-oauthlib``.
87+
client_type (str): The client type, either ``web`` or
88+
``installed``.
89+
client_config (Mapping[str, Any]): The client
90+
configuration in the Google `client secrets`_ format.
91+
92+
.. _client secrets:
93+
https://developers.google.com/api-client-library/python/guide
94+
/aaa_client_secrets
95+
"""
96+
self.client_type = client_type
97+
"""str: The client type, either ``'web'`` or ``'installed'``"""
98+
self.client_config = client_config[client_type]
99+
"""Mapping[str, Any]: The OAuth 2.0 client configuration."""
100+
self.oauth2session = oauth2session
101+
"""requests_oauthlib.OAuth2Session: The OAuth 2.0 session."""
102+
103+
@classmethod
104+
def from_client_config(cls, client_config, scopes, **kwargs):
105+
"""Creates a :class:`requests_oauthlib.OAuth2Session` from client
106+
configuration loaded from a Google-format client secrets file.
107+
87108
Args:
88109
client_config (Mapping[str, Any]): The client
89110
configuration in the Google `client secrets`_ format.
@@ -92,6 +113,9 @@ def __init__(self, client_config, scopes, **kwargs):
92113
kwargs: Any additional parameters passed to
93114
:class:`requests_oauthlib.OAuth2Session`
94115
116+
Returns:
117+
Flow: The constructed Flow instance.
118+
95119
Raises:
96120
ValueError: If the client configuration is not in the correct
97121
format.
@@ -100,29 +124,19 @@ def __init__(self, client_config, scopes, **kwargs):
100124
https://developers.google.com/api-client-library/python/guide
101125
/aaa_client_secrets
102126
"""
103-
self.client_config = None
104-
"""Mapping[str, Any]: The OAuth 2.0 client configuration."""
105-
self.client_type = None
106-
"""str: The client type, either ``'web'`` or ``'installed'``"""
107-
108127
if 'web' in client_config:
109-
self.client_config = client_config['web']
110-
self.client_type = 'web'
128+
client_type = 'web'
111129
elif 'installed' in client_config:
112-
self.client_config = client_config['installed']
113-
self.client_type = 'installed'
130+
client_type = 'installed'
114131
else:
115132
raise ValueError(
116133
'Client secrets must be for a web or installed app.')
117134

118-
if not _REQUIRED_CONFIG_KEYS.issubset(self.client_config.keys()):
119-
raise ValueError('Client secrets is not in the correct format.')
135+
session, client_config = (
136+
google.oauth2.oauthlib.session_from_client_config(
137+
client_config, scopes, **kwargs))
120138

121-
self.oauth2session = requests_oauthlib.OAuth2Session(
122-
client_id=self.client_config['client_id'],
123-
scope=scopes,
124-
**kwargs)
125-
"""requests_oauthlib.OAuth2Session: The OAuth 2.0 session."""
139+
return cls(session, client_type, client_config)
126140

127141
@classmethod
128142
def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
@@ -142,7 +156,7 @@ def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
142156
with open(client_secrets_file, 'r') as json_file:
143157
client_config = json.load(json_file)
144158

145-
return cls(client_config, scopes=scopes, **kwargs)
159+
return cls.from_client_config(client_config, scopes=scopes, **kwargs)
146160

147161
@property
148162
def redirect_uri(self):
@@ -226,18 +240,8 @@ def credentials(self):
226240
Raises:
227241
ValueError: If there is no access token in the session.
228242
"""
229-
if not self.oauth2session.token:
230-
raise ValueError(
231-
'There is no access token for this session, did you call '
232-
'fetch_token?')
233-
234-
return google.oauth2.credentials.Credentials(
235-
self.oauth2session.token['access_token'],
236-
refresh_token=self.oauth2session.token['refresh_token'],
237-
token_uri=self.client_config['token_uri'],
238-
client_id=self.client_config['client_id'],
239-
client_secret=self.client_config['client_secret'],
240-
scopes=self.oauth2session.scope)
243+
return google.oauth2.oauthlib.credentials_from_session(
244+
self.oauth2session, self.client_config)
241245

242246
def authorized_session(self):
243247
"""Returns a :class:`requests.Session` authorized with credentials.

google/oauth2/oauthlib.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Integration with oauthlib
16+
17+
.. warning::
18+
This module is experimental and is subject to change signficantly
19+
within major version releases.
20+
21+
This module provides helpers for integrating with `requests-oauthlib`_.
22+
Typically, you'll want to use the higher-level helpers in
23+
:mod:`google.oauth2.flow`.
24+
25+
.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/
26+
"""
27+
28+
import json
29+
30+
import requests_oauthlib
31+
32+
import google.oauth2.credentials
33+
34+
_REQUIRED_CONFIG_KEYS = frozenset(('auth_uri', 'token_uri', 'client_id'))
35+
36+
37+
def session_from_client_config(client_config, scopes, **kwargs):
38+
"""Creates a :class:`requests_oauthlib.OAuth2Session` from client
39+
configuration loaded from a Google-format client secrets file.
40+
41+
Args:
42+
client_config (Mapping[str, Any]): The client
43+
configuration in the Google `client secrets`_ format.
44+
scopes (Sequence[str]): The list of scopes to request during the
45+
flow.
46+
kwargs: Any additional parameters passed to
47+
:class:`requests_oauthlib.OAuth2Session`
48+
49+
Raises:
50+
ValueError: If the client configuration is not in the correct
51+
format.
52+
53+
Returns:
54+
Tuple[requests_oauthlib.OAuth2Session, Mapping[str, Any]]: The new
55+
oauthlib session and the validated client configuration.
56+
57+
.. _client secrets:
58+
https://developers.google.com/api-client-library/python/guide
59+
/aaa_client_secrets
60+
"""
61+
62+
if 'web' in client_config:
63+
config = client_config['web']
64+
elif 'installed' in client_config:
65+
config = client_config['installed']
66+
else:
67+
raise ValueError(
68+
'Client secrets must be for a web or installed app.')
69+
70+
if not _REQUIRED_CONFIG_KEYS.issubset(config.keys()):
71+
raise ValueError('Client secrets is not in the correct format.')
72+
73+
session = requests_oauthlib.OAuth2Session(
74+
client_id=config['client_id'],
75+
scope=scopes,
76+
**kwargs)
77+
78+
return session, client_config
79+
80+
81+
def session_from_client_secrets_file(client_secrets_file, scopes, **kwargs):
82+
"""Creates a :class:`requests_oauthlib.OAuth2Session` instance from a
83+
Google-format client secrets file.
84+
85+
Args:
86+
client_secrets_file (str): The path to the `client secrets`_ .json
87+
file.
88+
scopes (Sequence[str]): The list of scopes to request during the
89+
flow.
90+
kwargs: Any additional parameters passed to
91+
:class:`requests_oauthlib.OAuth2Session`
92+
93+
Returns:
94+
Tuple[requests_oauthlib.OAuth2Session, Mapping[str, Any]]: The new
95+
oauthlib session and the validated client configuration.
96+
97+
.. _client secrets:
98+
https://developers.google.com/api-client-library/python/guide
99+
/aaa_client_secrets
100+
"""
101+
with open(client_secrets_file, 'r') as json_file:
102+
client_config = json.load(json_file)
103+
104+
return session_from_client_config(client_config, scopes, **kwargs)
105+
106+
107+
def credentials_from_session(session, client_config=None):
108+
"""Creates :class:`google.oauth2.credentials.Credentials` from a
109+
:class:`requests_oauthlib.OAuth2Session`.
110+
111+
:meth:`fetch_token` must be called on the session before before calling
112+
this. This uses the session's auth token and the provided client
113+
configuration to create :class:`google.oauth2.credentials.Credentials`.
114+
This allows you to use the credentials from the session with Google
115+
API client libraries.
116+
117+
Args:
118+
session (requests_oauthlib.OAuth2Session): The OAuth 2.0 session.
119+
client_config (Mapping[str, Any]): The subset of the client
120+
configuration to use. For example, if you have a web client
121+
you would pass in `client_config['web']`.
122+
123+
Returns:
124+
google.oauth2.credentials.Credentials: The constructed credentials.
125+
126+
Raises:
127+
ValueError: If there is no access token in the session.
128+
"""
129+
client_config = client_config if client_config is not None else {}
130+
131+
if not session.token:
132+
raise ValueError(
133+
'There is no access token for this session, did you call '
134+
'fetch_token?')
135+
136+
return google.oauth2.credentials.Credentials(
137+
session.token['access_token'],
138+
refresh_token=session.token.get('refresh_token'),
139+
token_uri=client_config.get('token_uri'),
140+
client_id=client_config.get('client_id'),
141+
client_secret=client_config.get('client_secret'),
142+
scopes=session.scope)

tests/oauth2/test_flow.py

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,44 +27,34 @@
2727
CLIENT_SECRETS_INFO = json.load(fh)
2828

2929

30-
def test_constructor_web():
31-
instance = flow.Flow(CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes)
30+
def test_from_client_secrets_file():
31+
instance = flow.Flow.from_client_secrets_file(
32+
CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes)
3233
assert instance.client_config == CLIENT_SECRETS_INFO['web']
3334
assert (instance.oauth2session.client_id ==
3435
CLIENT_SECRETS_INFO['web']['client_id'])
3536
assert instance.oauth2session.scope == mock.sentinel.scopes
3637

3738

38-
def test_constructor_installed():
39-
info = {'installed': CLIENT_SECRETS_INFO['web']}
40-
instance = flow.Flow(info, scopes=mock.sentinel.scopes)
41-
assert instance.client_config == info['installed']
42-
assert instance.oauth2session.client_id == info['installed']['client_id']
39+
def test_from_client_config_installed():
40+
client_config = {'installed': CLIENT_SECRETS_INFO['web']}
41+
instance = flow.Flow.from_client_config(
42+
client_config, scopes=mock.sentinel.scopes)
43+
assert instance.client_config == client_config['installed']
44+
assert (instance.oauth2session.client_id ==
45+
client_config['installed']['client_id'])
4346
assert instance.oauth2session.scope == mock.sentinel.scopes
4447

4548

46-
def test_constructor_bad_format():
47-
with pytest.raises(ValueError):
48-
flow.Flow({}, scopes=[])
49-
50-
51-
def test_constructor_missing_keys():
49+
def test_from_client_config_bad_format():
5250
with pytest.raises(ValueError):
53-
flow.Flow({'web': {}}, scopes=[])
54-
55-
56-
def test_from_client_secrets_file():
57-
instance = flow.Flow.from_client_secrets_file(
58-
CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes)
59-
assert instance.client_config == CLIENT_SECRETS_INFO['web']
60-
assert (instance.oauth2session.client_id ==
61-
CLIENT_SECRETS_INFO['web']['client_id'])
62-
assert instance.oauth2session.scope == mock.sentinel.scopes
51+
flow.Flow.from_client_config({}, scopes=mock.sentinel.scopes)
6352

6453

6554
@pytest.fixture
6655
def instance():
67-
yield flow.Flow(CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes)
56+
yield flow.Flow.from_client_config(
57+
CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes)
6858

6959

7060
def test_redirect_uri(instance):
@@ -123,11 +113,6 @@ def test_credentials(instance):
123113
assert credentials._token_uri == CLIENT_SECRETS_INFO['web']['token_uri']
124114

125115

126-
def test_bad_credentials(instance):
127-
with pytest.raises(ValueError):
128-
assert instance.credentials
129-
130-
131116
def test_authorized_session(instance):
132117
instance.oauth2session.token = {
133118
'access_token': mock.sentinel.access_token,

0 commit comments

Comments
 (0)