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