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