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