OLD | NEW |
1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Google OAuth2 related functions.""" | 5 """Google OAuth2 related functions.""" |
6 | 6 |
7 import BaseHTTPServer | 7 import BaseHTTPServer |
8 import collections | 8 import collections |
9 import datetime | 9 import datetime |
10 import functools | 10 import functools |
(...skipping 227 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
238 def __init__(self, token_cache_key, config): | 238 def __init__(self, token_cache_key, config): |
239 assert isinstance(config, AuthConfig) | 239 assert isinstance(config, AuthConfig) |
240 assert config.use_oauth2 | 240 assert config.use_oauth2 |
241 self._access_token = None | 241 self._access_token = None |
242 self._config = config | 242 self._config = config |
243 self._lock = threading.Lock() | 243 self._lock = threading.Lock() |
244 self._token_cache_key = token_cache_key | 244 self._token_cache_key = token_cache_key |
245 self._external_token = None | 245 self._external_token = None |
246 if config.refresh_token_json: | 246 if config.refresh_token_json: |
247 self._external_token = _read_refresh_token_json(config.refresh_token_json) | 247 self._external_token = _read_refresh_token_json(config.refresh_token_json) |
| 248 logging.debug('Using auth config %r', config) |
248 | 249 |
249 def login(self): | 250 def login(self): |
250 """Performs interactive login flow if necessary. | 251 """Performs interactive login flow if necessary. |
251 | 252 |
252 Raises: | 253 Raises: |
253 AuthenticationError on error or if interrupted. | 254 AuthenticationError on error or if interrupted. |
254 """ | 255 """ |
255 if self._external_token: | 256 if self._external_token: |
256 raise AuthenticationError( | 257 raise AuthenticationError( |
257 'Can\'t run login flow when using --auth-refresh-token-json.') | 258 'Can\'t run login flow when using --auth-refresh-token-json.') |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
299 force_refresh: forcefully refresh access token even if it is not expired. | 300 force_refresh: forcefully refresh access token even if it is not expired. |
300 allow_user_interaction: True to enable blocking for user input if needed. | 301 allow_user_interaction: True to enable blocking for user input if needed. |
301 | 302 |
302 Raises: | 303 Raises: |
303 AuthenticationError on error or if authentication flow was interrupted. | 304 AuthenticationError on error or if authentication flow was interrupted. |
304 LoginRequiredError if user interaction is required, but | 305 LoginRequiredError if user interaction is required, but |
305 allow_user_interaction is False. | 306 allow_user_interaction is False. |
306 """ | 307 """ |
307 with self._lock: | 308 with self._lock: |
308 if force_refresh: | 309 if force_refresh: |
| 310 logging.debug('Forcing access token refresh') |
309 self._access_token = self._create_access_token(allow_user_interaction) | 311 self._access_token = self._create_access_token(allow_user_interaction) |
310 return self._access_token | 312 return self._access_token |
311 | 313 |
312 # Load from on-disk cache on a first access. | 314 # Load from on-disk cache on a first access. |
313 if not self._access_token: | 315 if not self._access_token: |
314 self._access_token = self._load_access_token() | 316 self._access_token = self._load_access_token() |
315 | 317 |
316 # Refresh if expired or missing. | 318 # Refresh if expired or missing. |
317 if not self._access_token or _needs_refresh(self._access_token): | 319 if not self._access_token or _needs_refresh(self._access_token): |
318 # Maybe some other process already updated it, reload from the cache. | 320 # Maybe some other process already updated it, reload from the cache. |
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
374 ## Private methods. | 376 ## Private methods. |
375 | 377 |
376 def _get_storage(self): | 378 def _get_storage(self): |
377 """Returns oauth2client.Storage with cached tokens.""" | 379 """Returns oauth2client.Storage with cached tokens.""" |
378 # Do not mix cache keys for different externally provided tokens. | 380 # Do not mix cache keys for different externally provided tokens. |
379 if self._external_token: | 381 if self._external_token: |
380 token_hash = hashlib.sha1(self._external_token.refresh_token).hexdigest() | 382 token_hash = hashlib.sha1(self._external_token.refresh_token).hexdigest() |
381 cache_key = '%s:refresh_tok:%s' % (self._token_cache_key, token_hash) | 383 cache_key = '%s:refresh_tok:%s' % (self._token_cache_key, token_hash) |
382 else: | 384 else: |
383 cache_key = self._token_cache_key | 385 cache_key = self._token_cache_key |
| 386 logging.debug( |
| 387 'Using token storage %r (cache key %r)', OAUTH_TOKENS_CACHE, cache_key) |
384 return multistore_file.get_credential_storage_custom_string_key( | 388 return multistore_file.get_credential_storage_custom_string_key( |
385 OAUTH_TOKENS_CACHE, cache_key) | 389 OAUTH_TOKENS_CACHE, cache_key) |
386 | 390 |
387 def _get_cached_credentials(self): | 391 def _get_cached_credentials(self): |
388 """Returns oauth2client.Credentials loaded from storage.""" | 392 """Returns oauth2client.Credentials loaded from storage.""" |
389 storage = self._get_storage() | 393 storage = self._get_storage() |
390 credentials = storage.get() | 394 credentials = storage.get() |
391 | 395 |
| 396 if not credentials: |
| 397 logging.debug('No cached token') |
| 398 else: |
| 399 _log_credentials_info('cached token', credentials) |
| 400 |
392 # Is using --auth-refresh-token-json? | 401 # Is using --auth-refresh-token-json? |
393 if self._external_token: | 402 if self._external_token: |
394 # Cached credentials are valid and match external token -> use them. It is | 403 # Cached credentials are valid and match external token -> use them. It is |
395 # important to reuse credentials from the storage because they contain | 404 # important to reuse credentials from the storage because they contain |
396 # cached access token. | 405 # cached access token. |
397 valid = ( | 406 valid = ( |
398 credentials and not credentials.invalid and | 407 credentials and not credentials.invalid and |
399 credentials.refresh_token == self._external_token.refresh_token and | 408 credentials.refresh_token == self._external_token.refresh_token and |
400 credentials.client_id == self._external_token.client_id and | 409 credentials.client_id == self._external_token.client_id and |
401 credentials.client_secret == self._external_token.client_secret) | 410 credentials.client_secret == self._external_token.client_secret) |
402 if valid: | 411 if valid: |
| 412 logging.debug('Cached credentials match external refresh token') |
403 return credentials | 413 return credentials |
404 # Construct new credentials from externally provided refresh token, | 414 # Construct new credentials from externally provided refresh token, |
405 # associate them with cache storage (so that access_token will be placed | 415 # associate them with cache storage (so that access_token will be placed |
406 # in the cache later too). | 416 # in the cache later too). |
| 417 logging.debug('Putting external refresh token into the cache') |
407 credentials = client.OAuth2Credentials( | 418 credentials = client.OAuth2Credentials( |
408 access_token=None, | 419 access_token=None, |
409 client_id=self._external_token.client_id, | 420 client_id=self._external_token.client_id, |
410 client_secret=self._external_token.client_secret, | 421 client_secret=self._external_token.client_secret, |
411 refresh_token=self._external_token.refresh_token, | 422 refresh_token=self._external_token.refresh_token, |
412 token_expiry=None, | 423 token_expiry=None, |
413 token_uri='https://accounts.google.com/o/oauth2/token', | 424 token_uri='https://accounts.google.com/o/oauth2/token', |
414 user_agent=None, | 425 user_agent=None, |
415 revoke_uri=None) | 426 revoke_uri=None) |
416 credentials.set_store(storage) | 427 credentials.set_store(storage) |
417 storage.put(credentials) | 428 storage.put(credentials) |
418 return credentials | 429 return credentials |
419 | 430 |
420 # Not using external refresh token -> return whatever is cached. | 431 # Not using external refresh token -> return whatever is cached. |
421 return credentials if (credentials and not credentials.invalid) else None | 432 return credentials if (credentials and not credentials.invalid) else None |
422 | 433 |
423 def _load_access_token(self): | 434 def _load_access_token(self): |
424 """Returns cached AccessToken if it is not expired yet.""" | 435 """Returns cached AccessToken if it is not expired yet.""" |
| 436 logging.debug('Reloading access token from cache') |
425 creds = self._get_cached_credentials() | 437 creds = self._get_cached_credentials() |
426 if not creds or not creds.access_token or creds.access_token_expired: | 438 if not creds or not creds.access_token or creds.access_token_expired: |
| 439 logging.debug('Access token is missing or expired') |
427 return None | 440 return None |
428 return AccessToken(str(creds.access_token), creds.token_expiry) | 441 return AccessToken(str(creds.access_token), creds.token_expiry) |
429 | 442 |
430 def _create_access_token(self, allow_user_interaction=False): | 443 def _create_access_token(self, allow_user_interaction=False): |
431 """Mints and caches a new access token, launching OAuth2 dance if necessary. | 444 """Mints and caches a new access token, launching OAuth2 dance if necessary. |
432 | 445 |
433 Uses cached refresh token, if present. In that case user interaction is not | 446 Uses cached refresh token, if present. In that case user interaction is not |
434 required and function will finish quietly. Otherwise it will launch 3-legged | 447 required and function will finish quietly. Otherwise it will launch 3-legged |
435 OAuth2 flow, that needs user interaction. | 448 OAuth2 flow, that needs user interaction. |
436 | 449 |
437 Args: | 450 Args: |
438 allow_user_interaction: if True, allow interaction with the user (e.g. | 451 allow_user_interaction: if True, allow interaction with the user (e.g. |
439 reading standard input, or launching a browser). | 452 reading standard input, or launching a browser). |
440 | 453 |
441 Returns: | 454 Returns: |
442 AccessToken. | 455 AccessToken. |
443 | 456 |
444 Raises: | 457 Raises: |
445 AuthenticationError on error or if authentication flow was interrupted. | 458 AuthenticationError on error or if authentication flow was interrupted. |
446 LoginRequiredError if user interaction is required, but | 459 LoginRequiredError if user interaction is required, but |
447 allow_user_interaction is False. | 460 allow_user_interaction is False. |
448 """ | 461 """ |
| 462 logging.debug( |
| 463 'Making new access token (allow_user_interaction=%r)', |
| 464 allow_user_interaction) |
449 credentials = self._get_cached_credentials() | 465 credentials = self._get_cached_credentials() |
450 | 466 |
451 # 3-legged flow with (perhaps cached) refresh token. | 467 # 3-legged flow with (perhaps cached) refresh token. |
452 refreshed = False | 468 refreshed = False |
453 if credentials and not credentials.invalid: | 469 if credentials and not credentials.invalid: |
454 try: | 470 try: |
| 471 logging.debug('Attempting to refresh access_token') |
455 credentials.refresh(httplib2.Http()) | 472 credentials.refresh(httplib2.Http()) |
| 473 _log_credentials_info('refreshed token', credentials) |
456 refreshed = True | 474 refreshed = True |
457 except client.Error as err: | 475 except client.Error as err: |
458 logging.warning( | 476 logging.warning( |
459 'OAuth error during access token refresh (%s). ' | 477 'OAuth error during access token refresh (%s). ' |
460 'Attempting a full authentication flow.', err) | 478 'Attempting a full authentication flow.', err) |
461 | 479 |
462 # Refresh token is missing or invalid, go through the full flow. | 480 # Refresh token is missing or invalid, go through the full flow. |
463 if not refreshed: | 481 if not refreshed: |
464 # Can't refresh externally provided token. | 482 # Can't refresh externally provided token. |
465 if self._external_token: | 483 if self._external_token: |
466 raise AuthenticationError( | 484 raise AuthenticationError( |
467 'Token provided via --auth-refresh-token-json is no longer valid.') | 485 'Token provided via --auth-refresh-token-json is no longer valid.') |
468 if not allow_user_interaction: | 486 if not allow_user_interaction: |
| 487 logging.debug('Requesting user to login') |
469 raise LoginRequiredError(self._token_cache_key) | 488 raise LoginRequiredError(self._token_cache_key) |
| 489 logging.debug('Launching OAuth browser flow') |
470 credentials = _run_oauth_dance(self._config) | 490 credentials = _run_oauth_dance(self._config) |
| 491 _log_credentials_info('new token', credentials) |
471 | 492 |
472 logging.info( | 493 logging.info( |
473 'OAuth access_token refreshed. Expires in %s.', | 494 'OAuth access_token refreshed. Expires in %s.', |
474 credentials.token_expiry - datetime.datetime.utcnow()) | 495 credentials.token_expiry - datetime.datetime.utcnow()) |
475 storage = self._get_storage() | 496 storage = self._get_storage() |
476 credentials.set_store(storage) | 497 credentials.set_store(storage) |
477 storage.put(credentials) | 498 storage.put(credentials) |
478 return AccessToken(str(credentials.access_token), credentials.token_expiry) | 499 return AccessToken(str(credentials.access_token), credentials.token_expiry) |
479 | 500 |
480 | 501 |
(...skipping 25 matching lines...) Expand all Loading... |
506 def _needs_refresh(access_token): | 527 def _needs_refresh(access_token): |
507 """True if AccessToken should be refreshed.""" | 528 """True if AccessToken should be refreshed.""" |
508 if access_token.expires_at is not None: | 529 if access_token.expires_at is not None: |
509 # Allow 5 min of clock skew between client and backend. | 530 # Allow 5 min of clock skew between client and backend. |
510 now = datetime.datetime.utcnow() + datetime.timedelta(seconds=300) | 531 now = datetime.datetime.utcnow() + datetime.timedelta(seconds=300) |
511 return now >= access_token.expires_at | 532 return now >= access_token.expires_at |
512 # Token without expiration time never expires. | 533 # Token without expiration time never expires. |
513 return False | 534 return False |
514 | 535 |
515 | 536 |
| 537 def _log_credentials_info(title, credentials): |
| 538 """Dumps (non sensitive) part of client.Credentials object to debug log.""" |
| 539 if credentials: |
| 540 logging.debug('%s info: %r', title, { |
| 541 'access_token_expired': credentials.access_token_expired, |
| 542 'has_access_token': bool(credentials.access_token), |
| 543 'invalid': credentials.invalid, |
| 544 'utcnow': datetime.datetime.utcnow(), |
| 545 'token_expiry': credentials.token_expiry, |
| 546 }) |
| 547 |
| 548 |
516 def _run_oauth_dance(config): | 549 def _run_oauth_dance(config): |
517 """Perform full 3-legged OAuth2 flow with the browser. | 550 """Perform full 3-legged OAuth2 flow with the browser. |
518 | 551 |
519 Returns: | 552 Returns: |
520 oauth2client.Credentials. | 553 oauth2client.Credentials. |
521 | 554 |
522 Raises: | 555 Raises: |
523 AuthenticationError on errors. | 556 AuthenticationError on errors. |
524 """ | 557 """ |
525 flow = client.OAuth2WebServerFlow( | 558 flow = client.OAuth2WebServerFlow( |
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
619 self.end_headers() | 652 self.end_headers() |
620 query = self.path.split('?', 1)[-1] | 653 query = self.path.split('?', 1)[-1] |
621 query = dict(urlparse.parse_qsl(query)) | 654 query = dict(urlparse.parse_qsl(query)) |
622 self.server.query_params = query | 655 self.server.query_params = query |
623 self.wfile.write('<html><head><title>Authentication Status</title></head>') | 656 self.wfile.write('<html><head><title>Authentication Status</title></head>') |
624 self.wfile.write('<body><p>The authentication flow has completed.</p>') | 657 self.wfile.write('<body><p>The authentication flow has completed.</p>') |
625 self.wfile.write('</body></html>') | 658 self.wfile.write('</body></html>') |
626 | 659 |
627 def log_message(self, _format, *args): | 660 def log_message(self, _format, *args): |
628 """Do not log messages to stdout while running as command line program.""" | 661 """Do not log messages to stdout while running as command line program.""" |
OLD | NEW |