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

Side by Side Diff: third_party/oauth2client/client.py

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

Powered by Google App Engine
This is Rietveld 408576698