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 _log_credentials_info('cached token', credentials) | |
397 | |
392 # Is using --auth-refresh-token-json? | 398 # Is using --auth-refresh-token-json? |
393 if self._external_token: | 399 if self._external_token: |
394 # Cached credentials are valid and match external token -> use them. It is | 400 # Cached credentials are valid and match external token -> use them. It is |
395 # important to reuse credentials from the storage because they contain | 401 # important to reuse credentials from the storage because they contain |
396 # cached access token. | 402 # cached access token. |
397 valid = ( | 403 valid = ( |
398 credentials and not credentials.invalid and | 404 credentials and not credentials.invalid and |
399 credentials.refresh_token == self._external_token.refresh_token and | 405 credentials.refresh_token == self._external_token.refresh_token and |
400 credentials.client_id == self._external_token.client_id and | 406 credentials.client_id == self._external_token.client_id and |
401 credentials.client_secret == self._external_token.client_secret) | 407 credentials.client_secret == self._external_token.client_secret) |
402 if valid: | 408 if valid: |
409 logging.debug('Cached credentials match external refresh token') | |
403 return credentials | 410 return credentials |
404 # Construct new credentials from externally provided refresh token, | 411 # Construct new credentials from externally provided refresh token, |
405 # associate them with cache storage (so that access_token will be placed | 412 # associate them with cache storage (so that access_token will be placed |
406 # in the cache later too). | 413 # in the cache later too). |
414 logging.debug('Putting external refresh token into the cache') | |
407 credentials = client.OAuth2Credentials( | 415 credentials = client.OAuth2Credentials( |
408 access_token=None, | 416 access_token=None, |
409 client_id=self._external_token.client_id, | 417 client_id=self._external_token.client_id, |
410 client_secret=self._external_token.client_secret, | 418 client_secret=self._external_token.client_secret, |
411 refresh_token=self._external_token.refresh_token, | 419 refresh_token=self._external_token.refresh_token, |
412 token_expiry=None, | 420 token_expiry=None, |
413 token_uri='https://accounts.google.com/o/oauth2/token', | 421 token_uri='https://accounts.google.com/o/oauth2/token', |
414 user_agent=None, | 422 user_agent=None, |
415 revoke_uri=None) | 423 revoke_uri=None) |
416 credentials.set_store(storage) | 424 credentials.set_store(storage) |
417 storage.put(credentials) | 425 storage.put(credentials) |
418 return credentials | 426 return credentials |
419 | 427 |
420 # Not using external refresh token -> return whatever is cached. | 428 # Not using external refresh token -> return whatever is cached. |
421 return credentials if (credentials and not credentials.invalid) else None | 429 return credentials if (credentials and not credentials.invalid) else None |
422 | 430 |
423 def _load_access_token(self): | 431 def _load_access_token(self): |
424 """Returns cached AccessToken if it is not expired yet.""" | 432 """Returns cached AccessToken if it is not expired yet.""" |
433 logging.debug('Reloading access token from cache') | |
425 creds = self._get_cached_credentials() | 434 creds = self._get_cached_credentials() |
426 if not creds or not creds.access_token or creds.access_token_expired: | 435 if not creds or not creds.access_token or creds.access_token_expired: |
436 logging.debug('Access token is missing or expired') | |
427 return None | 437 return None |
428 return AccessToken(str(creds.access_token), creds.token_expiry) | 438 return AccessToken(str(creds.access_token), creds.token_expiry) |
429 | 439 |
430 def _create_access_token(self, allow_user_interaction=False): | 440 def _create_access_token(self, allow_user_interaction=False): |
431 """Mints and caches a new access token, launching OAuth2 dance if necessary. | 441 """Mints and caches a new access token, launching OAuth2 dance if necessary. |
432 | 442 |
433 Uses cached refresh token, if present. In that case user interaction is not | 443 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 | 444 required and function will finish quietly. Otherwise it will launch 3-legged |
435 OAuth2 flow, that needs user interaction. | 445 OAuth2 flow, that needs user interaction. |
436 | 446 |
437 Args: | 447 Args: |
438 allow_user_interaction: if True, allow interaction with the user (e.g. | 448 allow_user_interaction: if True, allow interaction with the user (e.g. |
439 reading standard input, or launching a browser). | 449 reading standard input, or launching a browser). |
440 | 450 |
441 Returns: | 451 Returns: |
442 AccessToken. | 452 AccessToken. |
443 | 453 |
444 Raises: | 454 Raises: |
445 AuthenticationError on error or if authentication flow was interrupted. | 455 AuthenticationError on error or if authentication flow was interrupted. |
446 LoginRequiredError if user interaction is required, but | 456 LoginRequiredError if user interaction is required, but |
447 allow_user_interaction is False. | 457 allow_user_interaction is False. |
448 """ | 458 """ |
459 logging.debug( | |
460 'Making new access token (allow_user_interaction=%r)', | |
461 allow_user_interaction) | |
449 credentials = self._get_cached_credentials() | 462 credentials = self._get_cached_credentials() |
450 | 463 |
451 # 3-legged flow with (perhaps cached) refresh token. | 464 # 3-legged flow with (perhaps cached) refresh token. |
452 refreshed = False | 465 refreshed = False |
453 if credentials and not credentials.invalid: | 466 if credentials and not credentials.invalid: |
454 try: | 467 try: |
468 logging.debug('Attempting to refresh access_token') | |
455 credentials.refresh(httplib2.Http()) | 469 credentials.refresh(httplib2.Http()) |
470 _log_credentials_info('refreshed token', credentials) | |
456 refreshed = True | 471 refreshed = True |
457 except client.Error as err: | 472 except client.Error as err: |
458 logging.warning( | 473 logging.warning( |
459 'OAuth error during access token refresh (%s). ' | 474 'OAuth error during access token refresh (%s). ' |
460 'Attempting a full authentication flow.', err) | 475 'Attempting a full authentication flow.', err) |
461 | 476 |
462 # Refresh token is missing or invalid, go through the full flow. | 477 # Refresh token is missing or invalid, go through the full flow. |
463 if not refreshed: | 478 if not refreshed: |
464 # Can't refresh externally provided token. | 479 # Can't refresh externally provided token. |
465 if self._external_token: | 480 if self._external_token: |
466 raise AuthenticationError( | 481 raise AuthenticationError( |
467 'Token provided via --auth-refresh-token-json is no longer valid.') | 482 'Token provided via --auth-refresh-token-json is no longer valid.') |
468 if not allow_user_interaction: | 483 if not allow_user_interaction: |
484 logging.debug('Requesting user to login') | |
469 raise LoginRequiredError(self._token_cache_key) | 485 raise LoginRequiredError(self._token_cache_key) |
486 logging.debug('Launching OAuth browser flow') | |
470 credentials = _run_oauth_dance(self._config) | 487 credentials = _run_oauth_dance(self._config) |
488 _log_credentials_info('new token', credentials) | |
471 | 489 |
472 logging.info( | 490 logging.info( |
473 'OAuth access_token refreshed. Expires in %s.', | 491 'OAuth access_token refreshed. Expires in %s.', |
474 credentials.token_expiry - datetime.datetime.utcnow()) | 492 credentials.token_expiry - datetime.datetime.utcnow()) |
475 storage = self._get_storage() | 493 storage = self._get_storage() |
476 credentials.set_store(storage) | 494 credentials.set_store(storage) |
477 storage.put(credentials) | 495 storage.put(credentials) |
478 return AccessToken(str(credentials.access_token), credentials.token_expiry) | 496 return AccessToken(str(credentials.access_token), credentials.token_expiry) |
479 | 497 |
480 | 498 |
(...skipping 25 matching lines...) Expand all Loading... | |
506 def _needs_refresh(access_token): | 524 def _needs_refresh(access_token): |
507 """True if AccessToken should be refreshed.""" | 525 """True if AccessToken should be refreshed.""" |
508 if access_token.expires_at is not None: | 526 if access_token.expires_at is not None: |
509 # Allow 5 min of clock skew between client and backend. | 527 # Allow 5 min of clock skew between client and backend. |
510 now = datetime.datetime.utcnow() + datetime.timedelta(seconds=300) | 528 now = datetime.datetime.utcnow() + datetime.timedelta(seconds=300) |
511 return now >= access_token.expires_at | 529 return now >= access_token.expires_at |
512 # Token without expiration time never expires. | 530 # Token without expiration time never expires. |
513 return False | 531 return False |
514 | 532 |
515 | 533 |
534 def _log_credentials_info(title, credentials): | |
535 """Dumps (non sensitive) part of client.Credentials object to debug log.""" | |
536 if not credentials: | |
537 logging.debug('No %s', title) | |
nodir
2015/04/21 00:01:11
I think there is only one case that (not credentia
Vadim Sh.
2015/04/21 00:04:21
Done. Though I still prefer to be defensive and ch
| |
538 else: | |
539 logging.debug('%s info: %r', title, { | |
540 'access_token_expired': credentials.access_token_expired, | |
541 'has_access_token': bool(credentials.access_token), | |
542 'invalid': credentials.invalid, | |
543 'now': datetime.datetime.utcnow(), | |
nodir
2015/04/21 00:01:12
Please say utcnow
Vadim Sh.
2015/04/21 00:04:21
Done.
| |
544 'token_expiry': credentials.token_expiry, | |
545 }) | |
546 | |
547 | |
516 def _run_oauth_dance(config): | 548 def _run_oauth_dance(config): |
517 """Perform full 3-legged OAuth2 flow with the browser. | 549 """Perform full 3-legged OAuth2 flow with the browser. |
518 | 550 |
519 Returns: | 551 Returns: |
520 oauth2client.Credentials. | 552 oauth2client.Credentials. |
521 | 553 |
522 Raises: | 554 Raises: |
523 AuthenticationError on errors. | 555 AuthenticationError on errors. |
524 """ | 556 """ |
525 flow = client.OAuth2WebServerFlow( | 557 flow = client.OAuth2WebServerFlow( |
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
619 self.end_headers() | 651 self.end_headers() |
620 query = self.path.split('?', 1)[-1] | 652 query = self.path.split('?', 1)[-1] |
621 query = dict(urlparse.parse_qsl(query)) | 653 query = dict(urlparse.parse_qsl(query)) |
622 self.server.query_params = query | 654 self.server.query_params = query |
623 self.wfile.write('<html><head><title>Authentication Status</title></head>') | 655 self.wfile.write('<html><head><title>Authentication Status</title></head>') |
624 self.wfile.write('<body><p>The authentication flow has completed.</p>') | 656 self.wfile.write('<body><p>The authentication flow has completed.</p>') |
625 self.wfile.write('</body></html>') | 657 self.wfile.write('</body></html>') |
626 | 658 |
627 def log_message(self, _format, *args): | 659 def log_message(self, _format, *args): |
628 """Do not log messages to stdout while running as command line program.""" | 660 """Do not log messages to stdout while running as command line program.""" |
OLD | NEW |