OLD | NEW |
| (Empty) |
1 # Copyright (C) 2010 Google Inc. | |
2 # | |
3 # Licensed under the Apache License, Version 2.0 (the "License"); | |
4 # you may not use this file except in compliance with the License. | |
5 # You may obtain a copy of the License at | |
6 # | |
7 # http://www.apache.org/licenses/LICENSE-2.0 | |
8 # | |
9 # Unless required by applicable law or agreed to in writing, software | |
10 # distributed under the License is distributed on an "AS IS" BASIS, | |
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 # See the License for the specific language governing permissions and | |
13 # limitations under the License. | |
14 | |
15 """An OAuth 2.0 client. | |
16 | |
17 Tools for interacting with OAuth 2.0 protected resources. | |
18 """ | |
19 | |
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)' | |
21 | |
22 import base64 | |
23 import clientsecrets | |
24 import copy | |
25 import datetime | |
26 import httplib2 | |
27 import logging | |
28 import os | |
29 import sys | |
30 import time | |
31 import urllib | |
32 import urlparse | |
33 | |
34 from oauth2client import GOOGLE_AUTH_URI | |
35 from oauth2client import GOOGLE_REVOKE_URI | |
36 from oauth2client import GOOGLE_TOKEN_URI | |
37 from oauth2client import util | |
38 from oauth2client.anyjson import simplejson | |
39 | |
40 HAS_OPENSSL = False | |
41 HAS_CRYPTO = False | |
42 try: | |
43 from oauth2client import crypt | |
44 HAS_CRYPTO = True | |
45 if crypt.OpenSSLVerifier is not None: | |
46 HAS_OPENSSL = True | |
47 except ImportError: | |
48 pass | |
49 | |
50 try: | |
51 from urlparse import parse_qsl | |
52 except ImportError: | |
53 from cgi import parse_qsl | |
54 | |
55 logger = logging.getLogger(__name__) | |
56 | |
57 # Expiry is stored in RFC3339 UTC format | |
58 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' | |
59 | |
60 # Which certs to use to validate id_tokens received. | |
61 ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' | |
62 | |
63 # Constant to use for the out of band OAuth 2.0 flow. | |
64 OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' | |
65 | |
66 # Google Data client libraries may need to set this to [401, 403]. | |
67 REFRESH_STATUS_CODES = [401] | |
68 | |
69 | |
70 class Error(Exception): | |
71 """Base error for this module.""" | |
72 | |
73 | |
74 class FlowExchangeError(Error): | |
75 """Error trying to exchange an authorization grant for an access token.""" | |
76 | |
77 | |
78 class AccessTokenRefreshError(Error): | |
79 """Error trying to refresh an expired access token.""" | |
80 | |
81 | |
82 class TokenRevokeError(Error): | |
83 """Error trying to revoke a token.""" | |
84 | |
85 | |
86 class UnknownClientSecretsFlowError(Error): | |
87 """The client secrets file called for an unknown type of OAuth 2.0 flow. """ | |
88 | |
89 | |
90 class AccessTokenCredentialsError(Error): | |
91 """Having only the access_token means no refresh is possible.""" | |
92 | |
93 | |
94 class VerifyJwtTokenError(Error): | |
95 """Could on retrieve certificates for validation.""" | |
96 | |
97 | |
98 class NonAsciiHeaderError(Error): | |
99 """Header names and values must be ASCII strings.""" | |
100 | |
101 | |
102 def _abstract(): | |
103 raise NotImplementedError('You need to override this function') | |
104 | |
105 | |
106 class MemoryCache(object): | |
107 """httplib2 Cache implementation which only caches locally.""" | |
108 | |
109 def __init__(self): | |
110 self.cache = {} | |
111 | |
112 def get(self, key): | |
113 return self.cache.get(key) | |
114 | |
115 def set(self, key, value): | |
116 self.cache[key] = value | |
117 | |
118 def delete(self, key): | |
119 self.cache.pop(key, None) | |
120 | |
121 | |
122 class Credentials(object): | |
123 """Base class for all Credentials objects. | |
124 | |
125 Subclasses must define an authorize() method that applies the credentials to | |
126 an HTTP transport. | |
127 | |
128 Subclasses must also specify a classmethod named 'from_json' that takes a JSON | |
129 string as input and returns an instaniated Credentials object. | |
130 """ | |
131 | |
132 NON_SERIALIZED_MEMBERS = ['store'] | |
133 | |
134 def authorize(self, http): | |
135 """Take an httplib2.Http instance (or equivalent) and authorizes it. | |
136 | |
137 Authorizes it for the set of credentials, usually by replacing | |
138 http.request() with a method that adds in the appropriate headers and then | |
139 delegates to the original Http.request() method. | |
140 | |
141 Args: | |
142 http: httplib2.Http, an http object to be used to make the refresh | |
143 request. | |
144 """ | |
145 _abstract() | |
146 | |
147 def refresh(self, http): | |
148 """Forces a refresh of the access_token. | |
149 | |
150 Args: | |
151 http: httplib2.Http, an http object to be used to make the refresh | |
152 request. | |
153 """ | |
154 _abstract() | |
155 | |
156 def revoke(self, http): | |
157 """Revokes a refresh_token and makes the credentials void. | |
158 | |
159 Args: | |
160 http: httplib2.Http, an http object to be used to make the revoke | |
161 request. | |
162 """ | |
163 _abstract() | |
164 | |
165 def apply(self, headers): | |
166 """Add the authorization to the headers. | |
167 | |
168 Args: | |
169 headers: dict, the headers to add the Authorization header to. | |
170 """ | |
171 _abstract() | |
172 | |
173 def _to_json(self, strip): | |
174 """Utility function that creates JSON repr. of a Credentials object. | |
175 | |
176 Args: | |
177 strip: array, An array of names of members to not include in the JSON. | |
178 | |
179 Returns: | |
180 string, a JSON representation of this instance, suitable to pass to | |
181 from_json(). | |
182 """ | |
183 t = type(self) | |
184 d = copy.copy(self.__dict__) | |
185 for member in strip: | |
186 if member in d: | |
187 del d[member] | |
188 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): | |
189 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) | |
190 # Add in information we will need later to reconsistitue this instance. | |
191 d['_class'] = t.__name__ | |
192 d['_module'] = t.__module__ | |
193 return simplejson.dumps(d) | |
194 | |
195 def to_json(self): | |
196 """Creating a JSON representation of an instance of Credentials. | |
197 | |
198 Returns: | |
199 string, a JSON representation of this instance, suitable to pass to | |
200 from_json(). | |
201 """ | |
202 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) | |
203 | |
204 @classmethod | |
205 def new_from_json(cls, s): | |
206 """Utility class method to instantiate a Credentials subclass from a JSON | |
207 representation produced by to_json(). | |
208 | |
209 Args: | |
210 s: string, JSON from to_json(). | |
211 | |
212 Returns: | |
213 An instance of the subclass of Credentials that was serialized with | |
214 to_json(). | |
215 """ | |
216 data = simplejson.loads(s) | |
217 # Find and call the right classmethod from_json() to restore the object. | |
218 module = data['_module'] | |
219 try: | |
220 m = __import__(module) | |
221 except ImportError: | |
222 # In case there's an object from the old package structure, update it | |
223 module = module.replace('.apiclient', '') | |
224 m = __import__(module) | |
225 | |
226 m = __import__(module, fromlist=module.split('.')[:-1]) | |
227 kls = getattr(m, data['_class']) | |
228 from_json = getattr(kls, 'from_json') | |
229 return from_json(s) | |
230 | |
231 @classmethod | |
232 def from_json(cls, s): | |
233 """Instantiate a Credentials object from a JSON description of it. | |
234 | |
235 The JSON should have been produced by calling .to_json() on the object. | |
236 | |
237 Args: | |
238 data: dict, A deserialized JSON object. | |
239 | |
240 Returns: | |
241 An instance of a Credentials subclass. | |
242 """ | |
243 return Credentials() | |
244 | |
245 | |
246 class Flow(object): | |
247 """Base class for all Flow objects.""" | |
248 pass | |
249 | |
250 | |
251 class Storage(object): | |
252 """Base class for all Storage objects. | |
253 | |
254 Store and retrieve a single credential. This class supports locking | |
255 such that multiple processes and threads can operate on a single | |
256 store. | |
257 """ | |
258 | |
259 def acquire_lock(self): | |
260 """Acquires any lock necessary to access this Storage. | |
261 | |
262 This lock is not reentrant. | |
263 """ | |
264 pass | |
265 | |
266 def release_lock(self): | |
267 """Release the Storage lock. | |
268 | |
269 Trying to release a lock that isn't held will result in a | |
270 RuntimeError. | |
271 """ | |
272 pass | |
273 | |
274 def locked_get(self): | |
275 """Retrieve credential. | |
276 | |
277 The Storage lock must be held when this is called. | |
278 | |
279 Returns: | |
280 oauth2client.client.Credentials | |
281 """ | |
282 _abstract() | |
283 | |
284 def locked_put(self, credentials): | |
285 """Write a credential. | |
286 | |
287 The Storage lock must be held when this is called. | |
288 | |
289 Args: | |
290 credentials: Credentials, the credentials to store. | |
291 """ | |
292 _abstract() | |
293 | |
294 def locked_delete(self): | |
295 """Delete a credential. | |
296 | |
297 The Storage lock must be held when this is called. | |
298 """ | |
299 _abstract() | |
300 | |
301 def get(self): | |
302 """Retrieve credential. | |
303 | |
304 The Storage lock must *not* be held when this is called. | |
305 | |
306 Returns: | |
307 oauth2client.client.Credentials | |
308 """ | |
309 self.acquire_lock() | |
310 try: | |
311 return self.locked_get() | |
312 finally: | |
313 self.release_lock() | |
314 | |
315 def put(self, credentials): | |
316 """Write a credential. | |
317 | |
318 The Storage lock must be held when this is called. | |
319 | |
320 Args: | |
321 credentials: Credentials, the credentials to store. | |
322 """ | |
323 self.acquire_lock() | |
324 try: | |
325 self.locked_put(credentials) | |
326 finally: | |
327 self.release_lock() | |
328 | |
329 def delete(self): | |
330 """Delete credential. | |
331 | |
332 Frees any resources associated with storing the credential. | |
333 The Storage lock must *not* be held when this is called. | |
334 | |
335 Returns: | |
336 None | |
337 """ | |
338 self.acquire_lock() | |
339 try: | |
340 return self.locked_delete() | |
341 finally: | |
342 self.release_lock() | |
343 | |
344 | |
345 def clean_headers(headers): | |
346 """Forces header keys and values to be strings, i.e not unicode. | |
347 | |
348 The httplib module just concats the header keys and values in a way that may | |
349 make the message header a unicode string, which, if it then tries to | |
350 contatenate to a binary request body may result in a unicode decode error. | |
351 | |
352 Args: | |
353 headers: dict, A dictionary of headers. | |
354 | |
355 Returns: | |
356 The same dictionary but with all the keys converted to strings. | |
357 """ | |
358 clean = {} | |
359 try: | |
360 for k, v in headers.iteritems(): | |
361 clean[str(k)] = str(v) | |
362 except UnicodeEncodeError: | |
363 raise NonAsciiHeaderError(k + ': ' + v) | |
364 return clean | |
365 | |
366 | |
367 def _update_query_params(uri, params): | |
368 """Updates a URI with new query parameters. | |
369 | |
370 Args: | |
371 uri: string, A valid URI, with potential existing query parameters. | |
372 params: dict, A dictionary of query parameters. | |
373 | |
374 Returns: | |
375 The same URI but with the new query parameters added. | |
376 """ | |
377 parts = list(urlparse.urlparse(uri)) | |
378 query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part | |
379 query_params.update(params) | |
380 parts[4] = urllib.urlencode(query_params) | |
381 return urlparse.urlunparse(parts) | |
382 | |
383 | |
384 class OAuth2Credentials(Credentials): | |
385 """Credentials object for OAuth 2.0. | |
386 | |
387 Credentials can be applied to an httplib2.Http object using the authorize() | |
388 method, which then adds the OAuth 2.0 access token to each request. | |
389 | |
390 OAuth2Credentials objects may be safely pickled and unpickled. | |
391 """ | |
392 | |
393 @util.positional(8) | |
394 def __init__(self, access_token, client_id, client_secret, refresh_token, | |
395 token_expiry, token_uri, user_agent, revoke_uri=None, | |
396 id_token=None, token_response=None): | |
397 """Create an instance of OAuth2Credentials. | |
398 | |
399 This constructor is not usually called by the user, instead | |
400 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow. | |
401 | |
402 Args: | |
403 access_token: string, access token. | |
404 client_id: string, client identifier. | |
405 client_secret: string, client secret. | |
406 refresh_token: string, refresh token. | |
407 token_expiry: datetime, when the access_token expires. | |
408 token_uri: string, URI of token endpoint. | |
409 user_agent: string, The HTTP User-Agent to provide for this application. | |
410 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token | |
411 can't be revoked if this is None. | |
412 id_token: object, The identity of the resource owner. | |
413 token_response: dict, the decoded response to the token request. None | |
414 if a token hasn't been requested yet. Stored because some providers | |
415 (e.g. wordpress.com) include extra fields that clients may want. | |
416 | |
417 Notes: | |
418 store: callable, A callable that when passed a Credential | |
419 will store the credential back to where it came from. | |
420 This is needed to store the latest access_token if it | |
421 has expired and been refreshed. | |
422 """ | |
423 self.access_token = access_token | |
424 self.client_id = client_id | |
425 self.client_secret = client_secret | |
426 self.refresh_token = refresh_token | |
427 self.store = None | |
428 self.token_expiry = token_expiry | |
429 self.token_uri = token_uri | |
430 self.user_agent = user_agent | |
431 self.revoke_uri = revoke_uri | |
432 self.id_token = id_token | |
433 self.token_response = token_response | |
434 | |
435 # True if the credentials have been revoked or expired and can't be | |
436 # refreshed. | |
437 self.invalid = False | |
438 | |
439 def authorize(self, http): | |
440 """Authorize an httplib2.Http instance with these credentials. | |
441 | |
442 The modified http.request method will add authentication headers to each | |
443 request and will refresh access_tokens when a 401 is received on a | |
444 request. In addition the http.request method has a credentials property, | |
445 http.request.credentials, which is the Credentials object that authorized | |
446 it. | |
447 | |
448 Args: | |
449 http: An instance of httplib2.Http | |
450 or something that acts like it. | |
451 | |
452 Returns: | |
453 A modified instance of http that was passed in. | |
454 | |
455 Example: | |
456 | |
457 h = httplib2.Http() | |
458 h = credentials.authorize(h) | |
459 | |
460 You can't create a new OAuth subclass of httplib2.Authenication | |
461 because it never gets passed the absolute URI, which is needed for | |
462 signing. So instead we have to overload 'request' with a closure | |
463 that adds in the Authorization header and then calls the original | |
464 version of 'request()'. | |
465 """ | |
466 request_orig = http.request | |
467 | |
468 # The closure that will replace 'httplib2.Http.request'. | |
469 @util.positional(1) | |
470 def new_request(uri, method='GET', body=None, headers=None, | |
471 redirections=httplib2.DEFAULT_MAX_REDIRECTS, | |
472 connection_type=None): | |
473 if not self.access_token: | |
474 logger.info('Attempting refresh to obtain initial access_token') | |
475 self._refresh(request_orig) | |
476 | |
477 # Modify the request headers to add the appropriate | |
478 # Authorization header. | |
479 if headers is None: | |
480 headers = {} | |
481 self.apply(headers) | |
482 | |
483 if self.user_agent is not None: | |
484 if 'user-agent' in headers: | |
485 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] | |
486 else: | |
487 headers['user-agent'] = self.user_agent | |
488 | |
489 resp, content = request_orig(uri, method, body, clean_headers(headers), | |
490 redirections, connection_type) | |
491 | |
492 if resp.status in REFRESH_STATUS_CODES: | |
493 logger.info('Refreshing due to a %s' % str(resp.status)) | |
494 self._refresh(request_orig) | |
495 self.apply(headers) | |
496 return request_orig(uri, method, body, clean_headers(headers), | |
497 redirections, connection_type) | |
498 else: | |
499 return (resp, content) | |
500 | |
501 # Replace the request method with our own closure. | |
502 http.request = new_request | |
503 | |
504 # Set credentials as a property of the request method. | |
505 setattr(http.request, 'credentials', self) | |
506 | |
507 return http | |
508 | |
509 def refresh(self, http): | |
510 """Forces a refresh of the access_token. | |
511 | |
512 Args: | |
513 http: httplib2.Http, an http object to be used to make the refresh | |
514 request. | |
515 """ | |
516 self._refresh(http.request) | |
517 | |
518 def revoke(self, http): | |
519 """Revokes a refresh_token and makes the credentials void. | |
520 | |
521 Args: | |
522 http: httplib2.Http, an http object to be used to make the revoke | |
523 request. | |
524 """ | |
525 self._revoke(http.request) | |
526 | |
527 def apply(self, headers): | |
528 """Add the authorization to the headers. | |
529 | |
530 Args: | |
531 headers: dict, the headers to add the Authorization header to. | |
532 """ | |
533 headers['Authorization'] = 'Bearer ' + self.access_token | |
534 | |
535 def to_json(self): | |
536 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) | |
537 | |
538 @classmethod | |
539 def from_json(cls, s): | |
540 """Instantiate a Credentials object from a JSON description of it. The JSON | |
541 should have been produced by calling .to_json() on the object. | |
542 | |
543 Args: | |
544 data: dict, A deserialized JSON object. | |
545 | |
546 Returns: | |
547 An instance of a Credentials subclass. | |
548 """ | |
549 data = simplejson.loads(s) | |
550 if 'token_expiry' in data and not isinstance(data['token_expiry'], | |
551 datetime.datetime): | |
552 try: | |
553 data['token_expiry'] = datetime.datetime.strptime( | |
554 data['token_expiry'], EXPIRY_FORMAT) | |
555 except: | |
556 data['token_expiry'] = None | |
557 retval = cls( | |
558 data['access_token'], | |
559 data['client_id'], | |
560 data['client_secret'], | |
561 data['refresh_token'], | |
562 data['token_expiry'], | |
563 data['token_uri'], | |
564 data['user_agent'], | |
565 revoke_uri=data.get('revoke_uri', None), | |
566 id_token=data.get('id_token', None), | |
567 token_response=data.get('token_response', None)) | |
568 retval.invalid = data['invalid'] | |
569 return retval | |
570 | |
571 @property | |
572 def access_token_expired(self): | |
573 """True if the credential is expired or invalid. | |
574 | |
575 If the token_expiry isn't set, we assume the token doesn't expire. | |
576 """ | |
577 if self.invalid: | |
578 return True | |
579 | |
580 if not self.token_expiry: | |
581 return False | |
582 | |
583 now = datetime.datetime.utcnow() | |
584 if now >= self.token_expiry: | |
585 logger.info('access_token is expired. Now: %s, token_expiry: %s', | |
586 now, self.token_expiry) | |
587 return True | |
588 return False | |
589 | |
590 def set_store(self, store): | |
591 """Set the Storage for the credential. | |
592 | |
593 Args: | |
594 store: Storage, an implementation of Stroage object. | |
595 This is needed to store the latest access_token if it | |
596 has expired and been refreshed. This implementation uses | |
597 locking to check for updates before updating the | |
598 access_token. | |
599 """ | |
600 self.store = store | |
601 | |
602 def _updateFromCredential(self, other): | |
603 """Update this Credential from another instance.""" | |
604 self.__dict__.update(other.__getstate__()) | |
605 | |
606 def __getstate__(self): | |
607 """Trim the state down to something that can be pickled.""" | |
608 d = copy.copy(self.__dict__) | |
609 del d['store'] | |
610 return d | |
611 | |
612 def __setstate__(self, state): | |
613 """Reconstitute the state of the object from being pickled.""" | |
614 self.__dict__.update(state) | |
615 self.store = None | |
616 | |
617 def _generate_refresh_request_body(self): | |
618 """Generate the body that will be used in the refresh request.""" | |
619 body = urllib.urlencode({ | |
620 'grant_type': 'refresh_token', | |
621 'client_id': self.client_id, | |
622 'client_secret': self.client_secret, | |
623 'refresh_token': self.refresh_token, | |
624 }) | |
625 return body | |
626 | |
627 def _generate_refresh_request_headers(self): | |
628 """Generate the headers that will be used in the refresh request.""" | |
629 headers = { | |
630 'content-type': 'application/x-www-form-urlencoded', | |
631 } | |
632 | |
633 if self.user_agent is not None: | |
634 headers['user-agent'] = self.user_agent | |
635 | |
636 return headers | |
637 | |
638 def _refresh(self, http_request): | |
639 """Refreshes the access_token. | |
640 | |
641 This method first checks by reading the Storage object if available. | |
642 If a refresh is still needed, it holds the Storage lock until the | |
643 refresh is completed. | |
644 | |
645 Args: | |
646 http_request: callable, a callable that matches the method signature of | |
647 httplib2.Http.request, used to make the refresh request. | |
648 | |
649 Raises: | |
650 AccessTokenRefreshError: When the refresh fails. | |
651 """ | |
652 if not self.store: | |
653 self._do_refresh_request(http_request) | |
654 else: | |
655 self.store.acquire_lock() | |
656 try: | |
657 new_cred = self.store.locked_get() | |
658 if (new_cred and not new_cred.invalid and | |
659 new_cred.access_token != self.access_token): | |
660 logger.info('Updated access_token read from Storage') | |
661 self._updateFromCredential(new_cred) | |
662 else: | |
663 self._do_refresh_request(http_request) | |
664 finally: | |
665 self.store.release_lock() | |
666 | |
667 def _do_refresh_request(self, http_request): | |
668 """Refresh the access_token using the refresh_token. | |
669 | |
670 Args: | |
671 http_request: callable, a callable that matches the method signature of | |
672 httplib2.Http.request, used to make the refresh request. | |
673 | |
674 Raises: | |
675 AccessTokenRefreshError: When the refresh fails. | |
676 """ | |
677 body = self._generate_refresh_request_body() | |
678 headers = self._generate_refresh_request_headers() | |
679 | |
680 logger.info('Refreshing access_token') | |
681 resp, content = http_request( | |
682 self.token_uri, method='POST', body=body, headers=headers) | |
683 if resp.status == 200: | |
684 # TODO(jcgregorio) Raise an error if loads fails? | |
685 d = simplejson.loads(content) | |
686 self.token_response = d | |
687 self.access_token = d['access_token'] | |
688 self.refresh_token = d.get('refresh_token', self.refresh_token) | |
689 if 'expires_in' in d: | |
690 self.token_expiry = datetime.timedelta( | |
691 seconds=int(d['expires_in'])) + datetime.datetime.utcnow() | |
692 else: | |
693 self.token_expiry = None | |
694 if self.store: | |
695 self.store.locked_put(self) | |
696 else: | |
697 # An {'error':...} response body means the token is expired or revoked, | |
698 # so we flag the credentials as such. | |
699 logger.info('Failed to retrieve access token: %s' % content) | |
700 error_msg = 'Invalid response %s.' % resp['status'] | |
701 try: | |
702 d = simplejson.loads(content) | |
703 if 'error' in d: | |
704 error_msg = d['error'] | |
705 self.invalid = True | |
706 if self.store: | |
707 self.store.locked_put(self) | |
708 except StandardError: | |
709 pass | |
710 raise AccessTokenRefreshError(error_msg) | |
711 | |
712 def _revoke(self, http_request): | |
713 """Revokes the refresh_token and deletes the store if available. | |
714 | |
715 Args: | |
716 http_request: callable, a callable that matches the method signature of | |
717 httplib2.Http.request, used to make the revoke request. | |
718 """ | |
719 self._do_revoke(http_request, self.refresh_token) | |
720 | |
721 def _do_revoke(self, http_request, token): | |
722 """Revokes the credentials and deletes the store if available. | |
723 | |
724 Args: | |
725 http_request: callable, a callable that matches the method signature of | |
726 httplib2.Http.request, used to make the refresh request. | |
727 token: A string used as the token to be revoked. Can be either an | |
728 access_token or refresh_token. | |
729 | |
730 Raises: | |
731 TokenRevokeError: If the revoke request does not return with a 200 OK. | |
732 """ | |
733 logger.info('Revoking token') | |
734 query_params = {'token': token} | |
735 token_revoke_uri = _update_query_params(self.revoke_uri, query_params) | |
736 resp, content = http_request(token_revoke_uri) | |
737 if resp.status == 200: | |
738 self.invalid = True | |
739 else: | |
740 error_msg = 'Invalid response %s.' % resp.status | |
741 try: | |
742 d = simplejson.loads(content) | |
743 if 'error' in d: | |
744 error_msg = d['error'] | |
745 except StandardError: | |
746 pass | |
747 raise TokenRevokeError(error_msg) | |
748 | |
749 if self.store: | |
750 self.store.delete() | |
751 | |
752 | |
753 class AccessTokenCredentials(OAuth2Credentials): | |
754 """Credentials object for OAuth 2.0. | |
755 | |
756 Credentials can be applied to an httplib2.Http object using the | |
757 authorize() method, which then signs each request from that object | |
758 with the OAuth 2.0 access token. This set of credentials is for the | |
759 use case where you have acquired an OAuth 2.0 access_token from | |
760 another place such as a JavaScript client or another web | |
761 application, and wish to use it from Python. Because only the | |
762 access_token is present it can not be refreshed and will in time | |
763 expire. | |
764 | |
765 AccessTokenCredentials objects may be safely pickled and unpickled. | |
766 | |
767 Usage: | |
768 credentials = AccessTokenCredentials('<an access token>', | |
769 'my-user-agent/1.0') | |
770 http = httplib2.Http() | |
771 http = credentials.authorize(http) | |
772 | |
773 Exceptions: | |
774 AccessTokenCredentialsExpired: raised when the access_token expires or is | |
775 revoked. | |
776 """ | |
777 | |
778 def __init__(self, access_token, user_agent, revoke_uri=None): | |
779 """Create an instance of OAuth2Credentials | |
780 | |
781 This is one of the few types if Credentials that you should contrust, | |
782 Credentials objects are usually instantiated by a Flow. | |
783 | |
784 Args: | |
785 access_token: string, access token. | |
786 user_agent: string, The HTTP User-Agent to provide for this application. | |
787 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token | |
788 can't be revoked if this is None. | |
789 """ | |
790 super(AccessTokenCredentials, self).__init__( | |
791 access_token, | |
792 None, | |
793 None, | |
794 None, | |
795 None, | |
796 None, | |
797 user_agent, | |
798 revoke_uri=revoke_uri) | |
799 | |
800 | |
801 @classmethod | |
802 def from_json(cls, s): | |
803 data = simplejson.loads(s) | |
804 retval = AccessTokenCredentials( | |
805 data['access_token'], | |
806 data['user_agent']) | |
807 return retval | |
808 | |
809 def _refresh(self, http_request): | |
810 raise AccessTokenCredentialsError( | |
811 'The access_token is expired or invalid and can\'t be refreshed.') | |
812 | |
813 def _revoke(self, http_request): | |
814 """Revokes the access_token and deletes the store if available. | |
815 | |
816 Args: | |
817 http_request: callable, a callable that matches the method signature of | |
818 httplib2.Http.request, used to make the revoke request. | |
819 """ | |
820 self._do_revoke(http_request, self.access_token) | |
821 | |
822 | |
823 class AssertionCredentials(OAuth2Credentials): | |
824 """Abstract Credentials object used for OAuth 2.0 assertion grants. | |
825 | |
826 This credential does not require a flow to instantiate because it | |
827 represents a two legged flow, and therefore has all of the required | |
828 information to generate and refresh its own access tokens. It must | |
829 be subclassed to generate the appropriate assertion string. | |
830 | |
831 AssertionCredentials objects may be safely pickled and unpickled. | |
832 """ | |
833 | |
834 @util.positional(2) | |
835 def __init__(self, assertion_type, user_agent=None, | |
836 token_uri=GOOGLE_TOKEN_URI, | |
837 revoke_uri=GOOGLE_REVOKE_URI, | |
838 **unused_kwargs): | |
839 """Constructor for AssertionFlowCredentials. | |
840 | |
841 Args: | |
842 assertion_type: string, assertion type that will be declared to the auth | |
843 server | |
844 user_agent: string, The HTTP User-Agent to provide for this application. | |
845 token_uri: string, URI for token endpoint. For convenience | |
846 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
847 revoke_uri: string, URI for revoke endpoint. | |
848 """ | |
849 super(AssertionCredentials, self).__init__( | |
850 None, | |
851 None, | |
852 None, | |
853 None, | |
854 None, | |
855 token_uri, | |
856 user_agent, | |
857 revoke_uri=revoke_uri) | |
858 self.assertion_type = assertion_type | |
859 | |
860 def _generate_refresh_request_body(self): | |
861 assertion = self._generate_assertion() | |
862 | |
863 body = urllib.urlencode({ | |
864 'assertion': assertion, | |
865 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', | |
866 }) | |
867 | |
868 return body | |
869 | |
870 def _generate_assertion(self): | |
871 """Generate the assertion string that will be used in the access token | |
872 request. | |
873 """ | |
874 _abstract() | |
875 | |
876 def _revoke(self, http_request): | |
877 """Revokes the access_token and deletes the store if available. | |
878 | |
879 Args: | |
880 http_request: callable, a callable that matches the method signature of | |
881 httplib2.Http.request, used to make the revoke request. | |
882 """ | |
883 self._do_revoke(http_request, self.access_token) | |
884 | |
885 | |
886 if HAS_CRYPTO: | |
887 # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is | |
888 # missing then don't create the SignedJwtAssertionCredentials or the | |
889 # verify_id_token() method. | |
890 | |
891 class SignedJwtAssertionCredentials(AssertionCredentials): | |
892 """Credentials object used for OAuth 2.0 Signed JWT assertion grants. | |
893 | |
894 This credential does not require a flow to instantiate because it represents | |
895 a two legged flow, and therefore has all of the required information to | |
896 generate and refresh its own access tokens. | |
897 | |
898 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or | |
899 later. For App Engine you may also consider using AppAssertionCredentials. | |
900 """ | |
901 | |
902 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds | |
903 | |
904 @util.positional(4) | |
905 def __init__(self, | |
906 service_account_name, | |
907 private_key, | |
908 scope, | |
909 private_key_password='notasecret', | |
910 user_agent=None, | |
911 token_uri=GOOGLE_TOKEN_URI, | |
912 revoke_uri=GOOGLE_REVOKE_URI, | |
913 **kwargs): | |
914 """Constructor for SignedJwtAssertionCredentials. | |
915 | |
916 Args: | |
917 service_account_name: string, id for account, usually an email address. | |
918 private_key: string, private key in PKCS12 or PEM format. | |
919 scope: string or iterable of strings, scope(s) of the credentials being | |
920 requested. | |
921 private_key_password: string, password for private_key, unused if | |
922 private_key is in PEM format. | |
923 user_agent: string, HTTP User-Agent to provide for this application. | |
924 token_uri: string, URI for token endpoint. For convenience | |
925 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
926 revoke_uri: string, URI for revoke endpoint. | |
927 kwargs: kwargs, Additional parameters to add to the JWT token, for | |
928 example prn=joe@xample.org.""" | |
929 | |
930 super(SignedJwtAssertionCredentials, self).__init__( | |
931 None, | |
932 user_agent=user_agent, | |
933 token_uri=token_uri, | |
934 revoke_uri=revoke_uri, | |
935 ) | |
936 | |
937 self.scope = util.scopes_to_string(scope) | |
938 | |
939 # Keep base64 encoded so it can be stored in JSON. | |
940 self.private_key = base64.b64encode(private_key) | |
941 | |
942 self.private_key_password = private_key_password | |
943 self.service_account_name = service_account_name | |
944 self.kwargs = kwargs | |
945 | |
946 @classmethod | |
947 def from_json(cls, s): | |
948 data = simplejson.loads(s) | |
949 retval = SignedJwtAssertionCredentials( | |
950 data['service_account_name'], | |
951 base64.b64decode(data['private_key']), | |
952 data['scope'], | |
953 private_key_password=data['private_key_password'], | |
954 user_agent=data['user_agent'], | |
955 token_uri=data['token_uri'], | |
956 **data['kwargs'] | |
957 ) | |
958 retval.invalid = data['invalid'] | |
959 retval.access_token = data['access_token'] | |
960 return retval | |
961 | |
962 def _generate_assertion(self): | |
963 """Generate the assertion that will be used in the request.""" | |
964 now = long(time.time()) | |
965 payload = { | |
966 'aud': self.token_uri, | |
967 'scope': self.scope, | |
968 'iat': now, | |
969 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, | |
970 'iss': self.service_account_name | |
971 } | |
972 payload.update(self.kwargs) | |
973 logger.debug(str(payload)) | |
974 | |
975 private_key = base64.b64decode(self.private_key) | |
976 return crypt.make_signed_jwt(crypt.Signer.from_string( | |
977 private_key, self.private_key_password), payload) | |
978 | |
979 # Only used in verify_id_token(), which is always calling to the same URI | |
980 # for the certs. | |
981 _cached_http = httplib2.Http(MemoryCache()) | |
982 | |
983 @util.positional(2) | |
984 def verify_id_token(id_token, audience, http=None, | |
985 cert_uri=ID_TOKEN_VERIFICATON_CERTS): | |
986 """Verifies a signed JWT id_token. | |
987 | |
988 This function requires PyOpenSSL and because of that it does not work on | |
989 App Engine. | |
990 | |
991 Args: | |
992 id_token: string, A Signed JWT. | |
993 audience: string, The audience 'aud' that the token should be for. | |
994 http: httplib2.Http, instance to use to make the HTTP request. Callers | |
995 should supply an instance that has caching enabled. | |
996 cert_uri: string, URI of the certificates in JSON format to | |
997 verify the JWT against. | |
998 | |
999 Returns: | |
1000 The deserialized JSON in the JWT. | |
1001 | |
1002 Raises: | |
1003 oauth2client.crypt.AppIdentityError if the JWT fails to verify. | |
1004 """ | |
1005 if http is None: | |
1006 http = _cached_http | |
1007 | |
1008 resp, content = http.request(cert_uri) | |
1009 | |
1010 if resp.status == 200: | |
1011 certs = simplejson.loads(content) | |
1012 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) | |
1013 else: | |
1014 raise VerifyJwtTokenError('Status code: %d' % resp.status) | |
1015 | |
1016 | |
1017 def _urlsafe_b64decode(b64string): | |
1018 # Guard against unicode strings, which base64 can't handle. | |
1019 b64string = b64string.encode('ascii') | |
1020 padded = b64string + '=' * (4 - len(b64string) % 4) | |
1021 return base64.urlsafe_b64decode(padded) | |
1022 | |
1023 | |
1024 def _extract_id_token(id_token): | |
1025 """Extract the JSON payload from a JWT. | |
1026 | |
1027 Does the extraction w/o checking the signature. | |
1028 | |
1029 Args: | |
1030 id_token: string, OAuth 2.0 id_token. | |
1031 | |
1032 Returns: | |
1033 object, The deserialized JSON payload. | |
1034 """ | |
1035 segments = id_token.split('.') | |
1036 | |
1037 if (len(segments) != 3): | |
1038 raise VerifyJwtTokenError( | |
1039 'Wrong number of segments in token: %s' % id_token) | |
1040 | |
1041 return simplejson.loads(_urlsafe_b64decode(segments[1])) | |
1042 | |
1043 | |
1044 def _parse_exchange_token_response(content): | |
1045 """Parses response of an exchange token request. | |
1046 | |
1047 Most providers return JSON but some (e.g. Facebook) return a | |
1048 url-encoded string. | |
1049 | |
1050 Args: | |
1051 content: The body of a response | |
1052 | |
1053 Returns: | |
1054 Content as a dictionary object. Note that the dict could be empty, | |
1055 i.e. {}. That basically indicates a failure. | |
1056 """ | |
1057 resp = {} | |
1058 try: | |
1059 resp = simplejson.loads(content) | |
1060 except StandardError: | |
1061 # different JSON libs raise different exceptions, | |
1062 # so we just do a catch-all here | |
1063 resp = dict(parse_qsl(content)) | |
1064 | |
1065 # some providers respond with 'expires', others with 'expires_in' | |
1066 if resp and 'expires' in resp: | |
1067 resp['expires_in'] = resp.pop('expires') | |
1068 | |
1069 return resp | |
1070 | |
1071 | |
1072 @util.positional(4) | |
1073 def credentials_from_code(client_id, client_secret, scope, code, | |
1074 redirect_uri='postmessage', http=None, | |
1075 user_agent=None, token_uri=GOOGLE_TOKEN_URI, | |
1076 auth_uri=GOOGLE_AUTH_URI, | |
1077 revoke_uri=GOOGLE_REVOKE_URI): | |
1078 """Exchanges an authorization code for an OAuth2Credentials object. | |
1079 | |
1080 Args: | |
1081 client_id: string, client identifier. | |
1082 client_secret: string, client secret. | |
1083 scope: string or iterable of strings, scope(s) to request. | |
1084 code: string, An authroization code, most likely passed down from | |
1085 the client | |
1086 redirect_uri: string, this is generally set to 'postmessage' to match the | |
1087 redirect_uri that the client specified | |
1088 http: httplib2.Http, optional http instance to use to do the fetch | |
1089 token_uri: string, URI for token endpoint. For convenience | |
1090 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
1091 auth_uri: string, URI for authorization endpoint. For convenience | |
1092 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
1093 revoke_uri: string, URI for revoke endpoint. For convenience | |
1094 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
1095 | |
1096 Returns: | |
1097 An OAuth2Credentials object. | |
1098 | |
1099 Raises: | |
1100 FlowExchangeError if the authorization code cannot be exchanged for an | |
1101 access token | |
1102 """ | |
1103 flow = OAuth2WebServerFlow(client_id, client_secret, scope, | |
1104 redirect_uri=redirect_uri, user_agent=user_agent, | |
1105 auth_uri=auth_uri, token_uri=token_uri, | |
1106 revoke_uri=revoke_uri) | |
1107 | |
1108 credentials = flow.step2_exchange(code, http=http) | |
1109 return credentials | |
1110 | |
1111 | |
1112 @util.positional(3) | |
1113 def credentials_from_clientsecrets_and_code(filename, scope, code, | |
1114 message = None, | |
1115 redirect_uri='postmessage', | |
1116 http=None, | |
1117 cache=None): | |
1118 """Returns OAuth2Credentials from a clientsecrets file and an auth code. | |
1119 | |
1120 Will create the right kind of Flow based on the contents of the clientsecrets | |
1121 file or will raise InvalidClientSecretsError for unknown types of Flows. | |
1122 | |
1123 Args: | |
1124 filename: string, File name of clientsecrets. | |
1125 scope: string or iterable of strings, scope(s) to request. | |
1126 code: string, An authorization code, most likely passed down from | |
1127 the client | |
1128 message: string, A friendly string to display to the user if the | |
1129 clientsecrets file is missing or invalid. If message is provided then | |
1130 sys.exit will be called in the case of an error. If message in not | |
1131 provided then clientsecrets.InvalidClientSecretsError will be raised. | |
1132 redirect_uri: string, this is generally set to 'postmessage' to match the | |
1133 redirect_uri that the client specified | |
1134 http: httplib2.Http, optional http instance to use to do the fetch | |
1135 cache: An optional cache service client that implements get() and set() | |
1136 methods. See clientsecrets.loadfile() for details. | |
1137 | |
1138 Returns: | |
1139 An OAuth2Credentials object. | |
1140 | |
1141 Raises: | |
1142 FlowExchangeError if the authorization code cannot be exchanged for an | |
1143 access token | |
1144 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. | |
1145 clientsecrets.InvalidClientSecretsError if the clientsecrets file is | |
1146 invalid. | |
1147 """ | |
1148 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, | |
1149 redirect_uri=redirect_uri) | |
1150 credentials = flow.step2_exchange(code, http=http) | |
1151 return credentials | |
1152 | |
1153 | |
1154 class OAuth2WebServerFlow(Flow): | |
1155 """Does the Web Server Flow for OAuth 2.0. | |
1156 | |
1157 OAuth2WebServerFlow objects may be safely pickled and unpickled. | |
1158 """ | |
1159 | |
1160 @util.positional(4) | |
1161 def __init__(self, client_id, client_secret, scope, | |
1162 redirect_uri=None, | |
1163 user_agent=None, | |
1164 auth_uri=GOOGLE_AUTH_URI, | |
1165 token_uri=GOOGLE_TOKEN_URI, | |
1166 revoke_uri=GOOGLE_REVOKE_URI, | |
1167 **kwargs): | |
1168 """Constructor for OAuth2WebServerFlow. | |
1169 | |
1170 The kwargs argument is used to set extra query parameters on the | |
1171 auth_uri. For example, the access_type and approval_prompt | |
1172 query parameters can be set via kwargs. | |
1173 | |
1174 Args: | |
1175 client_id: string, client identifier. | |
1176 client_secret: string client secret. | |
1177 scope: string or iterable of strings, scope(s) of the credentials being | |
1178 requested. | |
1179 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for | |
1180 a non-web-based application, or a URI that handles the callback from | |
1181 the authorization server. | |
1182 user_agent: string, HTTP User-Agent to provide for this application. | |
1183 auth_uri: string, URI for authorization endpoint. For convenience | |
1184 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
1185 token_uri: string, URI for token endpoint. For convenience | |
1186 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
1187 revoke_uri: string, URI for revoke endpoint. For convenience | |
1188 defaults to Google's endpoints but any OAuth 2.0 provider can be used. | |
1189 **kwargs: dict, The keyword arguments are all optional and required | |
1190 parameters for the OAuth calls. | |
1191 """ | |
1192 self.client_id = client_id | |
1193 self.client_secret = client_secret | |
1194 self.scope = util.scopes_to_string(scope) | |
1195 self.redirect_uri = redirect_uri | |
1196 self.user_agent = user_agent | |
1197 self.auth_uri = auth_uri | |
1198 self.token_uri = token_uri | |
1199 self.revoke_uri = revoke_uri | |
1200 self.params = { | |
1201 'access_type': 'offline', | |
1202 'response_type': 'code', | |
1203 } | |
1204 self.params.update(kwargs) | |
1205 | |
1206 @util.positional(1) | |
1207 def step1_get_authorize_url(self, redirect_uri=None): | |
1208 """Returns a URI to redirect to the provider. | |
1209 | |
1210 Args: | |
1211 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for | |
1212 a non-web-based application, or a URI that handles the callback from | |
1213 the authorization server. This parameter is deprecated, please move to | |
1214 passing the redirect_uri in via the constructor. | |
1215 | |
1216 Returns: | |
1217 A URI as a string to redirect the user to begin the authorization flow. | |
1218 """ | |
1219 if redirect_uri is not None: | |
1220 logger.warning(('The redirect_uri parameter for' | |
1221 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please' | |
1222 'move to passing the redirect_uri in via the constructor.')) | |
1223 self.redirect_uri = redirect_uri | |
1224 | |
1225 if self.redirect_uri is None: | |
1226 raise ValueError('The value of redirect_uri must not be None.') | |
1227 | |
1228 query_params = { | |
1229 'client_id': self.client_id, | |
1230 'redirect_uri': self.redirect_uri, | |
1231 'scope': self.scope, | |
1232 } | |
1233 query_params.update(self.params) | |
1234 return _update_query_params(self.auth_uri, query_params) | |
1235 | |
1236 @util.positional(2) | |
1237 def step2_exchange(self, code, http=None): | |
1238 """Exhanges a code for OAuth2Credentials. | |
1239 | |
1240 Args: | |
1241 code: string or dict, either the code as a string, or a dictionary | |
1242 of the query parameters to the redirect_uri, which contains | |
1243 the code. | |
1244 http: httplib2.Http, optional http instance to use to do the fetch | |
1245 | |
1246 Returns: | |
1247 An OAuth2Credentials object that can be used to authorize requests. | |
1248 | |
1249 Raises: | |
1250 FlowExchangeError if a problem occured exchanging the code for a | |
1251 refresh_token. | |
1252 """ | |
1253 | |
1254 if not (isinstance(code, str) or isinstance(code, unicode)): | |
1255 if 'code' not in code: | |
1256 if 'error' in code: | |
1257 error_msg = code['error'] | |
1258 else: | |
1259 error_msg = 'No code was supplied in the query parameters.' | |
1260 raise FlowExchangeError(error_msg) | |
1261 else: | |
1262 code = code['code'] | |
1263 | |
1264 body = urllib.urlencode({ | |
1265 'grant_type': 'authorization_code', | |
1266 'client_id': self.client_id, | |
1267 'client_secret': self.client_secret, | |
1268 'code': code, | |
1269 'redirect_uri': self.redirect_uri, | |
1270 'scope': self.scope, | |
1271 }) | |
1272 headers = { | |
1273 'content-type': 'application/x-www-form-urlencoded', | |
1274 } | |
1275 | |
1276 if self.user_agent is not None: | |
1277 headers['user-agent'] = self.user_agent | |
1278 | |
1279 if http is None: | |
1280 http = httplib2.Http() | |
1281 | |
1282 resp, content = http.request(self.token_uri, method='POST', body=body, | |
1283 headers=headers) | |
1284 d = _parse_exchange_token_response(content) | |
1285 if resp.status == 200 and 'access_token' in d: | |
1286 access_token = d['access_token'] | |
1287 refresh_token = d.get('refresh_token', None) | |
1288 token_expiry = None | |
1289 if 'expires_in' in d: | |
1290 token_expiry = datetime.datetime.utcnow() + datetime.timedelta( | |
1291 seconds=int(d['expires_in'])) | |
1292 | |
1293 if 'id_token' in d: | |
1294 d['id_token'] = _extract_id_token(d['id_token']) | |
1295 | |
1296 logger.info('Successfully retrieved access token') | |
1297 return OAuth2Credentials(access_token, self.client_id, | |
1298 self.client_secret, refresh_token, token_expiry, | |
1299 self.token_uri, self.user_agent, | |
1300 revoke_uri=self.revoke_uri, | |
1301 id_token=d.get('id_token', None), | |
1302 token_response=d) | |
1303 else: | |
1304 logger.info('Failed to retrieve access token: %s' % content) | |
1305 if 'error' in d: | |
1306 # you never know what those providers got to say | |
1307 error_msg = unicode(d['error']) | |
1308 else: | |
1309 error_msg = 'Invalid response: %s.' % str(resp.status) | |
1310 raise FlowExchangeError(error_msg) | |
1311 | |
1312 | |
1313 @util.positional(2) | |
1314 def flow_from_clientsecrets(filename, scope, redirect_uri=None, | |
1315 message=None, cache=None): | |
1316 """Create a Flow from a clientsecrets file. | |
1317 | |
1318 Will create the right kind of Flow based on the contents of the clientsecrets | |
1319 file or will raise InvalidClientSecretsError for unknown types of Flows. | |
1320 | |
1321 Args: | |
1322 filename: string, File name of client secrets. | |
1323 scope: string or iterable of strings, scope(s) to request. | |
1324 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for | |
1325 a non-web-based application, or a URI that handles the callback from | |
1326 the authorization server. | |
1327 message: string, A friendly string to display to the user if the | |
1328 clientsecrets file is missing or invalid. If message is provided then | |
1329 sys.exit will be called in the case of an error. If message in not | |
1330 provided then clientsecrets.InvalidClientSecretsError will be raised. | |
1331 cache: An optional cache service client that implements get() and set() | |
1332 methods. See clientsecrets.loadfile() for details. | |
1333 | |
1334 Returns: | |
1335 A Flow object. | |
1336 | |
1337 Raises: | |
1338 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. | |
1339 clientsecrets.InvalidClientSecretsError if the clientsecrets file is | |
1340 invalid. | |
1341 """ | |
1342 try: | |
1343 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) | |
1344 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): | |
1345 constructor_kwargs = { | |
1346 'redirect_uri': redirect_uri, | |
1347 'auth_uri': client_info['auth_uri'], | |
1348 'token_uri': client_info['token_uri'], | |
1349 } | |
1350 revoke_uri = client_info.get('revoke_uri') | |
1351 if revoke_uri is not None: | |
1352 constructor_kwargs['revoke_uri'] = revoke_uri | |
1353 return OAuth2WebServerFlow( | |
1354 client_info['client_id'], client_info['client_secret'], | |
1355 scope, **constructor_kwargs) | |
1356 | |
1357 except clientsecrets.InvalidClientSecretsError: | |
1358 if message: | |
1359 sys.exit(message) | |
1360 else: | |
1361 raise | |
1362 else: | |
1363 raise UnknownClientSecretsFlowError( | |
1364 'This OAuth 2.0 flow is unsupported: %r' % client_type) | |
OLD | NEW |