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

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: Added another option 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
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
Vadim Sh. 2014/03/13 18:28:42 Did you modify this code? I remember it used to be
pgervais 2014/03/13 22:38:38 I haven't modified anything in httplib2, I just do
Vadim Sh. 2014/03/13 22:46:26 Modifying is fine. What I say is that it's useful
pgervais 2014/03/13 23:04:25 What you've done looks great. I'll do the same.
27 import logging
28 import sys
29 import time
30 import urllib
31 import urlparse
32
33 from anyjson import simplejson
34
35 HAS_OPENSSL = False
36 try:
37 from .crypt import Signer
38 from .crypt import make_signed_jwt
39 from .crypt import verify_signed_jwt_with_certs
40 HAS_OPENSSL = True
41 except ImportError:
42 pass
43
44 try:
45 from urlparse import parse_qsl
46 except ImportError:
47 from cgi import parse_qsl
48
49 logger = logging.getLogger(__name__)
50
51 # Expiry is stored in RFC3339 UTC format
52 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
53
54 # Which certs to use to validate id_tokens received.
55 ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
56
57
58 class Error(Exception):
59 """Base error for this module."""
60 pass
61
62
63 class FlowExchangeError(Error):
64 """Error trying to exchange an authorization grant for an access token."""
65 pass
66
67
68 class AccessTokenRefreshError(Error):
69 """Error trying to refresh an expired access token."""
70 pass
71
72 class UnknownClientSecretsFlowError(Error):
73 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
74 pass
75
76
77 class AccessTokenCredentialsError(Error):
78 """Having only the access_token means no refresh is possible."""
79 pass
80
81
82 class VerifyJwtTokenError(Error):
83 """Could on retrieve certificates for validation."""
84 pass
85
86
87 def _abstract():
88 raise NotImplementedError('You need to override this function')
89
90
91 class Credentials(object):
92 """Base class for all Credentials objects.
93
94 Subclasses must define an authorize() method that applies the credentials to
95 an HTTP transport.
96
97 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
98 string as input and returns an instaniated Crentials object.
99 """
100
101 NON_SERIALIZED_MEMBERS = ['store']
102
103 def authorize(self, http):
104 """Take an httplib2.Http instance (or equivalent) and
105 authorizes it for the set of credentials, usually by
106 replacing http.request() with a method that adds in
107 the appropriate headers and then delegates to the original
108 Http.request() method.
109 """
110 _abstract()
111
112 def _to_json(self, strip):
113 """Utility function for creating a JSON representation of an instance of Cre dentials.
114
115 Args:
116 strip: array, An array of names of members to not include in the JSON.
117
118 Returns:
119 string, a JSON representation of this instance, suitable to pass to
120 from_json().
121 """
122 t = type(self)
123 d = copy.copy(self.__dict__)
124 for member in strip:
125 del d[member]
126 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
127 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
128 # Add in information we will need later to reconsistitue this instance.
129 d['_class'] = t.__name__
130 d['_module'] = t.__module__
131 return simplejson.dumps(d)
132
133 def to_json(self):
134 """Creating a JSON representation of an instance of Credentials.
135
136 Returns:
137 string, a JSON representation of this instance, suitable to pass to
138 from_json().
139 """
140 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
141
142 @classmethod
143 def new_from_json(cls, s):
144 """Utility class method to instantiate a Credentials subclass from a JSON
145 representation produced by to_json().
146
147 Args:
148 s: string, JSON from to_json().
149
150 Returns:
151 An instance of the subclass of Credentials that was serialized with
152 to_json().
153 """
154 data = simplejson.loads(s)
155 # Find and call the right classmethod from_json() to restore the object.
156 module = data['_module']
157 m = __import__(module, fromlist=module.split('.')[:-1])
158 kls = getattr(m, data['_class'])
159 from_json = getattr(kls, 'from_json')
160 return from_json(s)
161
162
163 class Flow(object):
164 """Base class for all Flow objects."""
165 pass
166
167
168 class Storage(object):
169 """Base class for all Storage objects.
170
171 Store and retrieve a single credential. This class supports locking
172 such that multiple processes and threads can operate on a single
173 store.
174 """
175
176 def acquire_lock(self):
177 """Acquires any lock necessary to access this Storage.
178
179 This lock is not reentrant."""
180 pass
181
182 def release_lock(self):
183 """Release the Storage lock.
184
185 Trying to release a lock that isn't held will result in a
186 RuntimeError.
187 """
188 pass
189
190 def locked_get(self):
191 """Retrieve credential.
192
193 The Storage lock must be held when this is called.
194
195 Returns:
196 oauth2client.client.Credentials
197 """
198 _abstract()
199
200 def locked_put(self, credentials):
201 """Write a credential.
202
203 The Storage lock must be held when this is called.
204
205 Args:
206 credentials: Credentials, the credentials to store.
207 """
208 _abstract()
209
210 def get(self):
211 """Retrieve credential.
212
213 The Storage lock must *not* be held when this is called.
214
215 Returns:
216 oauth2client.client.Credentials
217 """
218 self.acquire_lock()
219 try:
220 return self.locked_get()
221 finally:
222 self.release_lock()
223
224 def put(self, credentials):
225 """Write a credential.
226
227 The Storage lock must be held when this is called.
228
229 Args:
230 credentials: Credentials, the credentials to store.
231 """
232 self.acquire_lock()
233 try:
234 self.locked_put(credentials)
235 finally:
236 self.release_lock()
237
238
239 class OAuth2Credentials(Credentials):
240 """Credentials object for OAuth 2.0.
241
242 Credentials can be applied to an httplib2.Http object using the authorize()
243 method, which then adds the OAuth 2.0 access token to each request.
244
245 OAuth2Credentials objects may be safely pickled and unpickled.
246 """
247
248 def __init__(self, access_token, client_id, client_secret, refresh_token,
249 token_expiry, token_uri, user_agent, id_token=None):
250 """Create an instance of OAuth2Credentials.
251
252 This constructor is not usually called by the user, instead
253 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
254
255 Args:
256 access_token: string, access token.
257 client_id: string, client identifier.
258 client_secret: string, client secret.
259 refresh_token: string, refresh token.
260 token_expiry: datetime, when the access_token expires.
261 token_uri: string, URI of token endpoint.
262 user_agent: string, The HTTP User-Agent to provide for this application.
263 id_token: object, The identity of the resource owner.
264
265 Notes:
266 store: callable, A callable that when passed a Credential
267 will store the credential back to where it came from.
268 This is needed to store the latest access_token if it
269 has expired and been refreshed.
270 """
271 self.access_token = access_token
272 self.client_id = client_id
273 self.client_secret = client_secret
274 self.refresh_token = refresh_token
275 self.store = None
276 self.token_expiry = token_expiry
277 self.token_uri = token_uri
278 self.user_agent = user_agent
279 self.id_token = id_token
280
281 # True if the credentials have been revoked or expired and can't be
282 # refreshed.
283 self.invalid = False
284
285 def to_json(self):
286 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
287
288 @classmethod
289 def from_json(cls, s):
290 """Instantiate a Credentials object from a JSON description of it. The JSON
291 should have been produced by calling .to_json() on the object.
292
293 Args:
294 data: dict, A deserialized JSON object.
295
296 Returns:
297 An instance of a Credentials subclass.
298 """
299 data = simplejson.loads(s)
300 if 'token_expiry' in data and not isinstance(data['token_expiry'],
301 datetime.datetime):
302 try:
303 data['token_expiry'] = datetime.datetime.strptime(
304 data['token_expiry'], EXPIRY_FORMAT)
305 except:
306 data['token_expiry'] = None
307 retval = OAuth2Credentials(
308 data['access_token'],
309 data['client_id'],
310 data['client_secret'],
311 data['refresh_token'],
312 data['token_expiry'],
313 data['token_uri'],
314 data['user_agent'],
315 data.get('id_token', None))
316 retval.invalid = data['invalid']
317 return retval
318
319 @property
320 def access_token_expired(self):
321 """True if the credential is expired or invalid.
322
323 If the token_expiry isn't set, we assume the token doesn't expire.
324 """
325 if self.invalid:
326 return True
327
328 if not self.token_expiry:
329 return False
330
331 now = datetime.datetime.utcnow()
332 if now >= self.token_expiry:
333 logger.info('access_token is expired. Now: %s, token_expiry: %s',
334 now, self.token_expiry)
335 return True
336 return False
337
338 def set_store(self, store):
339 """Set the Storage for the credential.
340
341 Args:
342 store: Storage, an implementation of Stroage object.
343 This is needed to store the latest access_token if it
344 has expired and been refreshed. This implementation uses
345 locking to check for updates before updating the
346 access_token.
347 """
348 self.store = store
349
350 def _updateFromCredential(self, other):
351 """Update this Credential from another instance."""
352 self.__dict__.update(other.__getstate__())
353
354 def __getstate__(self):
355 """Trim the state down to something that can be pickled."""
356 d = copy.copy(self.__dict__)
357 del d['store']
358 return d
359
360 def __setstate__(self, state):
361 """Reconstitute the state of the object from being pickled."""
362 self.__dict__.update(state)
363 self.store = None
364
365 def _generate_refresh_request_body(self):
366 """Generate the body that will be used in the refresh request."""
367 body = urllib.urlencode({
368 'grant_type': 'refresh_token',
369 'client_id': self.client_id,
370 'client_secret': self.client_secret,
371 'refresh_token': self.refresh_token,
372 })
373 return body
374
375 def _generate_refresh_request_headers(self):
376 """Generate the headers that will be used in the refresh request."""
377 headers = {
378 'content-type': 'application/x-www-form-urlencoded',
379 }
380
381 if self.user_agent is not None:
382 headers['user-agent'] = self.user_agent
383
384 return headers
385
386 def _refresh(self, http_request):
387 """Refreshes the access_token.
388
389 This method first checks by reading the Storage object if available.
390 If a refresh is still needed, it holds the Storage lock until the
391 refresh is completed.
392 """
393 if not self.store:
394 self._do_refresh_request(http_request)
395 else:
396 self.store.acquire_lock()
397 try:
398 new_cred = self.store.locked_get()
399 if (new_cred and not new_cred.invalid and
400 new_cred.access_token != self.access_token):
401 logger.info('Updated access_token read from Storage')
402 self._updateFromCredential(new_cred)
403 else:
404 self._do_refresh_request(http_request)
405 finally:
406 self.store.release_lock()
407
408 def _do_refresh_request(self, http_request):
409 """Refresh the access_token using the refresh_token.
410
411 Args:
412 http: An instance of httplib2.Http.request
413 or something that acts like it.
414
415 Raises:
416 AccessTokenRefreshError: When the refresh fails.
417 """
418 body = self._generate_refresh_request_body()
419 headers = self._generate_refresh_request_headers()
420
421 logger.info('Refresing access_token')
422 resp, content = http_request(
423 self.token_uri, method='POST', body=body, headers=headers)
424 if resp.status == 200:
425 # TODO(jcgregorio) Raise an error if loads fails?
426 d = simplejson.loads(content)
427 self.access_token = d['access_token']
428 self.refresh_token = d.get('refresh_token', self.refresh_token)
429 if 'expires_in' in d:
430 self.token_expiry = datetime.timedelta(
431 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
432 else:
433 self.token_expiry = None
434 if self.store:
435 self.store.locked_put(self)
436 else:
437 # An {'error':...} response body means the token is expired or revoked,
438 # so we flag the credentials as such.
439 logger.error('Failed to retrieve access token: %s' % content)
440 error_msg = 'Invalid response %s.' % resp['status']
441 try:
442 d = simplejson.loads(content)
443 if 'error' in d:
444 error_msg = d['error']
445 self.invalid = True
446 if self.store:
447 self.store.locked_put(self)
448 except:
449 pass
450 raise AccessTokenRefreshError(error_msg)
451
452 def authorize(self, http):
453 """Authorize an httplib2.Http instance with these credentials.
454
455 Args:
456 http: An instance of httplib2.Http
457 or something that acts like it.
458
459 Returns:
460 A modified instance of http that was passed in.
461
462 Example:
463
464 h = httplib2.Http()
465 h = credentials.authorize(h)
466
467 You can't create a new OAuth subclass of httplib2.Authenication
468 because it never gets passed the absolute URI, which is needed for
469 signing. So instead we have to overload 'request' with a closure
470 that adds in the Authorization header and then calls the original
471 version of 'request()'.
472 """
473 request_orig = http.request
474
475 # The closure that will replace 'httplib2.Http.request'.
476 def new_request(uri, method='GET', body=None, headers=None,
477 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
478 connection_type=None):
479 if not self.access_token:
480 logger.info('Attempting refresh to obtain initial access_token')
481 self._refresh(request_orig)
482
483 # Modify the request headers to add the appropriate
484 # Authorization header.
485 if headers is None:
486 headers = {}
487 headers['authorization'] = 'OAuth ' + self.access_token
488
489 if self.user_agent is not None:
490 if 'user-agent' in headers:
491 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
492 else:
493 headers['user-agent'] = self.user_agent
494
495 resp, content = request_orig(uri, method, body, headers,
496 redirections, connection_type)
497
498 if resp.status == 401:
499 logger.info('Refreshing due to a 401')
500 self._refresh(request_orig)
501 headers['authorization'] = 'OAuth ' + self.access_token
502 return request_orig(uri, method, body, headers,
503 redirections, connection_type)
504 else:
505 return (resp, content)
506
507 http.request = new_request
508 return http
509
510
511 class AccessTokenCredentials(OAuth2Credentials):
512 """Credentials object for OAuth 2.0.
513
514 Credentials can be applied to an httplib2.Http object using the
515 authorize() method, which then signs each request from that object
516 with the OAuth 2.0 access token. This set of credentials is for the
517 use case where you have acquired an OAuth 2.0 access_token from
518 another place such as a JavaScript client or another web
519 application, and wish to use it from Python. Because only the
520 access_token is present it can not be refreshed and will in time
521 expire.
522
523 AccessTokenCredentials objects may be safely pickled and unpickled.
524
525 Usage:
526 credentials = AccessTokenCredentials('<an access token>',
527 'my-user-agent/1.0')
528 http = httplib2.Http()
529 http = credentials.authorize(http)
530
531 Exceptions:
532 AccessTokenCredentialsExpired: raised when the access_token expires or is
533 revoked.
534 """
535
536 def __init__(self, access_token, user_agent):
537 """Create an instance of OAuth2Credentials
538
539 This is one of the few types if Credentials that you should contrust,
540 Credentials objects are usually instantiated by a Flow.
541
542 Args:
543 access_token: string, access token.
544 user_agent: string, The HTTP User-Agent to provide for this application.
545
546 Notes:
547 store: callable, a callable that when passed a Credential
548 will store the credential back to where it came from.
549 """
550 super(AccessTokenCredentials, self).__init__(
551 access_token,
552 None,
553 None,
554 None,
555 None,
556 None,
557 user_agent)
558
559
560 @classmethod
561 def from_json(cls, s):
562 data = simplejson.loads(s)
563 retval = AccessTokenCredentials(
564 data['access_token'],
565 data['user_agent'])
566 return retval
567
568 def _refresh(self, http_request):
569 raise AccessTokenCredentialsError(
570 "The access_token is expired or invalid and can't be refreshed.")
571
572
573 class AssertionCredentials(OAuth2Credentials):
574 """Abstract Credentials object used for OAuth 2.0 assertion grants.
575
576 This credential does not require a flow to instantiate because it
577 represents a two legged flow, and therefore has all of the required
578 information to generate and refresh its own access tokens. It must
579 be subclassed to generate the appropriate assertion string.
580
581 AssertionCredentials objects may be safely pickled and unpickled.
582 """
583
584 def __init__(self, assertion_type, user_agent,
585 token_uri='https://accounts.google.com/o/oauth2/token',
586 **unused_kwargs):
587 """Constructor for AssertionFlowCredentials.
588
589 Args:
590 assertion_type: string, assertion type that will be declared to the auth
591 server
592 user_agent: string, The HTTP User-Agent to provide for this application.
593 token_uri: string, URI for token endpoint. For convenience
594 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
595 """
596 super(AssertionCredentials, self).__init__(
597 None,
598 None,
599 None,
600 None,
601 None,
602 token_uri,
603 user_agent)
604 self.assertion_type = assertion_type
605
606 def _generate_refresh_request_body(self):
607 assertion = self._generate_assertion()
608
609 body = urllib.urlencode({
610 'assertion_type': self.assertion_type,
611 'assertion': assertion,
612 'grant_type': 'assertion',
613 })
614
615 return body
616
617 def _generate_assertion(self):
618 """Generate the assertion string that will be used in the access token
619 request.
620 """
621 _abstract()
622
623 if HAS_OPENSSL:
624 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
625 # don't create the SignedJwtAssertionCredentials or the verify_id_token()
626 # method.
627
628 class SignedJwtAssertionCredentials(AssertionCredentials):
629 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
630
631 This credential does not require a flow to instantiate because it
632 represents a two legged flow, and therefore has all of the required
633 information to generate and refresh its own access tokens.
634 """
635
636 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
637
638 def __init__(self,
639 service_account_name,
640 private_key,
641 scope,
642 private_key_password='notasecret',
643 user_agent=None,
644 token_uri='https://accounts.google.com/o/oauth2/token',
645 **kwargs):
646 """Constructor for SignedJwtAssertionCredentials.
647
648 Args:
649 service_account_name: string, id for account, usually an email address.
650 private_key: string, private key in P12 format.
651 scope: string or list of strings, scope(s) of the credentials being
652 requested.
653 private_key_password: string, password for private_key.
654 user_agent: string, HTTP User-Agent to provide for this application.
655 token_uri: string, URI for token endpoint. For convenience
656 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
657 kwargs: kwargs, Additional parameters to add to the JWT token, for
658 example prn=joe@xample.org."""
659
660 super(SignedJwtAssertionCredentials, self).__init__(
661 'http://oauth.net/grant_type/jwt/1.0/bearer',
662 user_agent,
663 token_uri=token_uri,
664 )
665
666 if type(scope) is list:
667 scope = ' '.join(scope)
668 self.scope = scope
669
670 self.private_key = private_key
671 self.private_key_password = private_key_password
672 self.service_account_name = service_account_name
673 self.kwargs = kwargs
674
675 @classmethod
676 def from_json(cls, s):
677 data = simplejson.loads(s)
678 retval = SignedJwtAssertionCredentials(
679 data['service_account_name'],
680 data['private_key'],
681 data['private_key_password'],
682 data['scope'],
683 data['user_agent'],
684 data['token_uri'],
685 data['kwargs']
686 )
687 retval.invalid = data['invalid']
688 return retval
689
690 def _generate_assertion(self):
691 """Generate the assertion that will be used in the request."""
692 now = long(time.time())
693 payload = {
694 'aud': self.token_uri,
695 'scope': self.scope,
696 'iat': now,
697 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
698 'iss': self.service_account_name
699 }
700 payload.update(self.kwargs)
701 logging.debug(str(payload))
702
703 return make_signed_jwt(
704 Signer.from_string(self.private_key, self.private_key_password),
705 payload)
706
707
708 def verify_id_token(id_token, audience, http=None,
709 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
710 """Verifies a signed JWT id_token.
711
712 Args:
713 id_token: string, A Signed JWT.
714 audience: string, The audience 'aud' that the token should be for.
715 http: httplib2.Http, instance to use to make the HTTP request. Callers
716 should supply an instance that has caching enabled.
717 cert_uri: string, URI of the certificates in JSON format to
718 verify the JWT against.
719
720 Returns:
721 The deserialized JSON in the JWT.
722
723 Raises:
724 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
725 """
726 if http is None:
727 http = httplib2.Http()
728
729 resp, content = http.request(cert_uri)
730
731 if resp.status == 200:
732 certs = simplejson.loads(content)
733 return verify_signed_jwt_with_certs(id_token, certs, audience)
734 else:
735 raise VerifyJwtTokenError('Status code: %d' % resp.status)
736
737
738 def _urlsafe_b64decode(b64string):
739 # Guard against unicode strings, which base64 can't handle.
740 b64string = b64string.encode('ascii')
741 padded = b64string + '=' * (4 - len(b64string) % 4)
742 return base64.urlsafe_b64decode(padded)
743
744
745 def _extract_id_token(id_token):
746 """Extract the JSON payload from a JWT.
747
748 Does the extraction w/o checking the signature.
749
750 Args:
751 id_token: string, OAuth 2.0 id_token.
752
753 Returns:
754 object, The deserialized JSON payload.
755 """
756 segments = id_token.split('.')
757
758 if (len(segments) != 3):
759 raise VerifyJwtTokenError(
760 'Wrong number of segments in token: %s' % id_token)
761
762 return simplejson.loads(_urlsafe_b64decode(segments[1]))
763
764
765 class OAuth2WebServerFlow(Flow):
766 """Does the Web Server Flow for OAuth 2.0.
767
768 OAuth2Credentials objects may be safely pickled and unpickled.
769 """
770
771 def __init__(self, client_id, client_secret, scope, user_agent=None,
772 auth_uri='https://accounts.google.com/o/oauth2/auth',
773 token_uri='https://accounts.google.com/o/oauth2/token',
774 **kwargs):
775 """Constructor for OAuth2WebServerFlow.
776
777 Args:
778 client_id: string, client identifier.
779 client_secret: string client secret.
780 scope: string or list of strings, scope(s) of the credentials being
781 requested.
782 user_agent: string, HTTP User-Agent to provide for this application.
783 auth_uri: string, URI for authorization endpoint. For convenience
784 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
785 token_uri: string, URI for token endpoint. For convenience
786 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
787 **kwargs: dict, The keyword arguments are all optional and required
788 parameters for the OAuth calls.
789 """
790 self.client_id = client_id
791 self.client_secret = client_secret
792 if type(scope) is list:
793 scope = ' '.join(scope)
794 self.scope = scope
795 self.user_agent = user_agent
796 self.auth_uri = auth_uri
797 self.token_uri = token_uri
798 self.params = {
799 'access_type': 'offline',
800 }
801 self.params.update(kwargs)
802 self.redirect_uri = None
803
804 def step1_get_authorize_url(self, redirect_uri='oob'):
805 """Returns a URI to redirect to the provider.
806
807 Args:
808 redirect_uri: string, Either the string 'oob' for a non-web-based
809 application, or a URI that handles the callback from
810 the authorization server.
811
812 If redirect_uri is 'oob' then pass in the
813 generated verification code to step2_exchange,
814 otherwise pass in the query parameters received
815 at the callback uri to step2_exchange.
816 """
817
818 self.redirect_uri = redirect_uri
819 query = {
820 'response_type': 'code',
821 'client_id': self.client_id,
822 'redirect_uri': redirect_uri,
823 'scope': self.scope,
824 }
825 query.update(self.params)
826 parts = list(urlparse.urlparse(self.auth_uri))
827 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
828 parts[4] = urllib.urlencode(query)
829 return urlparse.urlunparse(parts)
830
831 def step2_exchange(self, code, http=None):
832 """Exhanges a code for OAuth2Credentials.
833
834 Args:
835 code: string or dict, either the code as a string, or a dictionary
836 of the query parameters to the redirect_uri, which contains
837 the code.
838 http: httplib2.Http, optional http instance to use to do the fetch
839 """
840
841 if not (isinstance(code, str) or isinstance(code, unicode)):
842 code = code['code']
843
844 body = urllib.urlencode({
845 'grant_type': 'authorization_code',
846 'client_id': self.client_id,
847 'client_secret': self.client_secret,
848 'code': code,
849 'redirect_uri': self.redirect_uri,
850 'scope': self.scope,
851 })
852 headers = {
853 'content-type': 'application/x-www-form-urlencoded',
854 }
855
856 if self.user_agent is not None:
857 headers['user-agent'] = self.user_agent
858
859 if http is None:
860 http = httplib2.Http()
861
862 resp, content = http.request(self.token_uri, method='POST', body=body,
863 headers=headers)
864 if resp.status == 200:
865 # TODO(jcgregorio) Raise an error if simplejson.loads fails?
866 d = simplejson.loads(content)
867 access_token = d['access_token']
868 refresh_token = d.get('refresh_token', None)
869 token_expiry = None
870 if 'expires_in' in d:
871 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
872 seconds=int(d['expires_in']))
873
874 if 'id_token' in d:
875 d['id_token'] = _extract_id_token(d['id_token'])
876
877 logger.info('Successfully retrieved access token: %s' % content)
878 return OAuth2Credentials(access_token, self.client_id,
879 self.client_secret, refresh_token, token_expiry,
880 self.token_uri, self.user_agent,
881 id_token=d.get('id_token', None))
882 else:
883 logger.error('Failed to retrieve access token: %s' % content)
884 error_msg = 'Invalid response %s.' % resp['status']
885 try:
886 d = simplejson.loads(content)
887 if 'error' in d:
888 error_msg = d['error']
889 except:
890 pass
891
892 raise FlowExchangeError(error_msg)
893
894 def flow_from_clientsecrets(filename, scope, message=None):
895 """Create a Flow from a clientsecrets file.
896
897 Will create the right kind of Flow based on the contents of the clientsecrets
898 file or will raise InvalidClientSecretsError for unknown types of Flows.
899
900 Args:
901 filename: string, File name of client secrets.
902 scope: string or list of strings, scope(s) to request.
903 message: string, A friendly string to display to the user if the
904 clientsecrets file is missing or invalid. If message is provided then
905 sys.exit will be called in the case of an error. If message in not
906 provided then clientsecrets.InvalidClientSecretsError will be raised.
907
908 Returns:
909 A Flow object.
910
911 Raises:
912 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
913 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
914 invalid.
915 """
916 try:
917 client_type, client_info = clientsecrets.loadfile(filename)
918 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
919 return OAuth2WebServerFlow(
920 client_info['client_id'],
921 client_info['client_secret'],
922 scope,
923 None, # user_agent
924 client_info['auth_uri'],
925 client_info['token_uri'])
926 except clientsecrets.InvalidClientSecretsError:
927 if message:
928 sys.exit(message)
929 else:
930 raise
931 else:
932 raise UnknownClientSecretsFlowError(
933 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698