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