| 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 |