blob: 69e73ffd7d6eb35945f3c43d705607db857cea04 [file] [log] [blame]
Jon Wayne Parrott4382bc12017-01-10 13:35:51 -08001# Copyright 2016 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"""OAuth 2.0 Authorization Flow
16
17This module provides integration with `requests-oauthlib`_ for running the
18`OAuth 2.0 Authorization Flow`_ and acquiring user credentials.
19
20Here's an example of using the flow with the installed application
21authorization flow::
22
23 import google.oauth2.flow
24
25 # Create the flow using the client secrets file from the Google API
26 # Console.
27 flow = google.oauth2.flow.Flow.from_client_secrets_file(
28 'path/to/client_secrets.json',
29 scopes=['profile', 'email'],
30 redirect_uri='urn:ietf:wg:oauth:2.0:oob')
31
32 # Tell the user to go to the authorization URL.
33 auth_url, _ = flow.authorization_url(prompt='consent')
34
35 print('Please go to this URL: {}'.format(auth_url))
36
37 # The user will get an authorization code. This code is used to get the
38 # access token.
39 code = input('Enter the authorization code: ')
40 flow.fetch_token(code=code)
41
42 # You can use flow.credentials, or you can just get a requests session
43 # using flow.authorized_session.
44 session = flow.authorized_session()
45 print(session.get('https://www.googleapis.com/userinfo/v2/me').json())
46
47.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/
48.. _OAuth 2.0 Authorization Flow:
49 https://tools.ietf.org/html/rfc6749#section-1.2
50"""
51
52import json
53
54import requests_oauthlib
55
56import google.auth.transport.requests
57import google.oauth2.credentials
58
59_REQUIRED_CONFIG_KEYS = frozenset(('auth_uri', 'token_uri', 'client_id'))
60
61
62class Flow(object):
63 """OAuth 2.0 Authorization Flow
64
65 This class uses a :class:`requests_oauthlib.OAuth2Session` instance at
66 :attr:`oauth2session` to perform all of the OAuth 2.0 logic. This class
67 just provides convenience methods and sane defaults for doing Google's
68 particular flavors of OAuth 2.0.
69
70 Typically you'll construct an instance of this flow using
71 :meth:`from_client_secrets_file` and a `client secrets file`_ obtained
72 from the `Google API Console`_.
73
74 .. _client secrets file:
75 https://developers.google.com/identity/protocols/OAuth2WebServer
76 #creatingcred
77 .. _Google API Console:
78 https://console.developers.google.com/apis/credentials
79 """
80
81 def __init__(self, client_config, scopes, **kwargs):
82 """
83 Args:
84 client_config (Mapping[str, Any]): The client
85 configuration in the Google `client secrets`_ format.
86 scopes (Sequence[str]): The list of scopes to request during the
87 flow.
88 kwargs: Any additional parameters passed to
89 :class:`requests_oauthlib.OAuth2Session`
90
91 Raises:
92 ValueError: If the client configuration is not in the correct
93 format.
94
95 .. _client secrets:
96 https://developers.google.com/api-client-library/python/guide
97 /aaa_client_secrets
98 """
99 self.client_config = None
100 """Mapping[str, Any]: The OAuth 2.0 client configuration."""
101 self.client_type = None
102 """str: The client type, either ``'web'`` or ``'installed'``"""
103
104 if 'web' in client_config:
105 self.client_config = client_config['web']
106 self.client_type = 'web'
107 elif 'installed' in client_config:
108 self.client_config = client_config['installed']
109 self.client_type = 'installed'
110 else:
111 raise ValueError(
112 'Client secrets must be for a web or installed app.')
113
114 if not _REQUIRED_CONFIG_KEYS.issubset(self.client_config.keys()):
115 raise ValueError('Client secrets is not in the correct format.')
116
117 self.oauth2session = requests_oauthlib.OAuth2Session(
118 client_id=self.client_config['client_id'],
119 scope=scopes,
120 **kwargs)
121 """requests_oauthlib.OAuth2Session: The OAuth 2.0 session."""
122
123 @classmethod
124 def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
125 """Creates a :class:`Flow` instance from a Google client secrets file.
126
127 Args:
128 client_secrets_file (str): The path to the client secrets .json
129 file.
130 scopes (Sequence[str]): The list of scopes to request during the
131 flow.
132 kwargs: Any additional parameters passed to
133 :class:`requests_oauthlib.OAuth2Session`
134
135 Returns:
136 Flow: The constructed Flow instance.
137 """
138 with open(client_secrets_file, 'r') as json_file:
139 client_config = json.load(json_file)
140
141 return cls(client_config, scopes=scopes, **kwargs)
142
143 @property
144 def redirect_uri(self):
145 """The OAuth 2.0 redirect URI. Pass-through to
146 ``self.oauth2session.redirect_uri``."""
147 return self.oauth2session.redirect_uri
148
149 @redirect_uri.setter
150 def redirect_uri(self, value):
151 self.oauth2session.redirect_uri = value
152
153 def authorization_url(self, **kwargs):
154 """Generates an authorization URL.
155
156 This is the first step in the OAuth 2.0 Authorization Flow. The user's
157 browser should be redirected to the returned URL.
158
159 This method calls
160 :meth:`requests_oauthlib.OAuth2Session.authorization_url`
161 and specifies the client configuration's authorization URI (usually
162 Google's authorization server) and specifies that "offline" access is
163 desired. This is required in order to obtain a refresh token.
164
165 Args:
166 kwargs: Additional arguments passed through to
167 :meth:`requests_oauthlib.OAuth2Session.authorization_url`
168
169 Returns:
170 Tuple[str, str]: The generated authorization URL and state. The
171 user must visit the URL to complete the flow. The state is used
172 when completing the flow to verify that the request originated
173 from your application. If your application is using a different
174 :class:`Flow` instance to obtain the token, you will need to
175 specify the ``state`` when constructing the :class:`Flow`.
176 """
177 url, state = self.oauth2session.authorization_url(
178 self.client_config['auth_uri'],
179 access_type='offline', **kwargs)
180
181 return url, state
182
183 def fetch_token(self, **kwargs):
184 """Completes the Authorization Flow and obtains an access token.
185
186 This is the final step in the OAuth 2.0 Authorization Flow. This is
187 called after the user consents.
188
189 This method calls
190 :meth:`requests_oauthlib.OAuth2Session.fetch_token`
191 and specifies the client configuration's token URI (usually Google's
192 token server).
193
194 Args:
195 kwargs: Arguments passed through to
196 :meth:`requests_oauthlib.OAuth2Session.fetch_token`. At least
197 one of ``code`` or ``authorization_response`` must be
198 specified.
199
200 Returns:
201 Mapping[str, str]: The obtained tokens. Typically, you will not use
202 return value of this function and instead and use
203 :meth:`credentials` to obtain a
204 :class:`~google.auth.credentials.Credentials` instance.
205 """
206 return self.oauth2session.fetch_token(
207 self.client_config['token_uri'],
208 client_secret=self.client_config['client_secret'],
209 **kwargs)
210
211 @property
212 def credentials(self):
213 """Returns credentials from the OAuth 2.0 session.
214
215 :meth:`fetch_token` must be called before accessing this. This method
216 constructs a :class:`google.oauth2.credentials.Credentials` class using
217 the session's token and the client config.
218
219 Returns:
220 google.oauth2.credentials.Credentials: The constructed credentials.
221
222 Raises:
223 ValueError: If there is no access token in the session.
224 """
225 if not self.oauth2session.token:
226 raise ValueError(
227 'There is no access token for this session, did you call '
228 'fetch_token?')
229
230 return google.oauth2.credentials.Credentials(
231 self.oauth2session.token['access_token'],
232 refresh_token=self.oauth2session.token['refresh_token'],
233 token_uri=self.client_config['token_uri'],
234 client_id=self.client_config['client_id'],
235 client_secret=self.client_config['client_secret'],
236 scopes=self.oauth2session.scope)
237
238 def authorized_session(self):
239 """Returns a :class:`requests.Session` authorized with credentials.
240
241 :meth:`fetch_token` must be called before this method. This method
242 constructs a :class:`google.auth.transport.requests.AuthorizedSession`
243 class using this flow's :attr:`credentials`.
244
245 Returns:
246 google.auth.transport.requests.AuthorizedSession: The constructed
247 session.
248 """
249 return google.auth.transport.requests.AuthorizedSession(
250 self.credentials)