Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(191)

Side by Side Diff: reviewbot/third_party/google-api-python-client/oauth2client/client.py

Issue 20515002: Add google-api-python-client in third_party/ (Closed) Base URL: https://src.chromium.org/chrome/trunk/tools/
Patch Set: Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(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)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698