1
2
3
4
5
6
7
8
9
10
11
12
13
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 collections
24 import copy
25 import datetime
26 import json
27 import logging
28 import os
29 import sys
30 import time
31 import urllib
32 import urlparse
33
34 import httplib2
35 from oauth2client import clientsecrets
36 from oauth2client import GOOGLE_AUTH_URI
37 from oauth2client import GOOGLE_DEVICE_URI
38 from oauth2client import GOOGLE_REVOKE_URI
39 from oauth2client import GOOGLE_TOKEN_URI
40 from oauth2client import util
41
42 HAS_OPENSSL = False
43 HAS_CRYPTO = False
44 try:
45 from oauth2client import crypt
46 HAS_CRYPTO = True
47 if crypt.OpenSSLVerifier is not None:
48 HAS_OPENSSL = True
49 except ImportError:
50 pass
51
52 logger = logging.getLogger(__name__)
53
54
55 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
56
57
58 ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
59
60
61 ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS
62
63
64 OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
65
66
67 REFRESH_STATUS_CODES = [401]
68
69
70 AUTHORIZED_USER = 'authorized_user'
71
72
73 SERVICE_ACCOUNT = 'service_account'
74
75
76
77 GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS'
78
79
80
81 ADC_HELP_MSG = (
82 'The Application Default Credentials are not available. They are available '
83 'if running in Google Compute Engine. Otherwise, the environment variable '
84 + GOOGLE_APPLICATION_CREDENTIALS +
85 ' must be defined pointing to a file defining the credentials. See '
86 'https://developers.google.com/accounts/docs/application-default-credentials'
87 ' for more information.')
88
89
90 AccessTokenInfo = collections.namedtuple(
91 'AccessTokenInfo', ['access_token', 'expires_in'])
92
93
94 -class Error(Exception):
95 """Base error for this module."""
96
99 """Error trying to exchange an authorization grant for an access token."""
100
103 """Error trying to refresh an expired access token."""
104
107 """Error trying to revoke a token."""
108
111 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
112
115 """Having only the access_token means no refresh is possible."""
116
119 """Could not retrieve certificates for validation."""
120
123 """Header names and values must be ASCII strings."""
124
127 """Error retrieving the Application Default Credentials."""
128
131 """Error trying to retrieve a device code."""
132
135 """Raised when a crypto library is required, but none is available."""
136
139 raise NotImplementedError('You need to override this function')
140
143 """httplib2 Cache implementation which only caches locally."""
144
147
148 - def get(self, key):
149 return self.cache.get(key)
150
151 - def set(self, key, value):
152 self.cache[key] = value
153
155 self.cache.pop(key, None)
156
159 """Base class for all Credentials objects.
160
161 Subclasses must define an authorize() method that applies the credentials to
162 an HTTP transport.
163
164 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
165 string as input and returns an instantiated Credentials object.
166 """
167
168 NON_SERIALIZED_MEMBERS = ['store']
169
170
172 """Take an httplib2.Http instance (or equivalent) and authorizes it.
173
174 Authorizes it for the set of credentials, usually by replacing
175 http.request() with a method that adds in the appropriate headers and then
176 delegates to the original Http.request() method.
177
178 Args:
179 http: httplib2.Http, an http object to be used to make the refresh
180 request.
181 """
182 _abstract()
183
184
186 """Forces a refresh of the access_token.
187
188 Args:
189 http: httplib2.Http, an http object to be used to make the refresh
190 request.
191 """
192 _abstract()
193
194
196 """Revokes a refresh_token and makes the credentials void.
197
198 Args:
199 http: httplib2.Http, an http object to be used to make the revoke
200 request.
201 """
202 _abstract()
203
204
205 - def apply(self, headers):
206 """Add the authorization to the headers.
207
208 Args:
209 headers: dict, the headers to add the Authorization header to.
210 """
211 _abstract()
212
214 """Utility function that creates JSON repr. of a Credentials object.
215
216 Args:
217 strip: array, An array of names of members to not include in the JSON.
218
219 Returns:
220 string, a JSON representation of this instance, suitable to pass to
221 from_json().
222 """
223 t = type(self)
224 d = copy.copy(self.__dict__)
225 for member in strip:
226 if member in d:
227 del d[member]
228 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
229 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
230
231 d['_class'] = t.__name__
232 d['_module'] = t.__module__
233 return json.dumps(d)
234
236 """Creating a JSON representation of an instance of Credentials.
237
238 Returns:
239 string, a JSON representation of this instance, suitable to pass to
240 from_json().
241 """
242 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
243
244 @classmethod
246 """Utility class method to instantiate a Credentials subclass from a JSON
247 representation produced by to_json().
248
249 Args:
250 s: string, JSON from to_json().
251
252 Returns:
253 An instance of the subclass of Credentials that was serialized with
254 to_json().
255 """
256 data = json.loads(s)
257
258 module = data['_module']
259 try:
260 m = __import__(module)
261 except ImportError:
262
263 module = module.replace('.googleapiclient', '')
264 m = __import__(module)
265
266 m = __import__(module, fromlist=module.split('.')[:-1])
267 kls = getattr(m, data['_class'])
268 from_json = getattr(kls, 'from_json')
269 return from_json(s)
270
271 @classmethod
273 """Instantiate a Credentials object from a JSON description of it.
274
275 The JSON should have been produced by calling .to_json() on the object.
276
277 Args:
278 unused_data: dict, A deserialized JSON object.
279
280 Returns:
281 An instance of a Credentials subclass.
282 """
283 return Credentials()
284
285
286 -class Flow(object):
287 """Base class for all Flow objects."""
288 pass
289
292 """Base class for all Storage objects.
293
294 Store and retrieve a single credential. This class supports locking
295 such that multiple processes and threads can operate on a single
296 store.
297 """
298
300 """Acquires any lock necessary to access this Storage.
301
302 This lock is not reentrant.
303 """
304 pass
305
307 """Release the Storage lock.
308
309 Trying to release a lock that isn't held will result in a
310 RuntimeError.
311 """
312 pass
313
315 """Retrieve credential.
316
317 The Storage lock must be held when this is called.
318
319 Returns:
320 oauth2client.client.Credentials
321 """
322 _abstract()
323
325 """Write a credential.
326
327 The Storage lock must be held when this is called.
328
329 Args:
330 credentials: Credentials, the credentials to store.
331 """
332 _abstract()
333
335 """Delete a credential.
336
337 The Storage lock must be held when this is called.
338 """
339 _abstract()
340
342 """Retrieve credential.
343
344 The Storage lock must *not* be held when this is called.
345
346 Returns:
347 oauth2client.client.Credentials
348 """
349 self.acquire_lock()
350 try:
351 return self.locked_get()
352 finally:
353 self.release_lock()
354
355 - def put(self, credentials):
356 """Write a credential.
357
358 The Storage lock must be held when this is called.
359
360 Args:
361 credentials: Credentials, the credentials to store.
362 """
363 self.acquire_lock()
364 try:
365 self.locked_put(credentials)
366 finally:
367 self.release_lock()
368
370 """Delete credential.
371
372 Frees any resources associated with storing the credential.
373 The Storage lock must *not* be held when this is called.
374
375 Returns:
376 None
377 """
378 self.acquire_lock()
379 try:
380 return self.locked_delete()
381 finally:
382 self.release_lock()
383
386 """Forces header keys and values to be strings, i.e not unicode.
387
388 The httplib module just concats the header keys and values in a way that may
389 make the message header a unicode string, which, if it then tries to
390 contatenate to a binary request body may result in a unicode decode error.
391
392 Args:
393 headers: dict, A dictionary of headers.
394
395 Returns:
396 The same dictionary but with all the keys converted to strings.
397 """
398 clean = {}
399 try:
400 for k, v in headers.iteritems():
401 clean[str(k)] = str(v)
402 except UnicodeEncodeError:
403 raise NonAsciiHeaderError(k + ': ' + v)
404 return clean
405
408 """Updates a URI with new query parameters.
409
410 Args:
411 uri: string, A valid URI, with potential existing query parameters.
412 params: dict, A dictionary of query parameters.
413
414 Returns:
415 The same URI but with the new query parameters added.
416 """
417 parts = urlparse.urlparse(uri)
418 query_params = dict(urlparse.parse_qsl(parts.query))
419 query_params.update(params)
420 new_parts = parts._replace(query=urllib.urlencode(query_params))
421 return urlparse.urlunparse(new_parts)
422
425 """Credentials object for OAuth 2.0.
426
427 Credentials can be applied to an httplib2.Http object using the authorize()
428 method, which then adds the OAuth 2.0 access token to each request.
429
430 OAuth2Credentials objects may be safely pickled and unpickled.
431 """
432
433 @util.positional(8)
434 - def __init__(self, access_token, client_id, client_secret, refresh_token,
435 token_expiry, token_uri, user_agent, revoke_uri=None,
436 id_token=None, token_response=None):
437 """Create an instance of OAuth2Credentials.
438
439 This constructor is not usually called by the user, instead
440 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
441
442 Args:
443 access_token: string, access token.
444 client_id: string, client identifier.
445 client_secret: string, client secret.
446 refresh_token: string, refresh token.
447 token_expiry: datetime, when the access_token expires.
448 token_uri: string, URI of token endpoint.
449 user_agent: string, The HTTP User-Agent to provide for this application.
450 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token
451 can't be revoked if this is None.
452 id_token: object, The identity of the resource owner.
453 token_response: dict, the decoded response to the token request. None
454 if a token hasn't been requested yet. Stored because some providers
455 (e.g. wordpress.com) include extra fields that clients may want.
456
457 Notes:
458 store: callable, A callable that when passed a Credential
459 will store the credential back to where it came from.
460 This is needed to store the latest access_token if it
461 has expired and been refreshed.
462 """
463 self.access_token = access_token
464 self.client_id = client_id
465 self.client_secret = client_secret
466 self.refresh_token = refresh_token
467 self.store = None
468 self.token_expiry = token_expiry
469 self.token_uri = token_uri
470 self.user_agent = user_agent
471 self.revoke_uri = revoke_uri
472 self.id_token = id_token
473 self.token_response = token_response
474
475
476
477 self.invalid = False
478
480 """Authorize an httplib2.Http instance with these credentials.
481
482 The modified http.request method will add authentication headers to each
483 request and will refresh access_tokens when a 401 is received on a
484 request. In addition the http.request method has a credentials property,
485 http.request.credentials, which is the Credentials object that authorized
486 it.
487
488 Args:
489 http: An instance of httplib2.Http
490 or something that acts like it.
491
492 Returns:
493 A modified instance of http that was passed in.
494
495 Example:
496
497 h = httplib2.Http()
498 h = credentials.authorize(h)
499
500 You can't create a new OAuth subclass of httplib2.Authentication
501 because it never gets passed the absolute URI, which is needed for
502 signing. So instead we have to overload 'request' with a closure
503 that adds in the Authorization header and then calls the original
504 version of 'request()'.
505 """
506 request_orig = http.request
507
508
509 @util.positional(1)
510 def new_request(uri, method='GET', body=None, headers=None,
511 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
512 connection_type=None):
513 if not self.access_token:
514 logger.info('Attempting refresh to obtain initial access_token')
515 self._refresh(request_orig)
516
517
518
519 if headers is None:
520 headers = {}
521 else:
522 headers = dict(headers)
523 self.apply(headers)
524
525 if self.user_agent is not None:
526 if 'user-agent' in headers:
527 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
528 else:
529 headers['user-agent'] = self.user_agent
530
531 resp, content = request_orig(uri, method, body, clean_headers(headers),
532 redirections, connection_type)
533
534 if resp.status in REFRESH_STATUS_CODES:
535 logger.info('Refreshing due to a %s', resp.status)
536 self._refresh(request_orig)
537 self.apply(headers)
538 return request_orig(uri, method, body, clean_headers(headers),
539 redirections, connection_type)
540 else:
541 return (resp, content)
542
543
544 http.request = new_request
545
546
547 setattr(http.request, 'credentials', self)
548
549 return http
550
552 """Forces a refresh of the access_token.
553
554 Args:
555 http: httplib2.Http, an http object to be used to make the refresh
556 request.
557 """
558 self._refresh(http.request)
559
561 """Revokes a refresh_token and makes the credentials void.
562
563 Args:
564 http: httplib2.Http, an http object to be used to make the revoke
565 request.
566 """
567 self._revoke(http.request)
568
569 - def apply(self, headers):
570 """Add the authorization to the headers.
571
572 Args:
573 headers: dict, the headers to add the Authorization header to.
574 """
575 headers['Authorization'] = 'Bearer ' + self.access_token
576
579
580 @classmethod
582 """Instantiate a Credentials object from a JSON description of it. The JSON
583 should have been produced by calling .to_json() on the object.
584
585 Args:
586 data: dict, A deserialized JSON object.
587
588 Returns:
589 An instance of a Credentials subclass.
590 """
591 data = json.loads(s)
592 if ('token_expiry' in data and
593 not isinstance(data['token_expiry'], datetime.datetime)):
594 try:
595 data['token_expiry'] = datetime.datetime.strptime(
596 data['token_expiry'], EXPIRY_FORMAT)
597 except ValueError:
598 data['token_expiry'] = None
599 retval = cls(
600 data['access_token'],
601 data['client_id'],
602 data['client_secret'],
603 data['refresh_token'],
604 data['token_expiry'],
605 data['token_uri'],
606 data['user_agent'],
607 revoke_uri=data.get('revoke_uri', None),
608 id_token=data.get('id_token', None),
609 token_response=data.get('token_response', None))
610 retval.invalid = data['invalid']
611 return retval
612
613 @property
615 """True if the credential is expired or invalid.
616
617 If the token_expiry isn't set, we assume the token doesn't expire.
618 """
619 if self.invalid:
620 return True
621
622 if not self.token_expiry:
623 return False
624
625 now = datetime.datetime.utcnow()
626 if now >= self.token_expiry:
627 logger.info('access_token is expired. Now: %s, token_expiry: %s',
628 now, self.token_expiry)
629 return True
630 return False
631
633 """Return the access token and its expiration information.
634
635 If the token does not exist, get one.
636 If the token expired, refresh it.
637 """
638 if not self.access_token or self.access_token_expired:
639 if not http:
640 http = httplib2.Http()
641 self.refresh(http)
642 return AccessTokenInfo(access_token=self.access_token,
643 expires_in=self._expires_in())
644
646 """Set the Storage for the credential.
647
648 Args:
649 store: Storage, an implementation of Storage object.
650 This is needed to store the latest access_token if it
651 has expired and been refreshed. This implementation uses
652 locking to check for updates before updating the
653 access_token.
654 """
655 self.store = store
656
658 """Return the number of seconds until this token expires.
659
660 If token_expiry is in the past, this method will return 0, meaning the
661 token has already expired.
662 If token_expiry is None, this method will return None. Note that returning
663 0 in such a case would not be fair: the token may still be valid;
664 we just don't know anything about it.
665 """
666 if self.token_expiry:
667 now = datetime.datetime.utcnow()
668 if self.token_expiry > now:
669 time_delta = self.token_expiry - now
670
671
672 return time_delta.days * 86400 + time_delta.seconds
673 else:
674 return 0
675
677 """Update this Credential from another instance."""
678 self.__dict__.update(other.__getstate__())
679
681 """Trim the state down to something that can be pickled."""
682 d = copy.copy(self.__dict__)
683 del d['store']
684 return d
685
687 """Reconstitute the state of the object from being pickled."""
688 self.__dict__.update(state)
689 self.store = None
690
692 """Generate the body that will be used in the refresh request."""
693 body = urllib.urlencode({
694 'grant_type': 'refresh_token',
695 'client_id': self.client_id,
696 'client_secret': self.client_secret,
697 'refresh_token': self.refresh_token,
698 })
699 return body
700
702 """Generate the headers that will be used in the refresh request."""
703 headers = {
704 'content-type': 'application/x-www-form-urlencoded',
705 }
706
707 if self.user_agent is not None:
708 headers['user-agent'] = self.user_agent
709
710 return headers
711
713 """Refreshes the access_token.
714
715 This method first checks by reading the Storage object if available.
716 If a refresh is still needed, it holds the Storage lock until the
717 refresh is completed.
718
719 Args:
720 http_request: callable, a callable that matches the method signature of
721 httplib2.Http.request, used to make the refresh request.
722
723 Raises:
724 AccessTokenRefreshError: When the refresh fails.
725 """
726 if not self.store:
727 self._do_refresh_request(http_request)
728 else:
729 self.store.acquire_lock()
730 try:
731 new_cred = self.store.locked_get()
732 if (new_cred and not new_cred.invalid and
733 new_cred.access_token != self.access_token):
734 logger.info('Updated access_token read from Storage')
735 self._updateFromCredential(new_cred)
736 else:
737 self._do_refresh_request(http_request)
738 finally:
739 self.store.release_lock()
740
742 """Refresh the access_token using the refresh_token.
743
744 Args:
745 http_request: callable, a callable that matches the method signature of
746 httplib2.Http.request, used to make the refresh request.
747
748 Raises:
749 AccessTokenRefreshError: When the refresh fails.
750 """
751 body = self._generate_refresh_request_body()
752 headers = self._generate_refresh_request_headers()
753
754 logger.info('Refreshing access_token')
755 resp, content = http_request(
756 self.token_uri, method='POST', body=body, headers=headers)
757 if resp.status == 200:
758
759 d = json.loads(content)
760 self.token_response = d
761 self.access_token = d['access_token']
762 self.refresh_token = d.get('refresh_token', self.refresh_token)
763 if 'expires_in' in d:
764 self.token_expiry = datetime.timedelta(
765 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
766 else:
767 self.token_expiry = None
768
769
770 self.invalid = False
771 if self.store:
772 self.store.locked_put(self)
773 else:
774
775
776 logger.info('Failed to retrieve access token: %s', content)
777 error_msg = 'Invalid response %s.' % resp['status']
778 try:
779 d = json.loads(content)
780 if 'error' in d:
781 error_msg = d['error']
782 if 'error_description' in d:
783 error_msg += ': ' + d['error_description']
784 self.invalid = True
785 if self.store:
786 self.store.locked_put(self)
787 except StandardError:
788 pass
789 raise AccessTokenRefreshError(error_msg)
790
792 """Revokes the refresh_token and deletes the store if available.
793
794 Args:
795 http_request: callable, a callable that matches the method signature of
796 httplib2.Http.request, used to make the revoke request.
797 """
798 self._do_revoke(http_request, self.refresh_token)
799
801 """Revokes the credentials and deletes the store if available.
802
803 Args:
804 http_request: callable, a callable that matches the method signature of
805 httplib2.Http.request, used to make the refresh request.
806 token: A string used as the token to be revoked. Can be either an
807 access_token or refresh_token.
808
809 Raises:
810 TokenRevokeError: If the revoke request does not return with a 200 OK.
811 """
812 logger.info('Revoking token')
813 query_params = {'token': token}
814 token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
815 resp, content = http_request(token_revoke_uri)
816 if resp.status == 200:
817 self.invalid = True
818 else:
819 error_msg = 'Invalid response %s.' % resp.status
820 try:
821 d = json.loads(content)
822 if 'error' in d:
823 error_msg = d['error']
824 except StandardError:
825 pass
826 raise TokenRevokeError(error_msg)
827
828 if self.store:
829 self.store.delete()
830
833 """Credentials object for OAuth 2.0.
834
835 Credentials can be applied to an httplib2.Http object using the
836 authorize() method, which then signs each request from that object
837 with the OAuth 2.0 access token. This set of credentials is for the
838 use case where you have acquired an OAuth 2.0 access_token from
839 another place such as a JavaScript client or another web
840 application, and wish to use it from Python. Because only the
841 access_token is present it can not be refreshed and will in time
842 expire.
843
844 AccessTokenCredentials objects may be safely pickled and unpickled.
845
846 Usage:
847 credentials = AccessTokenCredentials('<an access token>',
848 'my-user-agent/1.0')
849 http = httplib2.Http()
850 http = credentials.authorize(http)
851
852 Exceptions:
853 AccessTokenCredentialsExpired: raised when the access_token expires or is
854 revoked.
855 """
856
857 - def __init__(self, access_token, user_agent, revoke_uri=None):
858 """Create an instance of OAuth2Credentials
859
860 This is one of the few types if Credentials that you should contrust,
861 Credentials objects are usually instantiated by a Flow.
862
863 Args:
864 access_token: string, access token.
865 user_agent: string, The HTTP User-Agent to provide for this application.
866 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token
867 can't be revoked if this is None.
868 """
869 super(AccessTokenCredentials, self).__init__(
870 access_token,
871 None,
872 None,
873 None,
874 None,
875 None,
876 user_agent,
877 revoke_uri=revoke_uri)
878
879
880 @classmethod
887
891
893 """Revokes the access_token and deletes the store if available.
894
895 Args:
896 http_request: callable, a callable that matches the method signature of
897 httplib2.Http.request, used to make the revoke request.
898 """
899 self._do_revoke(http_request, self.access_token)
900
901
902 _env_name = None
906 """Detect the environment the code is being run on."""
907
908 global _env_name
909
910 if _env_name:
911 return _env_name
912
913 server_software = os.environ.get('SERVER_SOFTWARE', '')
914 if server_software.startswith('Google App Engine/'):
915 _env_name = 'GAE_PRODUCTION'
916 elif server_software.startswith('Development/'):
917 _env_name = 'GAE_LOCAL'
918 else:
919 import urllib2
920 try:
921 if urllib2_urlopen is None:
922 urllib2_urlopen = urllib2.urlopen
923 response = urllib2_urlopen('http://metadata.google.internal')
924 if any('Metadata-Flavor: Google' in h for h in response.info().headers):
925 _env_name = 'GCE_PRODUCTION'
926 else:
927 _env_name = 'UNKNOWN'
928 except urllib2.URLError:
929 _env_name = 'UNKNOWN'
930
931 return _env_name
932
935 """Application Default Credentials for use in calling Google APIs.
936
937 The Application Default Credentials are being constructed as a function of
938 the environment where the code is being run.
939 More details can be found on this page:
940 https://developers.google.com/accounts/docs/application-default-credentials
941
942 Here is an example of how to use the Application Default Credentials for a
943 service that requires authentication:
944
945 <code>
946 from googleapiclient.discovery import build
947 from oauth2client.client import GoogleCredentials
948
949 PROJECT = 'bamboo-machine-422' # replace this with one of your projects
950 ZONE = 'us-central1-a' # replace this with the zone you care about
951
952 credentials = GoogleCredentials.get_application_default()
953 service = build('compute', 'v1', credentials=credentials)
954
955 request = service.instances().list(project=PROJECT, zone=ZONE)
956 response = request.execute()
957
958 print response
959 </code>
960
961 A service that does not require authentication does not need credentials
962 to be passed in:
963
964 <code>
965 from googleapiclient.discovery import build
966
967 service = build('discovery', 'v1')
968
969 request = service.apis().list()
970 response = request.execute()
971
972 print response
973 </code>
974 """
975
976 - def __init__(self, access_token, client_id, client_secret, refresh_token,
977 token_expiry, token_uri, user_agent,
978 revoke_uri=GOOGLE_REVOKE_URI):
979 """Create an instance of GoogleCredentials.
980
981 This constructor is not usually called by the user, instead
982 GoogleCredentials objects are instantiated by
983 GoogleCredentials.from_stream() or
984 GoogleCredentials.get_application_default().
985
986 Args:
987 access_token: string, access token.
988 client_id: string, client identifier.
989 client_secret: string, client secret.
990 refresh_token: string, refresh token.
991 token_expiry: datetime, when the access_token expires.
992 token_uri: string, URI of token endpoint.
993 user_agent: string, The HTTP User-Agent to provide for this application.
994 revoke_uri: string, URI for revoke endpoint.
995 Defaults to GOOGLE_REVOKE_URI; a token can't be revoked if this is None.
996 """
997 super(GoogleCredentials, self).__init__(
998 access_token, client_id, client_secret, refresh_token, token_expiry,
999 token_uri, user_agent, revoke_uri=revoke_uri)
1000
1002 """Whether this Credentials object is scopeless.
1003
1004 create_scoped(scopes) method needs to be called in order to create
1005 a Credentials object for API calls.
1006 """
1007 return False
1008
1010 """Create a Credentials object for the given scopes.
1011
1012 The Credentials type is preserved.
1013 """
1014 return self
1015
1016 @property
1018 """Get the fields and their values identifying the current credentials."""
1019 return {
1020 'type': 'authorized_user',
1021 'client_id': self.client_id,
1022 'client_secret': self.client_secret,
1023 'refresh_token': self.refresh_token
1024 }
1025
1026 @staticmethod
1070
1071 @staticmethod
1073 """Create a Credentials object by reading the information from a given file.
1074
1075 It returns an object of type GoogleCredentials.
1076
1077 Args:
1078 credential_filename: the path to the file from where the credentials
1079 are to be read
1080
1081 Exceptions:
1082 ApplicationDefaultCredentialsError: raised when the credentials fail
1083 to be retrieved.
1084 """
1085
1086 if credential_filename and os.path.isfile(credential_filename):
1087 try:
1088 return _get_application_default_credential_from_file(
1089 credential_filename)
1090 except (ApplicationDefaultCredentialsError, ValueError) as error:
1091 extra_help = ' (provided as parameter to the from_stream() method)'
1092 _raise_exception_for_reading_json(credential_filename,
1093 extra_help,
1094 error)
1095 else:
1096 raise ApplicationDefaultCredentialsError(
1097 'The parameter passed to the from_stream() '
1098 'method should point to a file.')
1099
1102 """Save the provided GoogleCredentials to the well known file.
1103
1104 Args:
1105 credentials:
1106 the credentials to be saved to the well known file;
1107 it should be an instance of GoogleCredentials
1108 well_known_file:
1109 the name of the file where the credentials are to be saved;
1110 this parameter is supposed to be used for testing only
1111 """
1112
1113
1114
1115 if well_known_file is None:
1116 well_known_file = _get_well_known_file()
1117
1118 credentials_data = credentials.serialization_data
1119
1120 with open(well_known_file, 'w') as f:
1121 json.dump(credentials_data, f, sort_keys=True, indent=2)
1122
1125 application_default_credential_filename = (
1126 os.environ.get(GOOGLE_APPLICATION_CREDENTIALS,
1127 None))
1128
1129 if application_default_credential_filename:
1130 if os.path.isfile(application_default_credential_filename):
1131 return application_default_credential_filename
1132 else:
1133 raise ApplicationDefaultCredentialsError(
1134 'File ' + application_default_credential_filename + ' (pointed by ' +
1135 GOOGLE_APPLICATION_CREDENTIALS +
1136 ' environment variable) does not exist!')
1137
1140 """Get the well known file produced by command 'gcloud auth login'."""
1141
1142
1143
1144 WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
1145 CLOUDSDK_CONFIG_DIRECTORY = 'gcloud'
1146
1147 if os.name == 'nt':
1148 try:
1149 default_config_path = os.path.join(os.environ['APPDATA'],
1150 CLOUDSDK_CONFIG_DIRECTORY)
1151 except KeyError:
1152
1153 drive = os.environ.get('SystemDrive', 'C:')
1154 default_config_path = os.path.join(drive, '\\', CLOUDSDK_CONFIG_DIRECTORY)
1155 else:
1156 default_config_path = os.path.join(os.path.expanduser('~'),
1157 '.config',
1158 CLOUDSDK_CONFIG_DIRECTORY)
1159
1160 default_config_path = os.path.join(default_config_path,
1161 WELL_KNOWN_CREDENTIALS_FILE)
1162
1163 return default_config_path
1164
1168 """Build the Application Default Credentials from file."""
1169
1170 import service_account
1171
1172
1173 with open(application_default_credential_filename) as (
1174 application_default_credential):
1175 client_credentials = json.load(application_default_credential)
1176
1177 credentials_type = client_credentials.get('type')
1178 if credentials_type == AUTHORIZED_USER:
1179 required_fields = set(['client_id', 'client_secret', 'refresh_token'])
1180 elif credentials_type == SERVICE_ACCOUNT:
1181 required_fields = set(['client_id', 'client_email', 'private_key_id',
1182 'private_key'])
1183 else:
1184 raise ApplicationDefaultCredentialsError(
1185 "'type' field should be defined (and have one of the '" +
1186 AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)")
1187
1188 missing_fields = required_fields.difference(client_credentials.keys())
1189
1190 if missing_fields:
1191 _raise_exception_for_missing_fields(missing_fields)
1192
1193 if client_credentials['type'] == AUTHORIZED_USER:
1194 return GoogleCredentials(
1195 access_token=None,
1196 client_id=client_credentials['client_id'],
1197 client_secret=client_credentials['client_secret'],
1198 refresh_token=client_credentials['refresh_token'],
1199 token_expiry=None,
1200 token_uri=GOOGLE_TOKEN_URI,
1201 user_agent='Python client library')
1202 else:
1203 return service_account._ServiceAccountCredentials(
1204 service_account_id=client_credentials['client_id'],
1205 service_account_email=client_credentials['client_email'],
1206 private_key_id=client_credentials['private_key_id'],
1207 private_key_pkcs8_text=client_credentials['private_key'],
1208 scopes=[])
1209
1214
1222
1228
1234
1237 """Abstract Credentials object used for OAuth 2.0 assertion grants.
1238
1239 This credential does not require a flow to instantiate because it
1240 represents a two legged flow, and therefore has all of the required
1241 information to generate and refresh its own access tokens. It must
1242 be subclassed to generate the appropriate assertion string.
1243
1244 AssertionCredentials objects may be safely pickled and unpickled.
1245 """
1246
1247 @util.positional(2)
1252 """Constructor for AssertionFlowCredentials.
1253
1254 Args:
1255 assertion_type: string, assertion type that will be declared to the auth
1256 server
1257 user_agent: string, The HTTP User-Agent to provide for this application.
1258 token_uri: string, URI for token endpoint. For convenience
1259 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1260 revoke_uri: string, URI for revoke endpoint.
1261 """
1262 super(AssertionCredentials, self).__init__(
1263 None,
1264 None,
1265 None,
1266 None,
1267 None,
1268 token_uri,
1269 user_agent,
1270 revoke_uri=revoke_uri)
1271 self.assertion_type = assertion_type
1272
1274 assertion = self._generate_assertion()
1275
1276 body = urllib.urlencode({
1277 'assertion': assertion,
1278 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
1279 })
1280
1281 return body
1282
1284 """Generate the assertion string that will be used in the access token
1285 request.
1286 """
1287 _abstract()
1288
1290 """Revokes the access_token and deletes the store if available.
1291
1292 Args:
1293 http_request: callable, a callable that matches the method signature of
1294 httplib2.Http.request, used to make the revoke request.
1295 """
1296 self._do_revoke(http_request, self.access_token)
1297
1300 """Ensure we have a crypto library, or throw CryptoUnavailableError.
1301
1302 The oauth2client.crypt module requires either PyCrypto or PyOpenSSL
1303 to be available in order to function, but these are optional
1304 dependencies.
1305 """
1306 if not HAS_CRYPTO:
1307 raise CryptoUnavailableError('No crypto library available')
1308
1311 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
1312
1313 This credential does not require a flow to instantiate because it
1314 represents a two legged flow, and therefore has all of the required
1315 information to generate and refresh its own access tokens.
1316
1317 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto
1318 2.6 or later. For App Engine you may also consider using
1319 AppAssertionCredentials.
1320 """
1321
1322 MAX_TOKEN_LIFETIME_SECS = 3600
1323
1324 @util.positional(4)
1325 - def __init__(self,
1326 service_account_name,
1327 private_key,
1328 scope,
1329 private_key_password='notasecret',
1330 user_agent=None,
1331 token_uri=GOOGLE_TOKEN_URI,
1332 revoke_uri=GOOGLE_REVOKE_URI,
1333 **kwargs):
1334 """Constructor for SignedJwtAssertionCredentials.
1335
1336 Args:
1337 service_account_name: string, id for account, usually an email address.
1338 private_key: string, private key in PKCS12 or PEM format.
1339 scope: string or iterable of strings, scope(s) of the credentials being
1340 requested.
1341 private_key_password: string, password for private_key, unused if
1342 private_key is in PEM format.
1343 user_agent: string, HTTP User-Agent to provide for this application.
1344 token_uri: string, URI for token endpoint. For convenience
1345 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1346 revoke_uri: string, URI for revoke endpoint.
1347 kwargs: kwargs, Additional parameters to add to the JWT token, for
1348 example sub=joe@xample.org.
1349
1350 Raises:
1351 CryptoUnavailableError if no crypto library is available.
1352 """
1353 _RequireCryptoOrDie()
1354 super(SignedJwtAssertionCredentials, self).__init__(
1355 None,
1356 user_agent=user_agent,
1357 token_uri=token_uri,
1358 revoke_uri=revoke_uri,
1359 )
1360
1361 self.scope = util.scopes_to_string(scope)
1362
1363
1364 self.private_key = base64.b64encode(private_key)
1365
1366 self.private_key_password = private_key_password
1367 self.service_account_name = service_account_name
1368 self.kwargs = kwargs
1369
1370 @classmethod
1372 data = json.loads(s)
1373 retval = SignedJwtAssertionCredentials(
1374 data['service_account_name'],
1375 base64.b64decode(data['private_key']),
1376 data['scope'],
1377 private_key_password=data['private_key_password'],
1378 user_agent=data['user_agent'],
1379 token_uri=data['token_uri'],
1380 **data['kwargs']
1381 )
1382 retval.invalid = data['invalid']
1383 retval.access_token = data['access_token']
1384 return retval
1385
1387 """Generate the assertion that will be used in the request."""
1388 now = long(time.time())
1389 payload = {
1390 'aud': self.token_uri,
1391 'scope': self.scope,
1392 'iat': now,
1393 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
1394 'iss': self.service_account_name
1395 }
1396 payload.update(self.kwargs)
1397 logger.debug(str(payload))
1398
1399 private_key = base64.b64decode(self.private_key)
1400 return crypt.make_signed_jwt(crypt.Signer.from_string(
1401 private_key, self.private_key_password), payload)
1402
1403
1404
1405 _cached_http = httplib2.Http(MemoryCache())
1410 """Verifies a signed JWT id_token.
1411
1412 This function requires PyOpenSSL and because of that it does not work on
1413 App Engine.
1414
1415 Args:
1416 id_token: string, A Signed JWT.
1417 audience: string, The audience 'aud' that the token should be for.
1418 http: httplib2.Http, instance to use to make the HTTP request. Callers
1419 should supply an instance that has caching enabled.
1420 cert_uri: string, URI of the certificates in JSON format to
1421 verify the JWT against.
1422
1423 Returns:
1424 The deserialized JSON in the JWT.
1425
1426 Raises:
1427 oauth2client.crypt.AppIdentityError: if the JWT fails to verify.
1428 CryptoUnavailableError: if no crypto library is available.
1429 """
1430 _RequireCryptoOrDie()
1431 if http is None:
1432 http = _cached_http
1433
1434 resp, content = http.request(cert_uri)
1435
1436 if resp.status == 200:
1437 certs = json.loads(content)
1438 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
1439 else:
1440 raise VerifyJwtTokenError('Status code: %d' % resp.status)
1441
1444
1445 b64string = b64string.encode('ascii')
1446 padded = b64string + '=' * (4 - len(b64string) % 4)
1447 return base64.urlsafe_b64decode(padded)
1448
1451 """Extract the JSON payload from a JWT.
1452
1453 Does the extraction w/o checking the signature.
1454
1455 Args:
1456 id_token: string, OAuth 2.0 id_token.
1457
1458 Returns:
1459 object, The deserialized JSON payload.
1460 """
1461 segments = id_token.split('.')
1462
1463 if len(segments) != 3:
1464 raise VerifyJwtTokenError(
1465 'Wrong number of segments in token: %s' % id_token)
1466
1467 return json.loads(_urlsafe_b64decode(segments[1]))
1468
1471 """Parses response of an exchange token request.
1472
1473 Most providers return JSON but some (e.g. Facebook) return a
1474 url-encoded string.
1475
1476 Args:
1477 content: The body of a response
1478
1479 Returns:
1480 Content as a dictionary object. Note that the dict could be empty,
1481 i.e. {}. That basically indicates a failure.
1482 """
1483 resp = {}
1484 try:
1485 resp = json.loads(content)
1486 except StandardError:
1487
1488
1489 resp = dict(urlparse.parse_qsl(content))
1490
1491
1492 if resp and 'expires' in resp:
1493 resp['expires_in'] = resp.pop('expires')
1494
1495 return resp
1496
1497
1498 @util.positional(4)
1499 -def credentials_from_code(client_id, client_secret, scope, code,
1500 redirect_uri='postmessage', http=None,
1501 user_agent=None, token_uri=GOOGLE_TOKEN_URI,
1502 auth_uri=GOOGLE_AUTH_URI,
1503 revoke_uri=GOOGLE_REVOKE_URI,
1504 device_uri=GOOGLE_DEVICE_URI):
1505 """Exchanges an authorization code for an OAuth2Credentials object.
1506
1507 Args:
1508 client_id: string, client identifier.
1509 client_secret: string, client secret.
1510 scope: string or iterable of strings, scope(s) to request.
1511 code: string, An authroization code, most likely passed down from
1512 the client
1513 redirect_uri: string, this is generally set to 'postmessage' to match the
1514 redirect_uri that the client specified
1515 http: httplib2.Http, optional http instance to use to do the fetch
1516 token_uri: string, URI for token endpoint. For convenience
1517 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1518 auth_uri: string, URI for authorization endpoint. For convenience
1519 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1520 revoke_uri: string, URI for revoke endpoint. For convenience
1521 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1522 device_uri: string, URI for device authorization endpoint. For convenience
1523 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1524
1525 Returns:
1526 An OAuth2Credentials object.
1527
1528 Raises:
1529 FlowExchangeError if the authorization code cannot be exchanged for an
1530 access token
1531 """
1532 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
1533 redirect_uri=redirect_uri, user_agent=user_agent,
1534 auth_uri=auth_uri, token_uri=token_uri,
1535 revoke_uri=revoke_uri, device_uri=device_uri)
1536
1537 credentials = flow.step2_exchange(code, http=http)
1538 return credentials
1539
1548 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
1549
1550 Will create the right kind of Flow based on the contents of the clientsecrets
1551 file or will raise InvalidClientSecretsError for unknown types of Flows.
1552
1553 Args:
1554 filename: string, File name of clientsecrets.
1555 scope: string or iterable of strings, scope(s) to request.
1556 code: string, An authorization code, most likely passed down from
1557 the client
1558 message: string, A friendly string to display to the user if the
1559 clientsecrets file is missing or invalid. If message is provided then
1560 sys.exit will be called in the case of an error. If message in not
1561 provided then clientsecrets.InvalidClientSecretsError will be raised.
1562 redirect_uri: string, this is generally set to 'postmessage' to match the
1563 redirect_uri that the client specified
1564 http: httplib2.Http, optional http instance to use to do the fetch
1565 cache: An optional cache service client that implements get() and set()
1566 methods. See clientsecrets.loadfile() for details.
1567 device_uri: string, OAuth 2.0 device authorization endpoint
1568
1569 Returns:
1570 An OAuth2Credentials object.
1571
1572 Raises:
1573 FlowExchangeError if the authorization code cannot be exchanged for an
1574 access token
1575 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1576 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1577 invalid.
1578 """
1579 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
1580 redirect_uri=redirect_uri,
1581 device_uri=device_uri)
1582 credentials = flow.step2_exchange(code, http=http)
1583 return credentials
1584
1585
1586 -class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
1587 'device_code', 'user_code', 'interval', 'verification_url',
1588 'user_code_expiry'))):
1589 """Intermediate information the OAuth2 for devices flow."""
1590
1591 @classmethod
1593 """Create a DeviceFlowInfo from a server response.
1594
1595 The response should be a dict containing entries as described
1596 here:
1597 http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1
1598 """
1599
1600 kwargs = {
1601 'device_code': response['device_code'],
1602 'user_code': response['user_code'],
1603 }
1604
1605
1606 verification_url = response.get(
1607 'verification_url', response.get('verification_uri'))
1608 if verification_url is None:
1609 raise OAuth2DeviceCodeError(
1610 'No verification_url provided in server response')
1611 kwargs['verification_url'] = verification_url
1612
1613 kwargs.update({
1614 'interval': response.get('interval'),
1615 'user_code_expiry': None,
1616 })
1617 if 'expires_in' in response:
1618 kwargs['user_code_expiry'] = datetime.datetime.now() + datetime.timedelta(
1619 seconds=int(response['expires_in']))
1620
1621 return cls(**kwargs)
1622
1624 """Does the Web Server Flow for OAuth 2.0.
1625
1626 OAuth2WebServerFlow objects may be safely pickled and unpickled.
1627 """
1628
1629 @util.positional(4)
1630 - def __init__(self, client_id, client_secret, scope,
1631 redirect_uri=None,
1632 user_agent=None,
1633 auth_uri=GOOGLE_AUTH_URI,
1634 token_uri=GOOGLE_TOKEN_URI,
1635 revoke_uri=GOOGLE_REVOKE_URI,
1636 login_hint=None,
1637 device_uri=GOOGLE_DEVICE_URI,
1638 **kwargs):
1639 """Constructor for OAuth2WebServerFlow.
1640
1641 The kwargs argument is used to set extra query parameters on the
1642 auth_uri. For example, the access_type and approval_prompt
1643 query parameters can be set via kwargs.
1644
1645 Args:
1646 client_id: string, client identifier.
1647 client_secret: string client secret.
1648 scope: string or iterable of strings, scope(s) of the credentials being
1649 requested.
1650 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1651 a non-web-based application, or a URI that handles the callback from
1652 the authorization server.
1653 user_agent: string, HTTP User-Agent to provide for this application.
1654 auth_uri: string, URI for authorization endpoint. For convenience
1655 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1656 token_uri: string, URI for token endpoint. For convenience
1657 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1658 revoke_uri: string, URI for revoke endpoint. For convenience
1659 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1660 login_hint: string, Either an email address or domain. Passing this hint
1661 will either pre-fill the email box on the sign-in form or select the
1662 proper multi-login session, thereby simplifying the login flow.
1663 device_uri: string, URI for device authorization endpoint. For convenience
1664 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1665 **kwargs: dict, The keyword arguments are all optional and required
1666 parameters for the OAuth calls.
1667 """
1668 self.client_id = client_id
1669 self.client_secret = client_secret
1670 self.scope = util.scopes_to_string(scope)
1671 self.redirect_uri = redirect_uri
1672 self.login_hint = login_hint
1673 self.user_agent = user_agent
1674 self.auth_uri = auth_uri
1675 self.token_uri = token_uri
1676 self.revoke_uri = revoke_uri
1677 self.device_uri = device_uri
1678 self.params = {
1679 'access_type': 'offline',
1680 'response_type': 'code',
1681 }
1682 self.params.update(kwargs)
1683
1684 @util.positional(1)
1686 """Returns a URI to redirect to the provider.
1687
1688 Args:
1689 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1690 a non-web-based application, or a URI that handles the callback from
1691 the authorization server. This parameter is deprecated, please move to
1692 passing the redirect_uri in via the constructor.
1693
1694 Returns:
1695 A URI as a string to redirect the user to begin the authorization flow.
1696 """
1697 if redirect_uri is not None:
1698 logger.warning((
1699 'The redirect_uri parameter for '
1700 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please '
1701 'move to passing the redirect_uri in via the constructor.'))
1702 self.redirect_uri = redirect_uri
1703
1704 if self.redirect_uri is None:
1705 raise ValueError('The value of redirect_uri must not be None.')
1706
1707 query_params = {
1708 'client_id': self.client_id,
1709 'redirect_uri': self.redirect_uri,
1710 'scope': self.scope,
1711 }
1712 if self.login_hint is not None:
1713 query_params['login_hint'] = self.login_hint
1714 query_params.update(self.params)
1715 return _update_query_params(self.auth_uri, query_params)
1716
1717 @util.positional(1)
1719 """Returns a user code and the verification URL where to enter it
1720
1721 Returns:
1722 A user code as a string for the user to authorize the application
1723 An URL as a string where the user has to enter the code
1724 """
1725 if self.device_uri is None:
1726 raise ValueError('The value of device_uri must not be None.')
1727
1728 body = urllib.urlencode({
1729 'client_id': self.client_id,
1730 'scope': self.scope,
1731 })
1732 headers = {
1733 'content-type': 'application/x-www-form-urlencoded',
1734 }
1735
1736 if self.user_agent is not None:
1737 headers['user-agent'] = self.user_agent
1738
1739 if http is None:
1740 http = httplib2.Http()
1741
1742 resp, content = http.request(self.device_uri, method='POST', body=body,
1743 headers=headers)
1744 if resp.status == 200:
1745 try:
1746 flow_info = json.loads(content)
1747 except ValueError as e:
1748 raise OAuth2DeviceCodeError(
1749 'Could not parse server response as JSON: "%s", error: "%s"' % (
1750 content, e))
1751 return DeviceFlowInfo.FromResponse(flow_info)
1752 else:
1753 error_msg = 'Invalid response %s.' % resp.status
1754 try:
1755 d = json.loads(content)
1756 if 'error' in d:
1757 error_msg += ' Error: %s' % d['error']
1758 except ValueError:
1759
1760 pass
1761 raise OAuth2DeviceCodeError(error_msg)
1762
1763 @util.positional(2)
1764 - def step2_exchange(self, code=None, http=None, device_flow_info=None):
1765 """Exchanges a code for OAuth2Credentials.
1766
1767 Args:
1768
1769 code: string, dict or None. For a non-device flow, this is
1770 either the response code as a string, or a dictionary of
1771 query parameters to the redirect_uri. For a device flow,
1772 this should be None.
1773 http: httplib2.Http, optional http instance to use when fetching
1774 credentials.
1775 device_flow_info: DeviceFlowInfo, return value from step1 in the
1776 case of a device flow.
1777
1778 Returns:
1779 An OAuth2Credentials object that can be used to authorize requests.
1780
1781 Raises:
1782 FlowExchangeError: if a problem occured exchanging the code for a
1783 refresh_token.
1784 ValueError: if code and device_flow_info are both provided or both
1785 missing.
1786
1787 """
1788 if code is None and device_flow_info is None:
1789 raise ValueError('No code or device_flow_info provided.')
1790 if code is not None and device_flow_info is not None:
1791 raise ValueError('Cannot provide both code and device_flow_info.')
1792
1793 if code is None:
1794 code = device_flow_info.device_code
1795 elif isinstance(code, dict):
1796 if 'code' not in code:
1797 raise FlowExchangeError(code.get(
1798 'error', 'No code was supplied in the query parameters.'))
1799 code = code['code']
1800
1801 post_data = {
1802 'client_id': self.client_id,
1803 'client_secret': self.client_secret,
1804 'code': code,
1805 'scope': self.scope,
1806 }
1807 if device_flow_info is not None:
1808 post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
1809 else:
1810 post_data['grant_type'] = 'authorization_code'
1811 post_data['redirect_uri'] = self.redirect_uri
1812 body = urllib.urlencode(post_data)
1813 headers = {
1814 'content-type': 'application/x-www-form-urlencoded',
1815 }
1816
1817 if self.user_agent is not None:
1818 headers['user-agent'] = self.user_agent
1819
1820 if http is None:
1821 http = httplib2.Http()
1822
1823 resp, content = http.request(self.token_uri, method='POST', body=body,
1824 headers=headers)
1825 d = _parse_exchange_token_response(content)
1826 if resp.status == 200 and 'access_token' in d:
1827 access_token = d['access_token']
1828 refresh_token = d.get('refresh_token', None)
1829 if not refresh_token:
1830 logger.info(
1831 'Received token response with no refresh_token. Consider '
1832 "reauthenticating with approval_prompt='force'.")
1833 token_expiry = None
1834 if 'expires_in' in d:
1835 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
1836 seconds=int(d['expires_in']))
1837
1838 if 'id_token' in d:
1839 d['id_token'] = _extract_id_token(d['id_token'])
1840
1841 logger.info('Successfully retrieved access token')
1842 return OAuth2Credentials(access_token, self.client_id,
1843 self.client_secret, refresh_token, token_expiry,
1844 self.token_uri, self.user_agent,
1845 revoke_uri=self.revoke_uri,
1846 id_token=d.get('id_token', None),
1847 token_response=d)
1848 else:
1849 logger.info('Failed to retrieve access token: %s', content)
1850 if 'error' in d:
1851
1852 error_msg = unicode(d['error'])
1853 else:
1854 error_msg = 'Invalid response: %s.' % str(resp.status)
1855 raise FlowExchangeError(error_msg)
1856
1857
1858 @util.positional(2)
1859 -def flow_from_clientsecrets(filename, scope, redirect_uri=None,
1860 message=None, cache=None, login_hint=None,
1861 device_uri=None):
1862 """Create a Flow from a clientsecrets file.
1863
1864 Will create the right kind of Flow based on the contents of the clientsecrets
1865 file or will raise InvalidClientSecretsError for unknown types of Flows.
1866
1867 Args:
1868 filename: string, File name of client secrets.
1869 scope: string or iterable of strings, scope(s) to request.
1870 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1871 a non-web-based application, or a URI that handles the callback from
1872 the authorization server.
1873 message: string, A friendly string to display to the user if the
1874 clientsecrets file is missing or invalid. If message is provided then
1875 sys.exit will be called in the case of an error. If message in not
1876 provided then clientsecrets.InvalidClientSecretsError will be raised.
1877 cache: An optional cache service client that implements get() and set()
1878 methods. See clientsecrets.loadfile() for details.
1879 login_hint: string, Either an email address or domain. Passing this hint
1880 will either pre-fill the email box on the sign-in form or select the
1881 proper multi-login session, thereby simplifying the login flow.
1882 device_uri: string, URI for device authorization endpoint. For convenience
1883 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1884
1885 Returns:
1886 A Flow object.
1887
1888 Raises:
1889 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1890 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1891 invalid.
1892 """
1893 try:
1894 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
1895 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED):
1896 constructor_kwargs = {
1897 'redirect_uri': redirect_uri,
1898 'auth_uri': client_info['auth_uri'],
1899 'token_uri': client_info['token_uri'],
1900 'login_hint': login_hint,
1901 }
1902 revoke_uri = client_info.get('revoke_uri')
1903 if revoke_uri is not None:
1904 constructor_kwargs['revoke_uri'] = revoke_uri
1905 if device_uri is not None:
1906 constructor_kwargs['device_uri'] = device_uri
1907 return OAuth2WebServerFlow(
1908 client_info['client_id'], client_info['client_secret'],
1909 scope, **constructor_kwargs)
1910
1911 except clientsecrets.InvalidClientSecretsError:
1912 if message:
1913 sys.exit(message)
1914 else:
1915 raise
1916 else:
1917 raise UnknownClientSecretsFlowError(
1918 'This OAuth 2.0 flow is unsupported: %r' % client_type)
1919