OLD | NEW |
1 # Copyright 2014 Google Inc. All rights reserved. | 1 # Copyright (C) 2010 Google Inc. |
2 # | 2 # |
3 # Licensed under the Apache License, Version 2.0 (the "License"); | 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
4 # you may not use this file except in compliance with 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 | 5 # You may obtain a copy of the License at |
6 # | 6 # |
7 # http://www.apache.org/licenses/LICENSE-2.0 | 7 # http://www.apache.org/licenses/LICENSE-2.0 |
8 # | 8 # |
9 # Unless required by applicable law or agreed to in writing, software | 9 # Unless required by applicable law or agreed to in writing, software |
10 # distributed under the License is distributed on an "AS IS" BASIS, | 10 # distributed under the License is distributed on an "AS IS" BASIS, |
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 # See the License for the specific language governing permissions and | 12 # See the License for the specific language governing permissions and |
13 # limitations under the License. | 13 # limitations under the License. |
14 | 14 |
15 """An OAuth 2.0 client. | 15 """An OAuth 2.0 client. |
16 | 16 |
17 Tools for interacting with OAuth 2.0 protected resources. | 17 Tools for interacting with OAuth 2.0 protected resources. |
18 """ | 18 """ |
19 | 19 |
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)' | 20 __author__ = 'jcgregorio@google.com (Joe Gregorio)' |
21 | 21 |
22 import base64 | 22 import base64 |
23 import collections | 23 import clientsecrets |
24 import copy | 24 import copy |
25 import datetime | 25 import datetime |
26 import json | 26 from .. import httplib2 |
27 import logging | 27 import logging |
28 import os | |
29 import socket | |
30 import sys | 28 import sys |
31 import tempfile | |
32 import time | 29 import time |
33 import shutil | 30 import urllib |
| 31 import urlparse |
34 | 32 |
35 from .. import httplib2 | |
36 from . import clientsecrets | |
37 from . import GOOGLE_AUTH_URI | 33 from . import GOOGLE_AUTH_URI |
38 from . import GOOGLE_DEVICE_URI | |
39 from . import GOOGLE_REVOKE_URI | 34 from . import GOOGLE_REVOKE_URI |
40 from . import GOOGLE_TOKEN_URI | 35 from . import GOOGLE_TOKEN_URI |
41 from . import util | 36 from . import util |
42 from third_party import six | 37 from .anyjson import simplejson |
43 from third_party.six.moves import urllib | |
44 | 38 |
45 HAS_OPENSSL = False | 39 HAS_OPENSSL = False |
46 HAS_CRYPTO = False | 40 HAS_CRYPTO = False |
47 try: | 41 try: |
48 from oauth2client import crypt | 42 from . import crypt |
49 HAS_CRYPTO = True | 43 HAS_CRYPTO = True |
50 if crypt.OpenSSLVerifier is not None: | 44 if crypt.OpenSSLVerifier is not None: |
51 HAS_OPENSSL = True | 45 HAS_OPENSSL = True |
52 except ImportError: | 46 except ImportError: |
53 pass | 47 pass |
54 | 48 |
| 49 try: |
| 50 from urlparse import parse_qsl |
| 51 except ImportError: |
| 52 from cgi import parse_qsl |
| 53 |
55 logger = logging.getLogger(__name__) | 54 logger = logging.getLogger(__name__) |
56 | 55 |
57 # Expiry is stored in RFC3339 UTC format | 56 # Expiry is stored in RFC3339 UTC format |
58 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' | 57 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' |
59 | 58 |
60 # Which certs to use to validate id_tokens received. | 59 # Which certs to use to validate id_tokens received. |
61 ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' | 60 ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' |
62 # This symbol previously had a typo in the name; we keep the old name | |
63 # around for now, but will remove it in the future. | |
64 ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS | |
65 | 61 |
66 # Constant to use for the out of band OAuth 2.0 flow. | 62 # Constant to use for the out of band OAuth 2.0 flow. |
67 OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' | 63 OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' |
68 | 64 |
69 # Google Data client libraries may need to set this to [401, 403]. | 65 # Google Data client libraries may need to set this to [401, 403]. |
70 REFRESH_STATUS_CODES = [401] | 66 REFRESH_STATUS_CODES = [401] |
71 | 67 |
72 # The value representing user credentials. | |
73 AUTHORIZED_USER = 'authorized_user' | |
74 | |
75 # The value representing service account credentials. | |
76 SERVICE_ACCOUNT = 'service_account' | |
77 | |
78 # The environment variable pointing the file with local | |
79 # Application Default Credentials. | |
80 GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS' | |
81 # The ~/.config subdirectory containing gcloud credentials. Intended | |
82 # to be swapped out in tests. | |
83 _CLOUDSDK_CONFIG_DIRECTORY = 'gcloud' | |
84 | |
85 # The error message we show users when we can't find the Application | |
86 # Default Credentials. | |
87 ADC_HELP_MSG = ( | |
88 'The Application Default Credentials are not available. They are available ' | |
89 'if running in Google Compute Engine. Otherwise, the environment variable ' | |
90 + GOOGLE_APPLICATION_CREDENTIALS + | |
91 ' must be defined pointing to a file defining the credentials. See ' | |
92 'https://developers.google.com/accounts/docs/application-default-credentials
' # pylint:disable=line-too-long | |
93 ' for more information.') | |
94 | |
95 # The access token along with the seconds in which it expires. | |
96 AccessTokenInfo = collections.namedtuple( | |
97 'AccessTokenInfo', ['access_token', 'expires_in']) | |
98 | |
99 DEFAULT_ENV_NAME = 'UNKNOWN' | |
100 | |
101 # If set to True _get_environment avoid GCE check (_detect_gce_environment) | |
102 NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False') | |
103 | |
104 class SETTINGS(object): | |
105 """Settings namespace for globally defined values.""" | |
106 env_name = None | |
107 | |
108 | 68 |
109 class Error(Exception): | 69 class Error(Exception): |
110 """Base error for this module.""" | 70 """Base error for this module.""" |
111 | 71 |
112 | 72 |
113 class FlowExchangeError(Error): | 73 class FlowExchangeError(Error): |
114 """Error trying to exchange an authorization grant for an access token.""" | 74 """Error trying to exchange an authorization grant for an access token.""" |
115 | 75 |
116 | 76 |
117 class AccessTokenRefreshError(Error): | 77 class AccessTokenRefreshError(Error): |
118 """Error trying to refresh an expired access token.""" | 78 """Error trying to refresh an expired access token.""" |
119 | 79 |
120 | 80 |
121 class TokenRevokeError(Error): | 81 class TokenRevokeError(Error): |
122 """Error trying to revoke a token.""" | 82 """Error trying to revoke a token.""" |
123 | 83 |
124 | 84 |
125 class UnknownClientSecretsFlowError(Error): | 85 class UnknownClientSecretsFlowError(Error): |
126 """The client secrets file called for an unknown type of OAuth 2.0 flow. """ | 86 """The client secrets file called for an unknown type of OAuth 2.0 flow. """ |
127 | 87 |
128 | 88 |
129 class AccessTokenCredentialsError(Error): | 89 class AccessTokenCredentialsError(Error): |
130 """Having only the access_token means no refresh is possible.""" | 90 """Having only the access_token means no refresh is possible.""" |
131 | 91 |
132 | 92 |
133 class VerifyJwtTokenError(Error): | 93 class VerifyJwtTokenError(Error): |
134 """Could not retrieve certificates for validation.""" | 94 """Could on retrieve certificates for validation.""" |
135 | 95 |
136 | 96 |
137 class NonAsciiHeaderError(Error): | 97 class NonAsciiHeaderError(Error): |
138 """Header names and values must be ASCII strings.""" | 98 """Header names and values must be ASCII strings.""" |
139 | 99 |
140 | 100 |
141 class ApplicationDefaultCredentialsError(Error): | |
142 """Error retrieving the Application Default Credentials.""" | |
143 | |
144 | |
145 class OAuth2DeviceCodeError(Error): | |
146 """Error trying to retrieve a device code.""" | |
147 | |
148 | |
149 class CryptoUnavailableError(Error, NotImplementedError): | |
150 """Raised when a crypto library is required, but none is available.""" | |
151 | |
152 | |
153 def _abstract(): | 101 def _abstract(): |
154 raise NotImplementedError('You need to override this function') | 102 raise NotImplementedError('You need to override this function') |
155 | 103 |
156 | 104 |
157 class MemoryCache(object): | 105 class MemoryCache(object): |
158 """httplib2 Cache implementation which only caches locally.""" | 106 """httplib2 Cache implementation which only caches locally.""" |
159 | 107 |
160 def __init__(self): | 108 def __init__(self): |
161 self.cache = {} | 109 self.cache = {} |
162 | 110 |
163 def get(self, key): | 111 def get(self, key): |
164 return self.cache.get(key) | 112 return self.cache.get(key) |
165 | 113 |
166 def set(self, key, value): | 114 def set(self, key, value): |
167 self.cache[key] = value | 115 self.cache[key] = value |
168 | 116 |
169 def delete(self, key): | 117 def delete(self, key): |
170 self.cache.pop(key, None) | 118 self.cache.pop(key, None) |
171 | 119 |
172 | 120 |
173 class Credentials(object): | 121 class Credentials(object): |
174 """Base class for all Credentials objects. | 122 """Base class for all Credentials objects. |
175 | 123 |
176 Subclasses must define an authorize() method that applies the credentials to | 124 Subclasses must define an authorize() method that applies the credentials to |
177 an HTTP transport. | 125 an HTTP transport. |
178 | 126 |
179 Subclasses must also specify a classmethod named 'from_json' that takes a JSON | 127 Subclasses must also specify a classmethod named 'from_json' that takes a JSON |
180 string as input and returns an instantiated Credentials object. | 128 string as input and returns an instaniated Credentials object. |
181 """ | 129 """ |
182 | 130 |
183 NON_SERIALIZED_MEMBERS = ['store'] | 131 NON_SERIALIZED_MEMBERS = ['store'] |
184 | 132 |
185 | |
186 def authorize(self, http): | 133 def authorize(self, http): |
187 """Take an httplib2.Http instance (or equivalent) and authorizes it. | 134 """Take an httplib2.Http instance (or equivalent) and authorizes it. |
188 | 135 |
189 Authorizes it for the set of credentials, usually by replacing | 136 Authorizes it for the set of credentials, usually by replacing |
190 http.request() with a method that adds in the appropriate headers and then | 137 http.request() with a method that adds in the appropriate headers and then |
191 delegates to the original Http.request() method. | 138 delegates to the original Http.request() method. |
192 | 139 |
193 Args: | 140 Args: |
194 http: httplib2.Http, an http object to be used to make the refresh | 141 http: httplib2.Http, an http object to be used to make the refresh |
195 request. | 142 request. |
196 """ | 143 """ |
197 _abstract() | 144 _abstract() |
198 | 145 |
199 | |
200 def refresh(self, http): | 146 def refresh(self, http): |
201 """Forces a refresh of the access_token. | 147 """Forces a refresh of the access_token. |
202 | 148 |
203 Args: | 149 Args: |
204 http: httplib2.Http, an http object to be used to make the refresh | 150 http: httplib2.Http, an http object to be used to make the refresh |
205 request. | 151 request. |
206 """ | 152 """ |
207 _abstract() | 153 _abstract() |
208 | 154 |
209 | |
210 def revoke(self, http): | 155 def revoke(self, http): |
211 """Revokes a refresh_token and makes the credentials void. | 156 """Revokes a refresh_token and makes the credentials void. |
212 | 157 |
213 Args: | 158 Args: |
214 http: httplib2.Http, an http object to be used to make the revoke | 159 http: httplib2.Http, an http object to be used to make the revoke |
215 request. | 160 request. |
216 """ | 161 """ |
217 _abstract() | 162 _abstract() |
218 | 163 |
219 | |
220 def apply(self, headers): | 164 def apply(self, headers): |
221 """Add the authorization to the headers. | 165 """Add the authorization to the headers. |
222 | 166 |
223 Args: | 167 Args: |
224 headers: dict, the headers to add the Authorization header to. | 168 headers: dict, the headers to add the Authorization header to. |
225 """ | 169 """ |
226 _abstract() | 170 _abstract() |
227 | 171 |
228 def _to_json(self, strip): | 172 def _to_json(self, strip): |
229 """Utility function that creates JSON repr. of a Credentials object. | 173 """Utility function that creates JSON repr. of a Credentials object. |
230 | 174 |
231 Args: | 175 Args: |
232 strip: array, An array of names of members to not include in the JSON. | 176 strip: array, An array of names of members to not include in the JSON. |
233 | 177 |
234 Returns: | 178 Returns: |
235 string, a JSON representation of this instance, suitable to pass to | 179 string, a JSON representation of this instance, suitable to pass to |
236 from_json(). | 180 from_json(). |
237 """ | 181 """ |
238 t = type(self) | 182 t = type(self) |
239 d = copy.copy(self.__dict__) | 183 d = copy.copy(self.__dict__) |
240 for member in strip: | 184 for member in strip: |
241 if member in d: | 185 if member in d: |
242 del d[member] | 186 del d[member] |
243 if (d.get('token_expiry') and | 187 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): |
244 isinstance(d['token_expiry'], datetime.datetime)): | |
245 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) | 188 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) |
246 # Add in information we will need later to reconsistitue this instance. | 189 # Add in information we will need later to reconsistitue this instance. |
247 d['_class'] = t.__name__ | 190 d['_class'] = t.__name__ |
248 d['_module'] = t.__module__ | 191 d['_module'] = t.__module__ |
249 for key, val in d.items(): | 192 return simplejson.dumps(d) |
250 if isinstance(val, bytes): | |
251 d[key] = val.decode('utf-8') | |
252 return json.dumps(d) | |
253 | 193 |
254 def to_json(self): | 194 def to_json(self): |
255 """Creating a JSON representation of an instance of Credentials. | 195 """Creating a JSON representation of an instance of Credentials. |
256 | 196 |
257 Returns: | 197 Returns: |
258 string, a JSON representation of this instance, suitable to pass to | 198 string, a JSON representation of this instance, suitable to pass to |
259 from_json(). | 199 from_json(). |
260 """ | 200 """ |
261 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) | 201 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) |
262 | 202 |
263 @classmethod | 203 @classmethod |
264 def new_from_json(cls, s): | 204 def new_from_json(cls, s): |
265 """Utility class method to instantiate a Credentials subclass from a JSON | 205 """Utility class method to instantiate a Credentials subclass from a JSON |
266 representation produced by to_json(). | 206 representation produced by to_json(). |
267 | 207 |
268 Args: | 208 Args: |
269 s: string, JSON from to_json(). | 209 s: string, JSON from to_json(). |
270 | 210 |
271 Returns: | 211 Returns: |
272 An instance of the subclass of Credentials that was serialized with | 212 An instance of the subclass of Credentials that was serialized with |
273 to_json(). | 213 to_json(). |
274 """ | 214 """ |
275 if six.PY3 and isinstance(s, bytes): | 215 data = simplejson.loads(s) |
276 s = s.decode('utf-8') | |
277 data = json.loads(s) | |
278 # Find and call the right classmethod from_json() to restore the object. | 216 # Find and call the right classmethod from_json() to restore the object. |
279 module = data['_module'] | 217 module = data['_module'] |
280 try: | 218 try: |
281 m = __import__(module) | 219 m = __import__(module) |
282 except ImportError: | 220 except ImportError: |
283 # In case there's an object from the old package structure, update it | 221 # In case there's an object from the old package structure, update it |
284 module = module.replace('.googleapiclient', '') | 222 module = module.replace('.apiclient', '') |
285 m = __import__(module) | 223 m = __import__(module) |
286 | 224 |
287 m = __import__(module, fromlist=module.split('.')[:-1]) | 225 m = __import__(module, fromlist=module.split('.')[:-1]) |
288 kls = getattr(m, data['_class']) | 226 kls = getattr(m, data['_class']) |
289 from_json = getattr(kls, 'from_json') | 227 from_json = getattr(kls, 'from_json') |
290 return from_json(s) | 228 return from_json(s) |
291 | 229 |
292 @classmethod | 230 @classmethod |
293 def from_json(cls, unused_data): | 231 def from_json(cls, s): |
294 """Instantiate a Credentials object from a JSON description of it. | 232 """Instantiate a Credentials object from a JSON description of it. |
295 | 233 |
296 The JSON should have been produced by calling .to_json() on the object. | 234 The JSON should have been produced by calling .to_json() on the object. |
297 | 235 |
298 Args: | 236 Args: |
299 unused_data: dict, A deserialized JSON object. | 237 data: dict, A deserialized JSON object. |
300 | 238 |
301 Returns: | 239 Returns: |
302 An instance of a Credentials subclass. | 240 An instance of a Credentials subclass. |
303 """ | 241 """ |
304 return Credentials() | 242 return Credentials() |
305 | 243 |
306 | 244 |
307 class Flow(object): | 245 class Flow(object): |
308 """Base class for all Flow objects.""" | 246 """Base class for all Flow objects.""" |
309 pass | 247 pass |
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
411 contatenate to a binary request body may result in a unicode decode error. | 349 contatenate to a binary request body may result in a unicode decode error. |
412 | 350 |
413 Args: | 351 Args: |
414 headers: dict, A dictionary of headers. | 352 headers: dict, A dictionary of headers. |
415 | 353 |
416 Returns: | 354 Returns: |
417 The same dictionary but with all the keys converted to strings. | 355 The same dictionary but with all the keys converted to strings. |
418 """ | 356 """ |
419 clean = {} | 357 clean = {} |
420 try: | 358 try: |
421 for k, v in six.iteritems(headers): | 359 for k, v in headers.iteritems(): |
422 clean_k = k if isinstance(k, bytes) else str(k).encode('ascii') | 360 clean[str(k)] = str(v) |
423 clean_v = v if isinstance(v, bytes) else str(v).encode('ascii') | |
424 clean[clean_k] = clean_v | |
425 except UnicodeEncodeError: | 361 except UnicodeEncodeError: |
426 raise NonAsciiHeaderError(k + ': ' + v) | 362 raise NonAsciiHeaderError(k + ': ' + v) |
427 return clean | 363 return clean |
428 | 364 |
429 | 365 |
430 def _update_query_params(uri, params): | 366 def _update_query_params(uri, params): |
431 """Updates a URI with new query parameters. | 367 """Updates a URI with new query parameters. |
432 | 368 |
433 Args: | 369 Args: |
434 uri: string, A valid URI, with potential existing query parameters. | 370 uri: string, A valid URI, with potential existing query parameters. |
435 params: dict, A dictionary of query parameters. | 371 params: dict, A dictionary of query parameters. |
436 | 372 |
437 Returns: | 373 Returns: |
438 The same URI but with the new query parameters added. | 374 The same URI but with the new query parameters added. |
439 """ | 375 """ |
440 parts = urllib.parse.urlparse(uri) | 376 parts = list(urlparse.urlparse(uri)) |
441 query_params = dict(urllib.parse.parse_qsl(parts.query)) | 377 query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part |
442 query_params.update(params) | 378 query_params.update(params) |
443 new_parts = parts._replace(query=urllib.parse.urlencode(query_params)) | 379 parts[4] = urllib.urlencode(query_params) |
444 return urllib.parse.urlunparse(new_parts) | 380 return urlparse.urlunparse(parts) |
445 | 381 |
446 | 382 |
447 class OAuth2Credentials(Credentials): | 383 class OAuth2Credentials(Credentials): |
448 """Credentials object for OAuth 2.0. | 384 """Credentials object for OAuth 2.0. |
449 | 385 |
450 Credentials can be applied to an httplib2.Http object using the authorize() | 386 Credentials can be applied to an httplib2.Http object using the authorize() |
451 method, which then adds the OAuth 2.0 access token to each request. | 387 method, which then adds the OAuth 2.0 access token to each request. |
452 | 388 |
453 OAuth2Credentials objects may be safely pickled and unpickled. | 389 OAuth2Credentials objects may be safely pickled and unpickled. |
454 """ | 390 """ |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
502 def authorize(self, http): | 438 def authorize(self, http): |
503 """Authorize an httplib2.Http instance with these credentials. | 439 """Authorize an httplib2.Http instance with these credentials. |
504 | 440 |
505 The modified http.request method will add authentication headers to each | 441 The modified http.request method will add authentication headers to each |
506 request and will refresh access_tokens when a 401 is received on a | 442 request and will refresh access_tokens when a 401 is received on a |
507 request. In addition the http.request method has a credentials property, | 443 request. In addition the http.request method has a credentials property, |
508 http.request.credentials, which is the Credentials object that authorized | 444 http.request.credentials, which is the Credentials object that authorized |
509 it. | 445 it. |
510 | 446 |
511 Args: | 447 Args: |
512 http: An instance of ``httplib2.Http`` or something that acts | 448 http: An instance of httplib2.Http |
513 like it. | 449 or something that acts like it. |
514 | 450 |
515 Returns: | 451 Returns: |
516 A modified instance of http that was passed in. | 452 A modified instance of http that was passed in. |
517 | 453 |
518 Example:: | 454 Example: |
519 | 455 |
520 h = httplib2.Http() | 456 h = httplib2.Http() |
521 h = credentials.authorize(h) | 457 h = credentials.authorize(h) |
522 | 458 |
523 You can't create a new OAuth subclass of httplib2.Authentication | 459 You can't create a new OAuth subclass of httplib2.Authenication |
524 because it never gets passed the absolute URI, which is needed for | 460 because it never gets passed the absolute URI, which is needed for |
525 signing. So instead we have to overload 'request' with a closure | 461 signing. So instead we have to overload 'request' with a closure |
526 that adds in the Authorization header and then calls the original | 462 that adds in the Authorization header and then calls the original |
527 version of 'request()'. | 463 version of 'request()'. |
528 | |
529 """ | 464 """ |
530 request_orig = http.request | 465 request_orig = http.request |
531 | 466 |
532 # The closure that will replace 'httplib2.Http.request'. | 467 # The closure that will replace 'httplib2.Http.request'. |
533 @util.positional(1) | 468 @util.positional(1) |
534 def new_request(uri, method='GET', body=None, headers=None, | 469 def new_request(uri, method='GET', body=None, headers=None, |
535 redirections=httplib2.DEFAULT_MAX_REDIRECTS, | 470 redirections=httplib2.DEFAULT_MAX_REDIRECTS, |
536 connection_type=None): | 471 connection_type=None): |
537 if not self.access_token: | 472 if not self.access_token: |
538 logger.info('Attempting refresh to obtain initial access_token') | 473 logger.info('Attempting refresh to obtain initial access_token') |
539 self._refresh(request_orig) | 474 self._refresh(request_orig) |
540 | 475 |
541 # Clone and modify the request headers to add the appropriate | 476 # Modify the request headers to add the appropriate |
542 # Authorization header. | 477 # Authorization header. |
543 if headers is None: | 478 if headers is None: |
544 headers = {} | 479 headers = {} |
545 else: | |
546 headers = dict(headers) | |
547 self.apply(headers) | 480 self.apply(headers) |
548 | 481 |
549 if self.user_agent is not None: | 482 if self.user_agent is not None: |
550 if 'user-agent' in headers: | 483 if 'user-agent' in headers: |
551 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] | 484 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] |
552 else: | 485 else: |
553 headers['user-agent'] = self.user_agent | 486 headers['user-agent'] = self.user_agent |
554 | 487 |
555 resp, content = request_orig(uri, method, body, clean_headers(headers), | 488 resp, content = request_orig(uri, method, body, clean_headers(headers), |
556 redirections, connection_type) | 489 redirections, connection_type) |
557 | 490 |
558 if resp.status in REFRESH_STATUS_CODES: | 491 if resp.status in REFRESH_STATUS_CODES: |
559 logger.info('Refreshing due to a %s', resp.status) | 492 logger.info('Refreshing due to a %s' % str(resp.status)) |
560 self._refresh(request_orig) | 493 self._refresh(request_orig) |
561 self.apply(headers) | 494 self.apply(headers) |
562 return request_orig(uri, method, body, clean_headers(headers), | 495 return request_orig(uri, method, body, clean_headers(headers), |
563 redirections, connection_type) | 496 redirections, connection_type) |
564 else: | 497 else: |
565 return (resp, content) | 498 return (resp, content) |
566 | 499 |
567 # Replace the request method with our own closure. | 500 # Replace the request method with our own closure. |
568 http.request = new_request | 501 http.request = new_request |
569 | 502 |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
605 def from_json(cls, s): | 538 def from_json(cls, s): |
606 """Instantiate a Credentials object from a JSON description of it. The JSON | 539 """Instantiate a Credentials object from a JSON description of it. The JSON |
607 should have been produced by calling .to_json() on the object. | 540 should have been produced by calling .to_json() on the object. |
608 | 541 |
609 Args: | 542 Args: |
610 data: dict, A deserialized JSON object. | 543 data: dict, A deserialized JSON object. |
611 | 544 |
612 Returns: | 545 Returns: |
613 An instance of a Credentials subclass. | 546 An instance of a Credentials subclass. |
614 """ | 547 """ |
615 if six.PY3 and isinstance(s, bytes): | 548 data = simplejson.loads(s) |
616 s = s.decode('utf-8') | 549 if 'token_expiry' in data and not isinstance(data['token_expiry'], |
617 data = json.loads(s) | 550 datetime.datetime): |
618 if (data.get('token_expiry') and | |
619 not isinstance(data['token_expiry'], datetime.datetime)): | |
620 try: | 551 try: |
621 data['token_expiry'] = datetime.datetime.strptime( | 552 data['token_expiry'] = datetime.datetime.strptime( |
622 data['token_expiry'], EXPIRY_FORMAT) | 553 data['token_expiry'], EXPIRY_FORMAT) |
623 except ValueError: | 554 except: |
624 data['token_expiry'] = None | 555 data['token_expiry'] = None |
625 retval = cls( | 556 retval = cls( |
626 data['access_token'], | 557 data['access_token'], |
627 data['client_id'], | 558 data['client_id'], |
628 data['client_secret'], | 559 data['client_secret'], |
629 data['refresh_token'], | 560 data['refresh_token'], |
630 data['token_expiry'], | 561 data['token_expiry'], |
631 data['token_uri'], | 562 data['token_uri'], |
632 data['user_agent'], | 563 data['user_agent'], |
633 revoke_uri=data.get('revoke_uri', None), | 564 revoke_uri=data.get('revoke_uri', None), |
(...skipping 14 matching lines...) Expand all Loading... |
648 if not self.token_expiry: | 579 if not self.token_expiry: |
649 return False | 580 return False |
650 | 581 |
651 now = datetime.datetime.utcnow() | 582 now = datetime.datetime.utcnow() |
652 if now >= self.token_expiry: | 583 if now >= self.token_expiry: |
653 logger.info('access_token is expired. Now: %s, token_expiry: %s', | 584 logger.info('access_token is expired. Now: %s, token_expiry: %s', |
654 now, self.token_expiry) | 585 now, self.token_expiry) |
655 return True | 586 return True |
656 return False | 587 return False |
657 | 588 |
658 def get_access_token(self, http=None): | |
659 """Return the access token and its expiration information. | |
660 | |
661 If the token does not exist, get one. | |
662 If the token expired, refresh it. | |
663 """ | |
664 if not self.access_token or self.access_token_expired: | |
665 if not http: | |
666 http = httplib2.Http() | |
667 self.refresh(http) | |
668 return AccessTokenInfo(access_token=self.access_token, | |
669 expires_in=self._expires_in()) | |
670 | |
671 def set_store(self, store): | 589 def set_store(self, store): |
672 """Set the Storage for the credential. | 590 """Set the Storage for the credential. |
673 | 591 |
674 Args: | 592 Args: |
675 store: Storage, an implementation of Storage object. | 593 store: Storage, an implementation of Stroage object. |
676 This is needed to store the latest access_token if it | 594 This is needed to store the latest access_token if it |
677 has expired and been refreshed. This implementation uses | 595 has expired and been refreshed. This implementation uses |
678 locking to check for updates before updating the | 596 locking to check for updates before updating the |
679 access_token. | 597 access_token. |
680 """ | 598 """ |
681 self.store = store | 599 self.store = store |
682 | 600 |
683 def _expires_in(self): | |
684 """Return the number of seconds until this token expires. | |
685 | |
686 If token_expiry is in the past, this method will return 0, meaning the | |
687 token has already expired. | |
688 If token_expiry is None, this method will return None. Note that returning | |
689 0 in such a case would not be fair: the token may still be valid; | |
690 we just don't know anything about it. | |
691 """ | |
692 if self.token_expiry: | |
693 now = datetime.datetime.utcnow() | |
694 if self.token_expiry > now: | |
695 time_delta = self.token_expiry - now | |
696 # TODO(orestica): return time_delta.total_seconds() | |
697 # once dropping support for Python 2.6 | |
698 return time_delta.days * 86400 + time_delta.seconds | |
699 else: | |
700 return 0 | |
701 | |
702 def _updateFromCredential(self, other): | 601 def _updateFromCredential(self, other): |
703 """Update this Credential from another instance.""" | 602 """Update this Credential from another instance.""" |
704 self.__dict__.update(other.__getstate__()) | 603 self.__dict__.update(other.__getstate__()) |
705 | 604 |
706 def __getstate__(self): | 605 def __getstate__(self): |
707 """Trim the state down to something that can be pickled.""" | 606 """Trim the state down to something that can be pickled.""" |
708 d = copy.copy(self.__dict__) | 607 d = copy.copy(self.__dict__) |
709 del d['store'] | 608 del d['store'] |
710 return d | 609 return d |
711 | 610 |
712 def __setstate__(self, state): | 611 def __setstate__(self, state): |
713 """Reconstitute the state of the object from being pickled.""" | 612 """Reconstitute the state of the object from being pickled.""" |
714 self.__dict__.update(state) | 613 self.__dict__.update(state) |
715 self.store = None | 614 self.store = None |
716 | 615 |
717 def _generate_refresh_request_body(self): | 616 def _generate_refresh_request_body(self): |
718 """Generate the body that will be used in the refresh request.""" | 617 """Generate the body that will be used in the refresh request.""" |
719 body = urllib.parse.urlencode({ | 618 body = urllib.urlencode({ |
720 'grant_type': 'refresh_token', | 619 'grant_type': 'refresh_token', |
721 'client_id': self.client_id, | 620 'client_id': self.client_id, |
722 'client_secret': self.client_secret, | 621 'client_secret': self.client_secret, |
723 'refresh_token': self.refresh_token, | 622 'refresh_token': self.refresh_token, |
724 }) | 623 }) |
725 return body | 624 return body |
726 | 625 |
727 def _generate_refresh_request_headers(self): | 626 def _generate_refresh_request_headers(self): |
728 """Generate the headers that will be used in the refresh request.""" | 627 """Generate the headers that will be used in the refresh request.""" |
729 headers = { | 628 headers = { |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
773 | 672 |
774 Raises: | 673 Raises: |
775 AccessTokenRefreshError: When the refresh fails. | 674 AccessTokenRefreshError: When the refresh fails. |
776 """ | 675 """ |
777 body = self._generate_refresh_request_body() | 676 body = self._generate_refresh_request_body() |
778 headers = self._generate_refresh_request_headers() | 677 headers = self._generate_refresh_request_headers() |
779 | 678 |
780 logger.info('Refreshing access_token') | 679 logger.info('Refreshing access_token') |
781 resp, content = http_request( | 680 resp, content = http_request( |
782 self.token_uri, method='POST', body=body, headers=headers) | 681 self.token_uri, method='POST', body=body, headers=headers) |
783 if six.PY3 and isinstance(content, bytes): | |
784 content = content.decode('utf-8') | |
785 if resp.status == 200: | 682 if resp.status == 200: |
786 d = json.loads(content) | 683 # TODO(jcgregorio) Raise an error if loads fails? |
| 684 d = simplejson.loads(content) |
787 self.token_response = d | 685 self.token_response = d |
788 self.access_token = d['access_token'] | 686 self.access_token = d['access_token'] |
789 self.refresh_token = d.get('refresh_token', self.refresh_token) | 687 self.refresh_token = d.get('refresh_token', self.refresh_token) |
790 if 'expires_in' in d: | 688 if 'expires_in' in d: |
791 self.token_expiry = datetime.timedelta( | 689 self.token_expiry = datetime.timedelta( |
792 seconds=int(d['expires_in'])) + datetime.datetime.utcnow() | 690 seconds=int(d['expires_in'])) + datetime.datetime.utcnow() |
793 else: | 691 else: |
794 self.token_expiry = None | 692 self.token_expiry = None |
795 # On temporary refresh errors, the user does not actually have to | |
796 # re-authorize, so we unflag here. | |
797 self.invalid = False | |
798 if self.store: | 693 if self.store: |
799 self.store.locked_put(self) | 694 self.store.locked_put(self) |
800 else: | 695 else: |
801 # An {'error':...} response body means the token is expired or revoked, | 696 # An {'error':...} response body means the token is expired or revoked, |
802 # so we flag the credentials as such. | 697 # so we flag the credentials as such. |
803 logger.info('Failed to retrieve access token: %s', content) | 698 logger.info('Failed to retrieve access token: %s' % content) |
804 error_msg = 'Invalid response %s.' % resp['status'] | 699 error_msg = 'Invalid response %s.' % resp['status'] |
805 try: | 700 try: |
806 d = json.loads(content) | 701 d = simplejson.loads(content) |
807 if 'error' in d: | 702 if 'error' in d: |
808 error_msg = d['error'] | 703 error_msg = d['error'] |
809 if 'error_description' in d: | |
810 error_msg += ': ' + d['error_description'] | |
811 self.invalid = True | 704 self.invalid = True |
812 if self.store: | 705 if self.store: |
813 self.store.locked_put(self) | 706 self.store.locked_put(self) |
814 except (TypeError, ValueError): | 707 except StandardError: |
815 pass | 708 pass |
816 raise AccessTokenRefreshError(error_msg) | 709 raise AccessTokenRefreshError(error_msg) |
817 | 710 |
818 def _revoke(self, http_request): | 711 def _revoke(self, http_request): |
819 """Revokes this credential and deletes the stored copy (if it exists). | 712 """Revokes the refresh_token and deletes the store if available. |
820 | 713 |
821 Args: | 714 Args: |
822 http_request: callable, a callable that matches the method signature of | 715 http_request: callable, a callable that matches the method signature of |
823 httplib2.Http.request, used to make the revoke request. | 716 httplib2.Http.request, used to make the revoke request. |
824 """ | 717 """ |
825 self._do_revoke(http_request, self.refresh_token or self.access_token) | 718 self._do_revoke(http_request, self.refresh_token) |
826 | 719 |
827 def _do_revoke(self, http_request, token): | 720 def _do_revoke(self, http_request, token): |
828 """Revokes this credential and deletes the stored copy (if it exists). | 721 """Revokes the credentials and deletes the store if available. |
829 | 722 |
830 Args: | 723 Args: |
831 http_request: callable, a callable that matches the method signature of | 724 http_request: callable, a callable that matches the method signature of |
832 httplib2.Http.request, used to make the refresh request. | 725 httplib2.Http.request, used to make the refresh request. |
833 token: A string used as the token to be revoked. Can be either an | 726 token: A string used as the token to be revoked. Can be either an |
834 access_token or refresh_token. | 727 access_token or refresh_token. |
835 | 728 |
836 Raises: | 729 Raises: |
837 TokenRevokeError: If the revoke request does not return with a 200 OK. | 730 TokenRevokeError: If the revoke request does not return with a 200 OK. |
838 """ | 731 """ |
839 logger.info('Revoking token') | 732 logger.info('Revoking token') |
840 query_params = {'token': token} | 733 query_params = {'token': token} |
841 token_revoke_uri = _update_query_params(self.revoke_uri, query_params) | 734 token_revoke_uri = _update_query_params(self.revoke_uri, query_params) |
842 resp, content = http_request(token_revoke_uri) | 735 resp, content = http_request(token_revoke_uri) |
843 if resp.status == 200: | 736 if resp.status == 200: |
844 self.invalid = True | 737 self.invalid = True |
845 else: | 738 else: |
846 error_msg = 'Invalid response %s.' % resp.status | 739 error_msg = 'Invalid response %s.' % resp.status |
847 try: | 740 try: |
848 d = json.loads(content) | 741 d = simplejson.loads(content) |
849 if 'error' in d: | 742 if 'error' in d: |
850 error_msg = d['error'] | 743 error_msg = d['error'] |
851 except (TypeError, ValueError): | 744 except StandardError: |
852 pass | 745 pass |
853 raise TokenRevokeError(error_msg) | 746 raise TokenRevokeError(error_msg) |
854 | 747 |
855 if self.store: | 748 if self.store: |
856 self.store.delete() | 749 self.store.delete() |
857 | 750 |
858 | 751 |
859 class AccessTokenCredentials(OAuth2Credentials): | 752 class AccessTokenCredentials(OAuth2Credentials): |
860 """Credentials object for OAuth 2.0. | 753 """Credentials object for OAuth 2.0. |
861 | 754 |
862 Credentials can be applied to an httplib2.Http object using the | 755 Credentials can be applied to an httplib2.Http object using the |
863 authorize() method, which then signs each request from that object | 756 authorize() method, which then signs each request from that object |
864 with the OAuth 2.0 access token. This set of credentials is for the | 757 with the OAuth 2.0 access token. This set of credentials is for the |
865 use case where you have acquired an OAuth 2.0 access_token from | 758 use case where you have acquired an OAuth 2.0 access_token from |
866 another place such as a JavaScript client or another web | 759 another place such as a JavaScript client or another web |
867 application, and wish to use it from Python. Because only the | 760 application, and wish to use it from Python. Because only the |
868 access_token is present it can not be refreshed and will in time | 761 access_token is present it can not be refreshed and will in time |
869 expire. | 762 expire. |
870 | 763 |
871 AccessTokenCredentials objects may be safely pickled and unpickled. | 764 AccessTokenCredentials objects may be safely pickled and unpickled. |
872 | 765 |
873 Usage:: | 766 Usage: |
874 | |
875 credentials = AccessTokenCredentials('<an access token>', | 767 credentials = AccessTokenCredentials('<an access token>', |
876 'my-user-agent/1.0') | 768 'my-user-agent/1.0') |
877 http = httplib2.Http() | 769 http = httplib2.Http() |
878 http = credentials.authorize(http) | 770 http = credentials.authorize(http) |
879 | 771 |
880 Exceptions: | 772 Exceptions: |
881 AccessTokenCredentialsExpired: raised when the access_token expires or is | 773 AccessTokenCredentialsExpired: raised when the access_token expires or is |
882 revoked. | 774 revoked. |
883 """ | 775 """ |
884 | 776 |
(...skipping 15 matching lines...) Expand all Loading... |
900 None, | 792 None, |
901 None, | 793 None, |
902 None, | 794 None, |
903 None, | 795 None, |
904 user_agent, | 796 user_agent, |
905 revoke_uri=revoke_uri) | 797 revoke_uri=revoke_uri) |
906 | 798 |
907 | 799 |
908 @classmethod | 800 @classmethod |
909 def from_json(cls, s): | 801 def from_json(cls, s): |
910 if six.PY3 and isinstance(s, bytes): | 802 data = simplejson.loads(s) |
911 s = s.decode('utf-8') | |
912 data = json.loads(s) | |
913 retval = AccessTokenCredentials( | 803 retval = AccessTokenCredentials( |
914 data['access_token'], | 804 data['access_token'], |
915 data['user_agent']) | 805 data['user_agent']) |
916 return retval | 806 return retval |
917 | 807 |
918 def _refresh(self, http_request): | 808 def _refresh(self, http_request): |
919 raise AccessTokenCredentialsError( | 809 raise AccessTokenCredentialsError( |
920 'The access_token is expired or invalid and can\'t be refreshed.') | 810 'The access_token is expired or invalid and can\'t be refreshed.') |
921 | 811 |
922 def _revoke(self, http_request): | 812 def _revoke(self, http_request): |
923 """Revokes the access_token and deletes the store if available. | 813 """Revokes the access_token and deletes the store if available. |
924 | 814 |
925 Args: | 815 Args: |
926 http_request: callable, a callable that matches the method signature of | 816 http_request: callable, a callable that matches the method signature of |
927 httplib2.Http.request, used to make the revoke request. | 817 httplib2.Http.request, used to make the revoke request. |
928 """ | 818 """ |
929 self._do_revoke(http_request, self.access_token) | 819 self._do_revoke(http_request, self.access_token) |
930 | 820 |
931 | 821 |
932 def _detect_gce_environment(urlopen=None): | 822 class AssertionCredentials(OAuth2Credentials): |
933 """Determine if the current environment is Compute Engine. | |
934 | |
935 Args: | |
936 urlopen: Optional argument. Function used to open a connection to a URL. | |
937 | |
938 Returns: | |
939 Boolean indicating whether or not the current environment is Google | |
940 Compute Engine. | |
941 """ | |
942 urlopen = urlopen or urllib.request.urlopen | |
943 # Note: the explicit `timeout` below is a workaround. The underlying | |
944 # issue is that resolving an unknown host on some networks will take | |
945 # 20-30 seconds; making this timeout short fixes the issue, but | |
946 # could lead to false negatives in the event that we are on GCE, but | |
947 # the metadata resolution was particularly slow. The latter case is | |
948 # "unlikely". | |
949 try: | |
950 response = urlopen('http://169.254.169.254/', timeout=1) | |
951 return response.info().get('Metadata-Flavor', '') == 'Google' | |
952 except socket.timeout: | |
953 logger.info('Timeout attempting to reach GCE metadata service.') | |
954 return False | |
955 except urllib.error.URLError as e: | |
956 if isinstance(getattr(e, 'reason', None), socket.timeout): | |
957 logger.info('Timeout attempting to reach GCE metadata service.') | |
958 return False | |
959 | |
960 | |
961 def _get_environment(urlopen=None): | |
962 """Detect the environment the code is being run on. | |
963 | |
964 Args: | |
965 urlopen: Optional argument. Function used to open a connection to a URL. | |
966 | |
967 Returns: | |
968 The value of SETTINGS.env_name after being set. If already | |
969 set, simply returns the value. | |
970 """ | |
971 if SETTINGS.env_name is not None: | |
972 return SETTINGS.env_name | |
973 | |
974 # None is an unset value, not the default. | |
975 SETTINGS.env_name = DEFAULT_ENV_NAME | |
976 | |
977 server_software = os.environ.get('SERVER_SOFTWARE', '') | |
978 if server_software.startswith('Google App Engine/'): | |
979 SETTINGS.env_name = 'GAE_PRODUCTION' | |
980 elif server_software.startswith('Development/'): | |
981 SETTINGS.env_name = 'GAE_LOCAL' | |
982 elif NO_GCE_CHECK != 'True' and _detect_gce_environment(urlopen=urlopen): | |
983 SETTINGS.env_name = 'GCE_PRODUCTION' | |
984 | |
985 return SETTINGS.env_name | |
986 | |
987 | |
988 class GoogleCredentials(OAuth2Credentials): | |
989 """Application Default Credentials for use in calling Google APIs. | |
990 | |
991 The Application Default Credentials are being constructed as a function of | |
992 the environment where the code is being run. | |
993 More details can be found on this page: | |
994 https://developers.google.com/accounts/docs/application-default-credentials | |
995 | |
996 Here is an example of how to use the Application Default Credentials for a | |
997 service that requires authentication: | |
998 | |
999 from googleapiclient.discovery import build | |
1000 from oauth2client.client import GoogleCredentials | |
1001 | |
1002 credentials = GoogleCredentials.get_application_default() | |
1003 service = build('compute', 'v1', credentials=credentials) | |
1004 | |
1005 PROJECT = 'bamboo-machine-422' | |
1006 ZONE = 'us-central1-a' | |
1007 request = service.instances().list(project=PROJECT, zone=ZONE) | |
1008 response = request.execute() | |
1009 | |
1010 print(response) | |
1011 """ | |
1012 | |
1013 def __init__(self, access_token, client_id, client_secret, refresh_token, | |
1014 token_expiry, token_uri, user_agent, | |
1015 revoke_uri=GOOGLE_REVOKE_URI): | |
1016 """Create an instance of GoogleCredentials. | |
1017 | |
1018 This constructor is not usually called by the user, instead | |
1019 GoogleCredentials objects are instantiated by | |
1020 GoogleCredentials.from_stream() or | |
1021 GoogleCredentials.get_application_default(). | |
1022 | |
1023 Args: | |
1024 access_token: string, access token. | |
1025 client_id: string, client identifier. | |
1026 client_secret: string, client secret. | |
1027 refresh_token: string, refresh token. | |
1028 token_expiry: datetime, when the access_token expires. | |
1029 token_uri: string, URI of token endpoint. | |
1030 user_agent: string, The HTTP User-Agent to provide for this application. | |
1031 revoke_uri: string, URI for revoke endpoint. | |
1032 Defaults to GOOGLE_REVOKE_URI; a token can't be revoked if this is None. | |
1033 """ | |
1034 super(GoogleCredentials, self).__init__( | |
1035 access_token, client_id, client_secret, refresh_token, token_expiry, | |
1036 token_uri, user_agent, revoke_uri=revoke_uri) | |
1037 | |
1038 def create_scoped_required(self): | |
1039 """Whether this Credentials object is scopeless. | |
1040 | |
1041 create_scoped(scopes) method needs to be called in order to create | |
1042 a Credentials object for API calls. | |
1043 """ | |
1044 return False | |
1045 | |
1046 def create_scoped(self, scopes): | |
1047 """Create a Credentials object for the given scopes. | |
1048 | |
1049 The Credentials type is preserved. | |
1050 """ | |
1051 return self | |
1052 | |
1053 @property | |
1054 def serialization_data(self): | |
1055 """Get the fields and their values identifying the current credentials.""" | |
1056 return { | |
1057 'type': 'authorized_user', | |
1058 'client_id': self.client_id, | |
1059 'client_secret': self.client_secret, | |
1060 'refresh_token': self.refresh_token | |
1061 } | |
1062 | |
1063 @staticmethod | |
1064 def _implicit_credentials_from_gae(env_name=None): | |
1065 """Attempts to get implicit credentials in Google App Engine env. | |
1066 | |
1067 If the current environment is not detected as App Engine, returns None, | |
1068 indicating no Google App Engine credentials can be detected from the | |
1069 current environment. | |
1070 | |
1071 Args: | |
1072 env_name: String, indicating current environment. | |
1073 | |
1074 Returns: | |
1075 None, if not in GAE, else an appengine.AppAssertionCredentials object. | |
1076 """ | |
1077 env_name = env_name or _get_environment() | |
1078 if env_name not in ('GAE_PRODUCTION', 'GAE_LOCAL'): | |
1079 return None | |
1080 | |
1081 return _get_application_default_credential_GAE() | |
1082 | |
1083 @staticmethod | |
1084 def _implicit_credentials_from_gce(env_name=None): | |
1085 """Attempts to get implicit credentials in Google Compute Engine env. | |
1086 | |
1087 If the current environment is not detected as Compute Engine, returns None, | |
1088 indicating no Google Compute Engine credentials can be detected from the | |
1089 current environment. | |
1090 | |
1091 Args: | |
1092 env_name: String, indicating current environment. | |
1093 | |
1094 Returns: | |
1095 None, if not in GCE, else a gce.AppAssertionCredentials object. | |
1096 """ | |
1097 env_name = env_name or _get_environment() | |
1098 if env_name != 'GCE_PRODUCTION': | |
1099 return None | |
1100 | |
1101 return _get_application_default_credential_GCE() | |
1102 | |
1103 @staticmethod | |
1104 def _implicit_credentials_from_files(env_name=None): | |
1105 """Attempts to get implicit credentials from local credential files. | |
1106 | |
1107 First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS | |
1108 is set with a filename and then falls back to a configuration file (the | |
1109 "well known" file) associated with the 'gcloud' command line tool. | |
1110 | |
1111 Args: | |
1112 env_name: Unused argument. | |
1113 | |
1114 Returns: | |
1115 Credentials object associated with the GOOGLE_APPLICATION_CREDENTIALS | |
1116 file or the "well known" file if either exist. If neither file is | |
1117 define, returns None, indicating no credentials from a file can | |
1118 detected from the current environment. | |
1119 """ | |
1120 credentials_filename = _get_environment_variable_file() | |
1121 if not credentials_filename: | |
1122 credentials_filename = _get_well_known_file() | |
1123 if os.path.isfile(credentials_filename): | |
1124 extra_help = (' (produced automatically when running' | |
1125 ' "gcloud auth login" command)') | |
1126 else: | |
1127 credentials_filename = None | |
1128 else: | |
1129 extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS + | |
1130 ' environment variable)') | |
1131 | |
1132 if not credentials_filename: | |
1133 return | |
1134 | |
1135 try: | |
1136 return _get_application_default_credential_from_file(credentials_filename) | |
1137 except (ApplicationDefaultCredentialsError, ValueError) as error: | |
1138 _raise_exception_for_reading_json(credentials_filename, extra_help, error) | |
1139 | |
1140 @classmethod | |
1141 def _get_implicit_credentials(cls): | |
1142 """Gets credentials implicitly from the environment. | |
1143 | |
1144 Checks environment in order of precedence: | |
1145 - Google App Engine (production and testing) | |
1146 - Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to | |
1147 a file with stored credentials information. | |
1148 - Stored "well known" file associated with `gcloud` command line tool. | |
1149 - Google Compute Engine production environment. | |
1150 | |
1151 Exceptions: | |
1152 ApplicationDefaultCredentialsError: raised when the credentials fail | |
1153 to be retrieved. | |
1154 """ | |
1155 env_name = _get_environment() | |
1156 | |
1157 # Environ checks (in order). Assumes each checker takes `env_name` | |
1158 # as a kwarg. | |
1159 environ_checkers = [ | |
1160 cls._implicit_credentials_from_gae, | |
1161 cls._implicit_credentials_from_files, | |
1162 cls._implicit_credentials_from_gce, | |
1163 ] | |
1164 | |
1165 for checker in environ_checkers: | |
1166 credentials = checker(env_name=env_name) | |
1167 if credentials is not None: | |
1168 return credentials | |
1169 | |
1170 # If no credentials, fail. | |
1171 raise ApplicationDefaultCredentialsError(ADC_HELP_MSG) | |
1172 | |
1173 @staticmethod | |
1174 def get_application_default(): | |
1175 """Get the Application Default Credentials for the current environment. | |
1176 | |
1177 Exceptions: | |
1178 ApplicationDefaultCredentialsError: raised when the credentials fail | |
1179 to be retrieved. | |
1180 """ | |
1181 return GoogleCredentials._get_implicit_credentials() | |
1182 | |
1183 @staticmethod | |
1184 def from_stream(credential_filename): | |
1185 """Create a Credentials object by reading the information from a given file. | |
1186 | |
1187 It returns an object of type GoogleCredentials. | |
1188 | |
1189 Args: | |
1190 credential_filename: the path to the file from where the credentials | |
1191 are to be read | |
1192 | |
1193 Exceptions: | |
1194 ApplicationDefaultCredentialsError: raised when the credentials fail | |
1195 to be retrieved. | |
1196 """ | |
1197 | |
1198 if credential_filename and os.path.isfile(credential_filename): | |
1199 try: | |
1200 return _get_application_default_credential_from_file( | |
1201 credential_filename) | |
1202 except (ApplicationDefaultCredentialsError, ValueError) as error: | |
1203 extra_help = ' (provided as parameter to the from_stream() method)' | |
1204 _raise_exception_for_reading_json(credential_filename, | |
1205 extra_help, | |
1206 error) | |
1207 else: | |
1208 raise ApplicationDefaultCredentialsError( | |
1209 'The parameter passed to the from_stream() ' | |
1210 'method should point to a file.') | |
1211 | |
1212 | |
1213 def _save_private_file(filename, json_contents): | |
1214 """Saves a file with read-write permissions on for the owner. | |
1215 | |
1216 Args: | |
1217 filename: String. Absolute path to file. | |
1218 json_contents: JSON serializable object to be saved. | |
1219 """ | |
1220 temp_filename = tempfile.mktemp() | |
1221 file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600) | |
1222 with os.fdopen(file_desc, 'w') as file_handle: | |
1223 json.dump(json_contents, file_handle, sort_keys=True, | |
1224 indent=2, separators=(',', ': ')) | |
1225 shutil.move(temp_filename, filename) | |
1226 | |
1227 | |
1228 def save_to_well_known_file(credentials, well_known_file=None): | |
1229 """Save the provided GoogleCredentials to the well known file. | |
1230 | |
1231 Args: | |
1232 credentials: | |
1233 the credentials to be saved to the well known file; | |
1234 it should be an instance of GoogleCredentials | |
1235 well_known_file: | |
1236 the name of the file where the credentials are to be saved; | |
1237 this parameter is supposed to be used for testing only | |
1238 """ | |
1239 # TODO(orestica): move this method to tools.py | |
1240 # once the argparse import gets fixed (it is not present in Python 2.6) | |
1241 | |
1242 if well_known_file is None: | |
1243 well_known_file = _get_well_known_file() | |
1244 | |
1245 credentials_data = credentials.serialization_data | |
1246 _save_private_file(well_known_file, credentials_data) | |
1247 | |
1248 | |
1249 def _get_environment_variable_file(): | |
1250 application_default_credential_filename = ( | |
1251 os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, | |
1252 None)) | |
1253 | |
1254 if application_default_credential_filename: | |
1255 if os.path.isfile(application_default_credential_filename): | |
1256 return application_default_credential_filename | |
1257 else: | |
1258 raise ApplicationDefaultCredentialsError( | |
1259 'File ' + application_default_credential_filename + ' (pointed by ' + | |
1260 GOOGLE_APPLICATION_CREDENTIALS + | |
1261 ' environment variable) does not exist!') | |
1262 | |
1263 | |
1264 def _get_well_known_file(): | |
1265 """Get the well known file produced by command 'gcloud auth login'.""" | |
1266 # TODO(orestica): Revisit this method once gcloud provides a better way | |
1267 # of pinpointing the exact location of the file. | |
1268 | |
1269 WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json' | |
1270 | |
1271 if os.name == 'nt': | |
1272 try: | |
1273 default_config_path = os.path.join(os.environ['APPDATA'], | |
1274 _CLOUDSDK_CONFIG_DIRECTORY) | |
1275 except KeyError: | |
1276 # This should never happen unless someone is really messing with things. | |
1277 drive = os.environ.get('SystemDrive', 'C:') | |
1278 default_config_path = os.path.join(drive, '\\', | |
1279 _CLOUDSDK_CONFIG_DIRECTORY) | |
1280 else: | |
1281 default_config_path = os.path.join(os.path.expanduser('~'), | |
1282 '.config', | |
1283 _CLOUDSDK_CONFIG_DIRECTORY) | |
1284 | |
1285 default_config_path = os.path.join(default_config_path, | |
1286 WELL_KNOWN_CREDENTIALS_FILE) | |
1287 | |
1288 return default_config_path | |
1289 | |
1290 | |
1291 def _get_application_default_credential_from_file(filename): | |
1292 """Build the Application Default Credentials from file.""" | |
1293 | |
1294 from oauth2client import service_account | |
1295 | |
1296 # read the credentials from the file | |
1297 with open(filename) as file_obj: | |
1298 client_credentials = json.load(file_obj) | |
1299 | |
1300 credentials_type = client_credentials.get('type') | |
1301 if credentials_type == AUTHORIZED_USER: | |
1302 required_fields = set(['client_id', 'client_secret', 'refresh_token']) | |
1303 elif credentials_type == SERVICE_ACCOUNT: | |
1304 required_fields = set(['client_id', 'client_email', 'private_key_id', | |
1305 'private_key']) | |
1306 else: | |
1307 raise ApplicationDefaultCredentialsError( | |
1308 "'type' field should be defined (and have one of the '" + | |
1309 AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)") | |
1310 | |
1311 missing_fields = required_fields.difference(client_credentials.keys()) | |
1312 | |
1313 if missing_fields: | |
1314 _raise_exception_for_missing_fields(missing_fields) | |
1315 | |
1316 if client_credentials['type'] == AUTHORIZED_USER: | |
1317 return GoogleCredentials( | |
1318 access_token=None, | |
1319 client_id=client_credentials['client_id'], | |
1320 client_secret=client_credentials['client_secret'], | |
1321 refresh_token=client_credentials['refresh_token'], | |
1322 token_expiry=None, | |
1323 token_uri=GOOGLE_TOKEN_URI, | |
1324 user_agent='Python client library') | |
1325 else: # client_credentials['type'] == SERVICE_ACCOUNT | |
1326 return service_account._ServiceAccountCredentials( | |
1327 service_account_id=client_credentials['client_id'], | |
1328 service_account_email=client_credentials['client_email'], | |
1329 private_key_id=client_credentials['private_key_id'], | |
1330 private_key_pkcs8_text=client_credentials['private_key'], | |
1331 scopes=[]) | |
1332 | |
1333 | |
1334 def _raise_exception_for_missing_fields(missing_fields): | |
1335 raise ApplicationDefaultCredentialsError( | |
1336 'The following field(s) must be defined: ' + ', '.join(missing_fields)) | |
1337 | |
1338 | |
1339 def _raise_exception_for_reading_json(credential_file, | |
1340 extra_help, | |
1341 error): | |
1342 raise ApplicationDefaultCredentialsError( | |
1343 'An error was encountered while reading json file: '+ | |
1344 credential_file + extra_help + ': ' + str(error)) | |
1345 | |
1346 | |
1347 def _get_application_default_credential_GAE(): | |
1348 from oauth2client.appengine import AppAssertionCredentials | |
1349 | |
1350 return AppAssertionCredentials([]) | |
1351 | |
1352 | |
1353 def _get_application_default_credential_GCE(): | |
1354 from oauth2client.gce import AppAssertionCredentials | |
1355 | |
1356 return AppAssertionCredentials([]) | |
1357 | |
1358 | |
1359 class AssertionCredentials(GoogleCredentials): | |
1360 """Abstract Credentials object used for OAuth 2.0 assertion grants. | 823 """Abstract Credentials object used for OAuth 2.0 assertion grants. |
1361 | 824 |
1362 This credential does not require a flow to instantiate because it | 825 This credential does not require a flow to instantiate because it |
1363 represents a two legged flow, and therefore has all of the required | 826 represents a two legged flow, and therefore has all of the required |
1364 information to generate and refresh its own access tokens. It must | 827 information to generate and refresh its own access tokens. It must |
1365 be subclassed to generate the appropriate assertion string. | 828 be subclassed to generate the appropriate assertion string. |
1366 | 829 |
1367 AssertionCredentials objects may be safely pickled and unpickled. | 830 AssertionCredentials objects may be safely pickled and unpickled. |
1368 """ | 831 """ |
1369 | 832 |
(...skipping 19 matching lines...) Expand all Loading... |
1389 None, | 852 None, |
1390 None, | 853 None, |
1391 token_uri, | 854 token_uri, |
1392 user_agent, | 855 user_agent, |
1393 revoke_uri=revoke_uri) | 856 revoke_uri=revoke_uri) |
1394 self.assertion_type = assertion_type | 857 self.assertion_type = assertion_type |
1395 | 858 |
1396 def _generate_refresh_request_body(self): | 859 def _generate_refresh_request_body(self): |
1397 assertion = self._generate_assertion() | 860 assertion = self._generate_assertion() |
1398 | 861 |
1399 body = urllib.parse.urlencode({ | 862 body = urllib.urlencode({ |
1400 'assertion': assertion, | 863 'assertion': assertion, |
1401 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', | 864 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', |
1402 }) | 865 }) |
1403 | 866 |
1404 return body | 867 return body |
1405 | 868 |
1406 def _generate_assertion(self): | 869 def _generate_assertion(self): |
1407 """Generate the assertion string that will be used in the access token | 870 """Generate the assertion string that will be used in the access token |
1408 request. | 871 request. |
1409 """ | 872 """ |
1410 _abstract() | 873 _abstract() |
1411 | 874 |
1412 def _revoke(self, http_request): | 875 def _revoke(self, http_request): |
1413 """Revokes the access_token and deletes the store if available. | 876 """Revokes the access_token and deletes the store if available. |
1414 | 877 |
1415 Args: | 878 Args: |
1416 http_request: callable, a callable that matches the method signature of | 879 http_request: callable, a callable that matches the method signature of |
1417 httplib2.Http.request, used to make the revoke request. | 880 httplib2.Http.request, used to make the revoke request. |
1418 """ | 881 """ |
1419 self._do_revoke(http_request, self.access_token) | 882 self._do_revoke(http_request, self.access_token) |
1420 | 883 |
1421 | 884 |
1422 def _RequireCryptoOrDie(): | 885 if HAS_CRYPTO: |
1423 """Ensure we have a crypto library, or throw CryptoUnavailableError. | 886 # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is |
| 887 # missing then don't create the SignedJwtAssertionCredentials or the |
| 888 # verify_id_token() method. |
1424 | 889 |
1425 The oauth2client.crypt module requires either PyCrypto or PyOpenSSL | 890 class SignedJwtAssertionCredentials(AssertionCredentials): |
1426 to be available in order to function, but these are optional | 891 """Credentials object used for OAuth 2.0 Signed JWT assertion grants. |
1427 dependencies. | |
1428 """ | |
1429 if not HAS_CRYPTO: | |
1430 raise CryptoUnavailableError('No crypto library available') | |
1431 | 892 |
| 893 This credential does not require a flow to instantiate because it represents |
| 894 a two legged flow, and therefore has all of the required information to |
| 895 generate and refresh its own access tokens. |
1432 | 896 |
1433 class SignedJwtAssertionCredentials(AssertionCredentials): | 897 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or |
1434 """Credentials object used for OAuth 2.0 Signed JWT assertion grants. | 898 later. For App Engine you may also consider using AppAssertionCredentials. |
| 899 """ |
1435 | 900 |
1436 This credential does not require a flow to instantiate because it | 901 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds |
1437 represents a two legged flow, and therefore has all of the required | |
1438 information to generate and refresh its own access tokens. | |
1439 | 902 |
1440 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto | 903 @util.positional(4) |
1441 2.6 or later. For App Engine you may also consider using | 904 def __init__(self, |
1442 AppAssertionCredentials. | 905 service_account_name, |
1443 """ | 906 private_key, |
| 907 scope, |
| 908 private_key_password='notasecret', |
| 909 user_agent=None, |
| 910 token_uri=GOOGLE_TOKEN_URI, |
| 911 revoke_uri=GOOGLE_REVOKE_URI, |
| 912 **kwargs): |
| 913 """Constructor for SignedJwtAssertionCredentials. |
1444 | 914 |
1445 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds | 915 Args: |
| 916 service_account_name: string, id for account, usually an email address. |
| 917 private_key: string, private key in PKCS12 or PEM format. |
| 918 scope: string or iterable of strings, scope(s) of the credentials being |
| 919 requested. |
| 920 private_key_password: string, password for private_key, unused if |
| 921 private_key is in PEM format. |
| 922 user_agent: string, HTTP User-Agent to provide for this application. |
| 923 token_uri: string, URI for token endpoint. For convenience |
| 924 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
| 925 revoke_uri: string, URI for revoke endpoint. |
| 926 kwargs: kwargs, Additional parameters to add to the JWT token, for |
| 927 example sub=joe@xample.org.""" |
1446 | 928 |
1447 @util.positional(4) | 929 super(SignedJwtAssertionCredentials, self).__init__( |
1448 def __init__(self, | 930 None, |
1449 service_account_name, | 931 user_agent=user_agent, |
1450 private_key, | 932 token_uri=token_uri, |
1451 scope, | 933 revoke_uri=revoke_uri, |
1452 private_key_password='notasecret', | 934 ) |
1453 user_agent=None, | 935 |
1454 token_uri=GOOGLE_TOKEN_URI, | 936 self.scope = util.scopes_to_string(scope) |
1455 revoke_uri=GOOGLE_REVOKE_URI, | 937 |
1456 **kwargs): | 938 # Keep base64 encoded so it can be stored in JSON. |
1457 """Constructor for SignedJwtAssertionCredentials. | 939 self.private_key = base64.b64encode(private_key) |
| 940 |
| 941 self.private_key_password = private_key_password |
| 942 self.service_account_name = service_account_name |
| 943 self.kwargs = kwargs |
| 944 |
| 945 @classmethod |
| 946 def from_json(cls, s): |
| 947 data = simplejson.loads(s) |
| 948 retval = SignedJwtAssertionCredentials( |
| 949 data['service_account_name'], |
| 950 base64.b64decode(data['private_key']), |
| 951 data['scope'], |
| 952 private_key_password=data['private_key_password'], |
| 953 user_agent=data['user_agent'], |
| 954 token_uri=data['token_uri'], |
| 955 **data['kwargs'] |
| 956 ) |
| 957 retval.invalid = data['invalid'] |
| 958 retval.access_token = data['access_token'] |
| 959 return retval |
| 960 |
| 961 def _generate_assertion(self): |
| 962 """Generate the assertion that will be used in the request.""" |
| 963 now = long(time.time()) |
| 964 payload = { |
| 965 'aud': self.token_uri, |
| 966 'scope': self.scope, |
| 967 'iat': now, |
| 968 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, |
| 969 'iss': self.service_account_name |
| 970 } |
| 971 payload.update(self.kwargs) |
| 972 logger.debug(str(payload)) |
| 973 |
| 974 private_key = base64.b64decode(self.private_key) |
| 975 return crypt.make_signed_jwt(crypt.Signer.from_string( |
| 976 private_key, self.private_key_password), payload) |
| 977 |
| 978 # Only used in verify_id_token(), which is always calling to the same URI |
| 979 # for the certs. |
| 980 _cached_http = httplib2.Http(MemoryCache()) |
| 981 |
| 982 @util.positional(2) |
| 983 def verify_id_token(id_token, audience, http=None, |
| 984 cert_uri=ID_TOKEN_VERIFICATON_CERTS): |
| 985 """Verifies a signed JWT id_token. |
| 986 |
| 987 This function requires PyOpenSSL and because of that it does not work on |
| 988 App Engine. |
1458 | 989 |
1459 Args: | 990 Args: |
1460 service_account_name: string, id for account, usually an email address. | 991 id_token: string, A Signed JWT. |
1461 private_key: string, private key in PKCS12 or PEM format. | 992 audience: string, The audience 'aud' that the token should be for. |
1462 scope: string or iterable of strings, scope(s) of the credentials being | 993 http: httplib2.Http, instance to use to make the HTTP request. Callers |
1463 requested. | 994 should supply an instance that has caching enabled. |
1464 private_key_password: string, password for private_key, unused if | 995 cert_uri: string, URI of the certificates in JSON format to |
1465 private_key is in PEM format. | 996 verify the JWT against. |
1466 user_agent: string, HTTP User-Agent to provide for this application. | 997 |
1467 token_uri: string, URI for token endpoint. For convenience | 998 Returns: |
1468 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | 999 The deserialized JSON in the JWT. |
1469 revoke_uri: string, URI for revoke endpoint. | |
1470 kwargs: kwargs, Additional parameters to add to the JWT token, for | |
1471 example sub=joe@xample.org. | |
1472 | 1000 |
1473 Raises: | 1001 Raises: |
1474 CryptoUnavailableError if no crypto library is available. | 1002 oauth2client.crypt.AppIdentityError if the JWT fails to verify. |
1475 """ | 1003 """ |
1476 _RequireCryptoOrDie() | 1004 if http is None: |
1477 super(SignedJwtAssertionCredentials, self).__init__( | 1005 http = _cached_http |
1478 None, | |
1479 user_agent=user_agent, | |
1480 token_uri=token_uri, | |
1481 revoke_uri=revoke_uri, | |
1482 ) | |
1483 | 1006 |
1484 self.scope = util.scopes_to_string(scope) | 1007 resp, content = http.request(cert_uri) |
1485 | 1008 |
1486 # Keep base64 encoded so it can be stored in JSON. | 1009 if resp.status == 200: |
1487 self.private_key = base64.b64encode(private_key) | 1010 certs = simplejson.loads(content) |
1488 if isinstance(self.private_key, six.text_type): | 1011 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) |
1489 self.private_key = self.private_key.encode('utf-8') | 1012 else: |
1490 | 1013 raise VerifyJwtTokenError('Status code: %d' % resp.status) |
1491 self.private_key_password = private_key_password | |
1492 self.service_account_name = service_account_name | |
1493 self.kwargs = kwargs | |
1494 | |
1495 @classmethod | |
1496 def from_json(cls, s): | |
1497 data = json.loads(s) | |
1498 retval = SignedJwtAssertionCredentials( | |
1499 data['service_account_name'], | |
1500 base64.b64decode(data['private_key']), | |
1501 data['scope'], | |
1502 private_key_password=data['private_key_password'], | |
1503 user_agent=data['user_agent'], | |
1504 token_uri=data['token_uri'], | |
1505 **data['kwargs'] | |
1506 ) | |
1507 retval.invalid = data['invalid'] | |
1508 retval.access_token = data['access_token'] | |
1509 return retval | |
1510 | |
1511 def _generate_assertion(self): | |
1512 """Generate the assertion that will be used in the request.""" | |
1513 now = int(time.time()) | |
1514 payload = { | |
1515 'aud': self.token_uri, | |
1516 'scope': self.scope, | |
1517 'iat': now, | |
1518 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, | |
1519 'iss': self.service_account_name | |
1520 } | |
1521 payload.update(self.kwargs) | |
1522 logger.debug(str(payload)) | |
1523 | |
1524 private_key = base64.b64decode(self.private_key) | |
1525 return crypt.make_signed_jwt(crypt.Signer.from_string( | |
1526 private_key, self.private_key_password), payload) | |
1527 | |
1528 # Only used in verify_id_token(), which is always calling to the same URI | |
1529 # for the certs. | |
1530 _cached_http = httplib2.Http(MemoryCache()) | |
1531 | |
1532 @util.positional(2) | |
1533 def verify_id_token(id_token, audience, http=None, | |
1534 cert_uri=ID_TOKEN_VERIFICATION_CERTS): | |
1535 """Verifies a signed JWT id_token. | |
1536 | |
1537 This function requires PyOpenSSL and because of that it does not work on | |
1538 App Engine. | |
1539 | |
1540 Args: | |
1541 id_token: string, A Signed JWT. | |
1542 audience: string, The audience 'aud' that the token should be for. | |
1543 http: httplib2.Http, instance to use to make the HTTP request. Callers | |
1544 should supply an instance that has caching enabled. | |
1545 cert_uri: string, URI of the certificates in JSON format to | |
1546 verify the JWT against. | |
1547 | |
1548 Returns: | |
1549 The deserialized JSON in the JWT. | |
1550 | |
1551 Raises: | |
1552 oauth2client.crypt.AppIdentityError: if the JWT fails to verify. | |
1553 CryptoUnavailableError: if no crypto library is available. | |
1554 """ | |
1555 _RequireCryptoOrDie() | |
1556 if http is None: | |
1557 http = _cached_http | |
1558 | |
1559 resp, content = http.request(cert_uri) | |
1560 | |
1561 if resp.status == 200: | |
1562 certs = json.loads(content.decode('utf-8')) | |
1563 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) | |
1564 else: | |
1565 raise VerifyJwtTokenError('Status code: %d' % resp.status) | |
1566 | 1014 |
1567 | 1015 |
1568 def _urlsafe_b64decode(b64string): | 1016 def _urlsafe_b64decode(b64string): |
1569 # Guard against unicode strings, which base64 can't handle. | 1017 # Guard against unicode strings, which base64 can't handle. |
1570 if isinstance(b64string, six.text_type): | 1018 b64string = b64string.encode('ascii') |
1571 b64string = b64string.encode('ascii') | 1019 padded = b64string + '=' * (4 - len(b64string) % 4) |
1572 padded = b64string + b'=' * (4 - len(b64string) % 4) | |
1573 return base64.urlsafe_b64decode(padded) | 1020 return base64.urlsafe_b64decode(padded) |
1574 | 1021 |
1575 | 1022 |
1576 def _extract_id_token(id_token): | 1023 def _extract_id_token(id_token): |
1577 """Extract the JSON payload from a JWT. | 1024 """Extract the JSON payload from a JWT. |
1578 | 1025 |
1579 Does the extraction w/o checking the signature. | 1026 Does the extraction w/o checking the signature. |
1580 | 1027 |
1581 Args: | 1028 Args: |
1582 id_token: string or bytestring, OAuth 2.0 id_token. | 1029 id_token: string, OAuth 2.0 id_token. |
1583 | 1030 |
1584 Returns: | 1031 Returns: |
1585 object, The deserialized JSON payload. | 1032 object, The deserialized JSON payload. |
1586 """ | 1033 """ |
1587 if type(id_token) == bytes: | 1034 segments = id_token.split('.') |
1588 segments = id_token.split(b'.') | |
1589 else: | |
1590 segments = id_token.split(u'.') | |
1591 | 1035 |
1592 if len(segments) != 3: | 1036 if (len(segments) != 3): |
1593 raise VerifyJwtTokenError( | 1037 raise VerifyJwtTokenError( |
1594 'Wrong number of segments in token: %s' % id_token) | 1038 'Wrong number of segments in token: %s' % id_token) |
1595 | 1039 |
1596 return json.loads(_urlsafe_b64decode(segments[1]).decode('utf-8')) | 1040 return simplejson.loads(_urlsafe_b64decode(segments[1])) |
1597 | 1041 |
1598 | 1042 |
1599 def _parse_exchange_token_response(content): | 1043 def _parse_exchange_token_response(content): |
1600 """Parses response of an exchange token request. | 1044 """Parses response of an exchange token request. |
1601 | 1045 |
1602 Most providers return JSON but some (e.g. Facebook) return a | 1046 Most providers return JSON but some (e.g. Facebook) return a |
1603 url-encoded string. | 1047 url-encoded string. |
1604 | 1048 |
1605 Args: | 1049 Args: |
1606 content: The body of a response | 1050 content: The body of a response |
1607 | 1051 |
1608 Returns: | 1052 Returns: |
1609 Content as a dictionary object. Note that the dict could be empty, | 1053 Content as a dictionary object. Note that the dict could be empty, |
1610 i.e. {}. That basically indicates a failure. | 1054 i.e. {}. That basically indicates a failure. |
1611 """ | 1055 """ |
1612 resp = {} | 1056 resp = {} |
1613 try: | 1057 try: |
1614 resp = json.loads(content.decode('utf-8')) | 1058 resp = simplejson.loads(content) |
1615 except Exception: | 1059 except StandardError: |
1616 # different JSON libs raise different exceptions, | 1060 # different JSON libs raise different exceptions, |
1617 # so we just do a catch-all here | 1061 # so we just do a catch-all here |
1618 content = content.decode('utf-8') | 1062 resp = dict(parse_qsl(content)) |
1619 resp = dict(urllib.parse.parse_qsl(content)) | |
1620 | 1063 |
1621 # some providers respond with 'expires', others with 'expires_in' | 1064 # some providers respond with 'expires', others with 'expires_in' |
1622 if resp and 'expires' in resp: | 1065 if resp and 'expires' in resp: |
1623 resp['expires_in'] = resp.pop('expires') | 1066 resp['expires_in'] = resp.pop('expires') |
1624 | 1067 |
1625 return resp | 1068 return resp |
1626 | 1069 |
1627 | 1070 |
1628 @util.positional(4) | 1071 @util.positional(4) |
1629 def credentials_from_code(client_id, client_secret, scope, code, | 1072 def credentials_from_code(client_id, client_secret, scope, code, |
1630 redirect_uri='postmessage', http=None, | 1073 redirect_uri='postmessage', http=None, |
1631 user_agent=None, token_uri=GOOGLE_TOKEN_URI, | 1074 user_agent=None, token_uri=GOOGLE_TOKEN_URI, |
1632 auth_uri=GOOGLE_AUTH_URI, | 1075 auth_uri=GOOGLE_AUTH_URI, |
1633 revoke_uri=GOOGLE_REVOKE_URI, | 1076 revoke_uri=GOOGLE_REVOKE_URI): |
1634 device_uri=GOOGLE_DEVICE_URI): | |
1635 """Exchanges an authorization code for an OAuth2Credentials object. | 1077 """Exchanges an authorization code for an OAuth2Credentials object. |
1636 | 1078 |
1637 Args: | 1079 Args: |
1638 client_id: string, client identifier. | 1080 client_id: string, client identifier. |
1639 client_secret: string, client secret. | 1081 client_secret: string, client secret. |
1640 scope: string or iterable of strings, scope(s) to request. | 1082 scope: string or iterable of strings, scope(s) to request. |
1641 code: string, An authorization code, most likely passed down from | 1083 code: string, An authroization code, most likely passed down from |
1642 the client | 1084 the client |
1643 redirect_uri: string, this is generally set to 'postmessage' to match the | 1085 redirect_uri: string, this is generally set to 'postmessage' to match the |
1644 redirect_uri that the client specified | 1086 redirect_uri that the client specified |
1645 http: httplib2.Http, optional http instance to use to do the fetch | 1087 http: httplib2.Http, optional http instance to use to do the fetch |
1646 token_uri: string, URI for token endpoint. For convenience | 1088 token_uri: string, URI for token endpoint. For convenience |
1647 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | 1089 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
1648 auth_uri: string, URI for authorization endpoint. For convenience | 1090 auth_uri: string, URI for authorization endpoint. For convenience |
1649 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | 1091 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
1650 revoke_uri: string, URI for revoke endpoint. For convenience | 1092 revoke_uri: string, URI for revoke endpoint. For convenience |
1651 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | 1093 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
1652 device_uri: string, URI for device authorization endpoint. For convenience | |
1653 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
1654 | 1094 |
1655 Returns: | 1095 Returns: |
1656 An OAuth2Credentials object. | 1096 An OAuth2Credentials object. |
1657 | 1097 |
1658 Raises: | 1098 Raises: |
1659 FlowExchangeError if the authorization code cannot be exchanged for an | 1099 FlowExchangeError if the authorization code cannot be exchanged for an |
1660 access token | 1100 access token |
1661 """ | 1101 """ |
1662 flow = OAuth2WebServerFlow(client_id, client_secret, scope, | 1102 flow = OAuth2WebServerFlow(client_id, client_secret, scope, |
1663 redirect_uri=redirect_uri, user_agent=user_agent, | 1103 redirect_uri=redirect_uri, user_agent=user_agent, |
1664 auth_uri=auth_uri, token_uri=token_uri, | 1104 auth_uri=auth_uri, token_uri=token_uri, |
1665 revoke_uri=revoke_uri, device_uri=device_uri) | 1105 revoke_uri=revoke_uri) |
1666 | 1106 |
1667 credentials = flow.step2_exchange(code, http=http) | 1107 credentials = flow.step2_exchange(code, http=http) |
1668 return credentials | 1108 return credentials |
1669 | 1109 |
1670 | 1110 |
1671 @util.positional(3) | 1111 @util.positional(3) |
1672 def credentials_from_clientsecrets_and_code(filename, scope, code, | 1112 def credentials_from_clientsecrets_and_code(filename, scope, code, |
1673 message = None, | 1113 message = None, |
1674 redirect_uri='postmessage', | 1114 redirect_uri='postmessage', |
1675 http=None, | 1115 http=None, |
1676 cache=None, | 1116 cache=None): |
1677 device_uri=None): | |
1678 """Returns OAuth2Credentials from a clientsecrets file and an auth code. | 1117 """Returns OAuth2Credentials from a clientsecrets file and an auth code. |
1679 | 1118 |
1680 Will create the right kind of Flow based on the contents of the clientsecrets | 1119 Will create the right kind of Flow based on the contents of the clientsecrets |
1681 file or will raise InvalidClientSecretsError for unknown types of Flows. | 1120 file or will raise InvalidClientSecretsError for unknown types of Flows. |
1682 | 1121 |
1683 Args: | 1122 Args: |
1684 filename: string, File name of clientsecrets. | 1123 filename: string, File name of clientsecrets. |
1685 scope: string or iterable of strings, scope(s) to request. | 1124 scope: string or iterable of strings, scope(s) to request. |
1686 code: string, An authorization code, most likely passed down from | 1125 code: string, An authorization code, most likely passed down from |
1687 the client | 1126 the client |
1688 message: string, A friendly string to display to the user if the | 1127 message: string, A friendly string to display to the user if the |
1689 clientsecrets file is missing or invalid. If message is provided then | 1128 clientsecrets file is missing or invalid. If message is provided then |
1690 sys.exit will be called in the case of an error. If message in not | 1129 sys.exit will be called in the case of an error. If message in not |
1691 provided then clientsecrets.InvalidClientSecretsError will be raised. | 1130 provided then clientsecrets.InvalidClientSecretsError will be raised. |
1692 redirect_uri: string, this is generally set to 'postmessage' to match the | 1131 redirect_uri: string, this is generally set to 'postmessage' to match the |
1693 redirect_uri that the client specified | 1132 redirect_uri that the client specified |
1694 http: httplib2.Http, optional http instance to use to do the fetch | 1133 http: httplib2.Http, optional http instance to use to do the fetch |
1695 cache: An optional cache service client that implements get() and set() | 1134 cache: An optional cache service client that implements get() and set() |
1696 methods. See clientsecrets.loadfile() for details. | 1135 methods. See clientsecrets.loadfile() for details. |
1697 device_uri: string, OAuth 2.0 device authorization endpoint | |
1698 | 1136 |
1699 Returns: | 1137 Returns: |
1700 An OAuth2Credentials object. | 1138 An OAuth2Credentials object. |
1701 | 1139 |
1702 Raises: | 1140 Raises: |
1703 FlowExchangeError if the authorization code cannot be exchanged for an | 1141 FlowExchangeError if the authorization code cannot be exchanged for an |
1704 access token | 1142 access token |
1705 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. | 1143 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. |
1706 clientsecrets.InvalidClientSecretsError if the clientsecrets file is | 1144 clientsecrets.InvalidClientSecretsError if the clientsecrets file is |
1707 invalid. | 1145 invalid. |
1708 """ | 1146 """ |
1709 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, | 1147 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, |
1710 redirect_uri=redirect_uri, | 1148 redirect_uri=redirect_uri) |
1711 device_uri=device_uri) | |
1712 credentials = flow.step2_exchange(code, http=http) | 1149 credentials = flow.step2_exchange(code, http=http) |
1713 return credentials | 1150 return credentials |
1714 | 1151 |
1715 | 1152 |
1716 class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', ( | |
1717 'device_code', 'user_code', 'interval', 'verification_url', | |
1718 'user_code_expiry'))): | |
1719 """Intermediate information the OAuth2 for devices flow.""" | |
1720 | |
1721 @classmethod | |
1722 def FromResponse(cls, response): | |
1723 """Create a DeviceFlowInfo from a server response. | |
1724 | |
1725 The response should be a dict containing entries as described here: | |
1726 | |
1727 http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1 | |
1728 """ | |
1729 # device_code, user_code, and verification_url are required. | |
1730 kwargs = { | |
1731 'device_code': response['device_code'], | |
1732 'user_code': response['user_code'], | |
1733 } | |
1734 # The response may list the verification address as either | |
1735 # verification_url or verification_uri, so we check for both. | |
1736 verification_url = response.get( | |
1737 'verification_url', response.get('verification_uri')) | |
1738 if verification_url is None: | |
1739 raise OAuth2DeviceCodeError( | |
1740 'No verification_url provided in server response') | |
1741 kwargs['verification_url'] = verification_url | |
1742 # expires_in and interval are optional. | |
1743 kwargs.update({ | |
1744 'interval': response.get('interval'), | |
1745 'user_code_expiry': None, | |
1746 }) | |
1747 if 'expires_in' in response: | |
1748 kwargs['user_code_expiry'] = datetime.datetime.now() + datetime.timedelta( | |
1749 seconds=int(response['expires_in'])) | |
1750 | |
1751 return cls(**kwargs) | |
1752 | |
1753 class OAuth2WebServerFlow(Flow): | 1153 class OAuth2WebServerFlow(Flow): |
1754 """Does the Web Server Flow for OAuth 2.0. | 1154 """Does the Web Server Flow for OAuth 2.0. |
1755 | 1155 |
1756 OAuth2WebServerFlow objects may be safely pickled and unpickled. | 1156 OAuth2WebServerFlow objects may be safely pickled and unpickled. |
1757 """ | 1157 """ |
1758 | 1158 |
1759 @util.positional(4) | 1159 @util.positional(4) |
1760 def __init__(self, client_id, client_secret, scope, | 1160 def __init__(self, client_id, client_secret, scope, |
1761 redirect_uri=None, | 1161 redirect_uri=None, |
1762 user_agent=None, | 1162 user_agent=None, |
1763 auth_uri=GOOGLE_AUTH_URI, | 1163 auth_uri=GOOGLE_AUTH_URI, |
1764 token_uri=GOOGLE_TOKEN_URI, | 1164 token_uri=GOOGLE_TOKEN_URI, |
1765 revoke_uri=GOOGLE_REVOKE_URI, | 1165 revoke_uri=GOOGLE_REVOKE_URI, |
1766 login_hint=None, | |
1767 device_uri=GOOGLE_DEVICE_URI, | |
1768 **kwargs): | 1166 **kwargs): |
1769 """Constructor for OAuth2WebServerFlow. | 1167 """Constructor for OAuth2WebServerFlow. |
1770 | 1168 |
1771 The kwargs argument is used to set extra query parameters on the | 1169 The kwargs argument is used to set extra query parameters on the |
1772 auth_uri. For example, the access_type and approval_prompt | 1170 auth_uri. For example, the access_type and approval_prompt |
1773 query parameters can be set via kwargs. | 1171 query parameters can be set via kwargs. |
1774 | 1172 |
1775 Args: | 1173 Args: |
1776 client_id: string, client identifier. | 1174 client_id: string, client identifier. |
1777 client_secret: string client secret. | 1175 client_secret: string client secret. |
1778 scope: string or iterable of strings, scope(s) of the credentials being | 1176 scope: string or iterable of strings, scope(s) of the credentials being |
1779 requested. | 1177 requested. |
1780 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for | 1178 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for |
1781 a non-web-based application, or a URI that handles the callback from | 1179 a non-web-based application, or a URI that handles the callback from |
1782 the authorization server. | 1180 the authorization server. |
1783 user_agent: string, HTTP User-Agent to provide for this application. | 1181 user_agent: string, HTTP User-Agent to provide for this application. |
1784 auth_uri: string, URI for authorization endpoint. For convenience | 1182 auth_uri: string, URI for authorization endpoint. For convenience |
1785 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | 1183 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
1786 token_uri: string, URI for token endpoint. For convenience | 1184 token_uri: string, URI for token endpoint. For convenience |
1787 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | 1185 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
1788 revoke_uri: string, URI for revoke endpoint. For convenience | 1186 revoke_uri: string, URI for revoke endpoint. For convenience |
1789 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | 1187 defaults to Google's endpoints but any OAuth 2.0 provider can be used. |
1790 login_hint: string, Either an email address or domain. Passing this hint | |
1791 will either pre-fill the email box on the sign-in form or select the | |
1792 proper multi-login session, thereby simplifying the login flow. | |
1793 device_uri: string, URI for device authorization endpoint. For convenience | |
1794 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
1795 **kwargs: dict, The keyword arguments are all optional and required | 1188 **kwargs: dict, The keyword arguments are all optional and required |
1796 parameters for the OAuth calls. | 1189 parameters for the OAuth calls. |
1797 """ | 1190 """ |
1798 self.client_id = client_id | 1191 self.client_id = client_id |
1799 self.client_secret = client_secret | 1192 self.client_secret = client_secret |
1800 self.scope = util.scopes_to_string(scope) | 1193 self.scope = util.scopes_to_string(scope) |
1801 self.redirect_uri = redirect_uri | 1194 self.redirect_uri = redirect_uri |
1802 self.login_hint = login_hint | |
1803 self.user_agent = user_agent | 1195 self.user_agent = user_agent |
1804 self.auth_uri = auth_uri | 1196 self.auth_uri = auth_uri |
1805 self.token_uri = token_uri | 1197 self.token_uri = token_uri |
1806 self.revoke_uri = revoke_uri | 1198 self.revoke_uri = revoke_uri |
1807 self.device_uri = device_uri | |
1808 self.params = { | 1199 self.params = { |
1809 'access_type': 'offline', | 1200 'access_type': 'offline', |
1810 'response_type': 'code', | 1201 'response_type': 'code', |
1811 } | 1202 } |
1812 self.params.update(kwargs) | 1203 self.params.update(kwargs) |
1813 | 1204 |
1814 @util.positional(1) | 1205 @util.positional(1) |
1815 def step1_get_authorize_url(self, redirect_uri=None): | 1206 def step1_get_authorize_url(self, redirect_uri=None): |
1816 """Returns a URI to redirect to the provider. | 1207 """Returns a URI to redirect to the provider. |
1817 | 1208 |
1818 Args: | 1209 Args: |
1819 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for | 1210 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for |
1820 a non-web-based application, or a URI that handles the callback from | 1211 a non-web-based application, or a URI that handles the callback from |
1821 the authorization server. This parameter is deprecated, please move to | 1212 the authorization server. This parameter is deprecated, please move to |
1822 passing the redirect_uri in via the constructor. | 1213 passing the redirect_uri in via the constructor. |
1823 | 1214 |
1824 Returns: | 1215 Returns: |
1825 A URI as a string to redirect the user to begin the authorization flow. | 1216 A URI as a string to redirect the user to begin the authorization flow. |
1826 """ | 1217 """ |
1827 if redirect_uri is not None: | 1218 if redirect_uri is not None: |
1828 logger.warning(( | 1219 logger.warning(('The redirect_uri parameter for' |
1829 'The redirect_uri parameter for ' | 1220 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please' |
1830 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please ' | |
1831 'move to passing the redirect_uri in via the constructor.')) | 1221 'move to passing the redirect_uri in via the constructor.')) |
1832 self.redirect_uri = redirect_uri | 1222 self.redirect_uri = redirect_uri |
1833 | 1223 |
1834 if self.redirect_uri is None: | 1224 if self.redirect_uri is None: |
1835 raise ValueError('The value of redirect_uri must not be None.') | 1225 raise ValueError('The value of redirect_uri must not be None.') |
1836 | 1226 |
1837 query_params = { | 1227 query_params = { |
1838 'client_id': self.client_id, | 1228 'client_id': self.client_id, |
1839 'redirect_uri': self.redirect_uri, | 1229 'redirect_uri': self.redirect_uri, |
1840 'scope': self.scope, | 1230 'scope': self.scope, |
1841 } | 1231 } |
1842 if self.login_hint is not None: | |
1843 query_params['login_hint'] = self.login_hint | |
1844 query_params.update(self.params) | 1232 query_params.update(self.params) |
1845 return _update_query_params(self.auth_uri, query_params) | 1233 return _update_query_params(self.auth_uri, query_params) |
1846 | 1234 |
1847 @util.positional(1) | |
1848 def step1_get_device_and_user_codes(self, http=None): | |
1849 """Returns a user code and the verification URL where to enter it | |
1850 | |
1851 Returns: | |
1852 A user code as a string for the user to authorize the application | |
1853 An URL as a string where the user has to enter the code | |
1854 """ | |
1855 if self.device_uri is None: | |
1856 raise ValueError('The value of device_uri must not be None.') | |
1857 | |
1858 body = urllib.parse.urlencode({ | |
1859 'client_id': self.client_id, | |
1860 'scope': self.scope, | |
1861 }) | |
1862 headers = { | |
1863 'content-type': 'application/x-www-form-urlencoded', | |
1864 } | |
1865 | |
1866 if self.user_agent is not None: | |
1867 headers['user-agent'] = self.user_agent | |
1868 | |
1869 if http is None: | |
1870 http = httplib2.Http() | |
1871 | |
1872 resp, content = http.request(self.device_uri, method='POST', body=body, | |
1873 headers=headers) | |
1874 if resp.status == 200: | |
1875 try: | |
1876 flow_info = json.loads(content) | |
1877 except ValueError as e: | |
1878 raise OAuth2DeviceCodeError( | |
1879 'Could not parse server response as JSON: "%s", error: "%s"' % ( | |
1880 content, e)) | |
1881 return DeviceFlowInfo.FromResponse(flow_info) | |
1882 else: | |
1883 error_msg = 'Invalid response %s.' % resp.status | |
1884 try: | |
1885 d = json.loads(content) | |
1886 if 'error' in d: | |
1887 error_msg += ' Error: %s' % d['error'] | |
1888 except ValueError: | |
1889 # Couldn't decode a JSON response, stick with the default message. | |
1890 pass | |
1891 raise OAuth2DeviceCodeError(error_msg) | |
1892 | |
1893 @util.positional(2) | 1235 @util.positional(2) |
1894 def step2_exchange(self, code=None, http=None, device_flow_info=None): | 1236 def step2_exchange(self, code, http=None): |
1895 """Exchanges a code for OAuth2Credentials. | 1237 """Exhanges a code for OAuth2Credentials. |
1896 | 1238 |
1897 Args: | 1239 Args: |
1898 | 1240 code: string or dict, either the code as a string, or a dictionary |
1899 code: string, a dict-like object, or None. For a non-device | 1241 of the query parameters to the redirect_uri, which contains |
1900 flow, this is either the response code as a string, or a | 1242 the code. |
1901 dictionary of query parameters to the redirect_uri. For a | 1243 http: httplib2.Http, optional http instance to use to do the fetch |
1902 device flow, this should be None. | |
1903 http: httplib2.Http, optional http instance to use when fetching | |
1904 credentials. | |
1905 device_flow_info: DeviceFlowInfo, return value from step1 in the | |
1906 case of a device flow. | |
1907 | 1244 |
1908 Returns: | 1245 Returns: |
1909 An OAuth2Credentials object that can be used to authorize requests. | 1246 An OAuth2Credentials object that can be used to authorize requests. |
1910 | 1247 |
1911 Raises: | 1248 Raises: |
1912 FlowExchangeError: if a problem occurred exchanging the code for a | 1249 FlowExchangeError if a problem occured exchanging the code for a |
1913 refresh_token. | 1250 refresh_token. |
1914 ValueError: if code and device_flow_info are both provided or both | 1251 """ |
1915 missing. | |
1916 | 1252 |
1917 """ | 1253 if not (isinstance(code, str) or isinstance(code, unicode)): |
1918 if code is None and device_flow_info is None: | 1254 if 'code' not in code: |
1919 raise ValueError('No code or device_flow_info provided.') | 1255 if 'error' in code: |
1920 if code is not None and device_flow_info is not None: | 1256 error_msg = code['error'] |
1921 raise ValueError('Cannot provide both code and device_flow_info.') | 1257 else: |
| 1258 error_msg = 'No code was supplied in the query parameters.' |
| 1259 raise FlowExchangeError(error_msg) |
| 1260 else: |
| 1261 code = code['code'] |
1922 | 1262 |
1923 if code is None: | 1263 body = urllib.urlencode({ |
1924 code = device_flow_info.device_code | 1264 'grant_type': 'authorization_code', |
1925 elif not isinstance(code, six.string_types): | |
1926 if 'code' not in code: | |
1927 raise FlowExchangeError(code.get( | |
1928 'error', 'No code was supplied in the query parameters.')) | |
1929 code = code['code'] | |
1930 | |
1931 post_data = { | |
1932 'client_id': self.client_id, | 1265 'client_id': self.client_id, |
1933 'client_secret': self.client_secret, | 1266 'client_secret': self.client_secret, |
1934 'code': code, | 1267 'code': code, |
| 1268 'redirect_uri': self.redirect_uri, |
1935 'scope': self.scope, | 1269 'scope': self.scope, |
1936 } | 1270 }) |
1937 if device_flow_info is not None: | |
1938 post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0' | |
1939 else: | |
1940 post_data['grant_type'] = 'authorization_code' | |
1941 post_data['redirect_uri'] = self.redirect_uri | |
1942 body = urllib.parse.urlencode(post_data) | |
1943 headers = { | 1271 headers = { |
1944 'content-type': 'application/x-www-form-urlencoded', | 1272 'content-type': 'application/x-www-form-urlencoded', |
1945 } | 1273 } |
1946 | 1274 |
1947 if self.user_agent is not None: | 1275 if self.user_agent is not None: |
1948 headers['user-agent'] = self.user_agent | 1276 headers['user-agent'] = self.user_agent |
1949 | 1277 |
1950 if http is None: | 1278 if http is None: |
1951 http = httplib2.Http() | 1279 http = httplib2.Http() |
1952 | 1280 |
1953 resp, content = http.request(self.token_uri, method='POST', body=body, | 1281 resp, content = http.request(self.token_uri, method='POST', body=body, |
1954 headers=headers) | 1282 headers=headers) |
1955 d = _parse_exchange_token_response(content) | 1283 d = _parse_exchange_token_response(content) |
1956 if resp.status == 200 and 'access_token' in d: | 1284 if resp.status == 200 and 'access_token' in d: |
1957 access_token = d['access_token'] | 1285 access_token = d['access_token'] |
1958 refresh_token = d.get('refresh_token', None) | 1286 refresh_token = d.get('refresh_token', None) |
1959 if not refresh_token: | |
1960 logger.info( | |
1961 'Received token response with no refresh_token. Consider ' | |
1962 "reauthenticating with approval_prompt='force'.") | |
1963 token_expiry = None | 1287 token_expiry = None |
1964 if 'expires_in' in d: | 1288 if 'expires_in' in d: |
1965 token_expiry = datetime.datetime.utcnow() + datetime.timedelta( | 1289 token_expiry = datetime.datetime.utcnow() + datetime.timedelta( |
1966 seconds=int(d['expires_in'])) | 1290 seconds=int(d['expires_in'])) |
1967 | 1291 |
1968 extracted_id_token = None | |
1969 if 'id_token' in d: | 1292 if 'id_token' in d: |
1970 extracted_id_token = _extract_id_token(d['id_token']) | 1293 d['id_token'] = _extract_id_token(d['id_token']) |
1971 | 1294 |
1972 logger.info('Successfully retrieved access token') | 1295 logger.info('Successfully retrieved access token') |
1973 return OAuth2Credentials(access_token, self.client_id, | 1296 return OAuth2Credentials(access_token, self.client_id, |
1974 self.client_secret, refresh_token, token_expiry, | 1297 self.client_secret, refresh_token, token_expiry, |
1975 self.token_uri, self.user_agent, | 1298 self.token_uri, self.user_agent, |
1976 revoke_uri=self.revoke_uri, | 1299 revoke_uri=self.revoke_uri, |
1977 id_token=extracted_id_token, | 1300 id_token=d.get('id_token', None), |
1978 token_response=d) | 1301 token_response=d) |
1979 else: | 1302 else: |
1980 logger.info('Failed to retrieve access token: %s', content) | 1303 logger.info('Failed to retrieve access token: %s' % content) |
1981 if 'error' in d: | 1304 if 'error' in d: |
1982 # you never know what those providers got to say | 1305 # you never know what those providers got to say |
1983 error_msg = str(d['error']) + str(d.get('error_description', '')) | 1306 error_msg = unicode(d['error']) |
1984 else: | 1307 else: |
1985 error_msg = 'Invalid response: %s.' % str(resp.status) | 1308 error_msg = 'Invalid response: %s.' % str(resp.status) |
1986 raise FlowExchangeError(error_msg) | 1309 raise FlowExchangeError(error_msg) |
1987 | 1310 |
1988 | 1311 |
1989 @util.positional(2) | 1312 @util.positional(2) |
1990 def flow_from_clientsecrets(filename, scope, redirect_uri=None, | 1313 def flow_from_clientsecrets(filename, scope, redirect_uri=None, |
1991 message=None, cache=None, login_hint=None, | 1314 message=None, cache=None): |
1992 device_uri=None): | |
1993 """Create a Flow from a clientsecrets file. | 1315 """Create a Flow from a clientsecrets file. |
1994 | 1316 |
1995 Will create the right kind of Flow based on the contents of the clientsecrets | 1317 Will create the right kind of Flow based on the contents of the clientsecrets |
1996 file or will raise InvalidClientSecretsError for unknown types of Flows. | 1318 file or will raise InvalidClientSecretsError for unknown types of Flows. |
1997 | 1319 |
1998 Args: | 1320 Args: |
1999 filename: string, File name of client secrets. | 1321 filename: string, File name of client secrets. |
2000 scope: string or iterable of strings, scope(s) to request. | 1322 scope: string or iterable of strings, scope(s) to request. |
2001 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for | 1323 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for |
2002 a non-web-based application, or a URI that handles the callback from | 1324 a non-web-based application, or a URI that handles the callback from |
2003 the authorization server. | 1325 the authorization server. |
2004 message: string, A friendly string to display to the user if the | 1326 message: string, A friendly string to display to the user if the |
2005 clientsecrets file is missing or invalid. If message is provided then | 1327 clientsecrets file is missing or invalid. If message is provided then |
2006 sys.exit will be called in the case of an error. If message in not | 1328 sys.exit will be called in the case of an error. If message in not |
2007 provided then clientsecrets.InvalidClientSecretsError will be raised. | 1329 provided then clientsecrets.InvalidClientSecretsError will be raised. |
2008 cache: An optional cache service client that implements get() and set() | 1330 cache: An optional cache service client that implements get() and set() |
2009 methods. See clientsecrets.loadfile() for details. | 1331 methods. See clientsecrets.loadfile() for details. |
2010 login_hint: string, Either an email address or domain. Passing this hint | |
2011 will either pre-fill the email box on the sign-in form or select the | |
2012 proper multi-login session, thereby simplifying the login flow. | |
2013 device_uri: string, URI for device authorization endpoint. For convenience | |
2014 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
2015 | 1332 |
2016 Returns: | 1333 Returns: |
2017 A Flow object. | 1334 A Flow object. |
2018 | 1335 |
2019 Raises: | 1336 Raises: |
2020 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. | 1337 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. |
2021 clientsecrets.InvalidClientSecretsError if the clientsecrets file is | 1338 clientsecrets.InvalidClientSecretsError if the clientsecrets file is |
2022 invalid. | 1339 invalid. |
2023 """ | 1340 """ |
2024 try: | 1341 try: |
2025 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) | 1342 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) |
2026 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): | 1343 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): |
2027 constructor_kwargs = { | 1344 constructor_kwargs = { |
2028 'redirect_uri': redirect_uri, | 1345 'redirect_uri': redirect_uri, |
2029 'auth_uri': client_info['auth_uri'], | 1346 'auth_uri': client_info['auth_uri'], |
2030 'token_uri': client_info['token_uri'], | 1347 'token_uri': client_info['token_uri'], |
2031 'login_hint': login_hint, | |
2032 } | 1348 } |
2033 revoke_uri = client_info.get('revoke_uri') | 1349 revoke_uri = client_info.get('revoke_uri') |
2034 if revoke_uri is not None: | 1350 if revoke_uri is not None: |
2035 constructor_kwargs['revoke_uri'] = revoke_uri | 1351 constructor_kwargs['revoke_uri'] = revoke_uri |
2036 if device_uri is not None: | |
2037 constructor_kwargs['device_uri'] = device_uri | |
2038 return OAuth2WebServerFlow( | 1352 return OAuth2WebServerFlow( |
2039 client_info['client_id'], client_info['client_secret'], | 1353 client_info['client_id'], client_info['client_secret'], |
2040 scope, **constructor_kwargs) | 1354 scope, **constructor_kwargs) |
2041 | 1355 |
2042 except clientsecrets.InvalidClientSecretsError: | 1356 except clientsecrets.InvalidClientSecretsError: |
2043 if message: | 1357 if message: |
2044 sys.exit(message) | 1358 sys.exit(message) |
2045 else: | 1359 else: |
2046 raise | 1360 raise |
2047 else: | 1361 else: |
2048 raise UnknownClientSecretsFlowError( | 1362 raise UnknownClientSecretsFlowError( |
2049 'This OAuth 2.0 flow is unsupported: %r' % client_type) | 1363 'This OAuth 2.0 flow is unsupported: %r' % client_type) |
OLD | NEW |