Chromium Code Reviews| 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 201 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 212 a cache key for token cache. | 212 a cache key for token cache. |
| 213 config: AuthConfig instance. | 213 config: AuthConfig instance. |
| 214 | 214 |
| 215 Returns: | 215 Returns: |
| 216 Authenticator object. | 216 Authenticator object. |
| 217 """ | 217 """ |
| 218 hostname = hostname.lower().rstrip('/') | 218 hostname = hostname.lower().rstrip('/') |
| 219 # Append some scheme, otherwise urlparse puts hostname into parsed.path. | 219 # Append some scheme, otherwise urlparse puts hostname into parsed.path. |
| 220 if '://' not in hostname: | 220 if '://' not in hostname: |
| 221 hostname = 'https://' + hostname | 221 hostname = 'https://' + hostname |
| 222 scopes = OAUTH_SCOPES | |
| 223 if hostname == 'https://code.google.com': | |
|
Vadim Sh.
2015/06/11 01:37:31
make it a top-level config-like dict, so it looks
seanmccullough
2015/06/11 02:05:22
Done.
| |
| 224 scopes = "%s https://www.googleapis.com/auth/projecthosting" % scopes | |
| 222 parsed = urlparse.urlparse(hostname) | 225 parsed = urlparse.urlparse(hostname) |
| 223 if parsed.path or parsed.params or parsed.query or parsed.fragment: | 226 if parsed.path or parsed.params or parsed.query or parsed.fragment: |
| 224 raise AuthenticationError( | 227 raise AuthenticationError( |
| 225 'Expecting a hostname or root host URL, got %s instead' % hostname) | 228 'Expecting a hostname or root host URL, got %s instead' % hostname) |
| 226 return Authenticator(parsed.netloc, config) | 229 return Authenticator(parsed.netloc, config, scopes) |
| 227 | 230 |
| 228 | 231 |
| 229 class Authenticator(object): | 232 class Authenticator(object): |
| 230 """Object that knows how to refresh access tokens when needed. | 233 """Object that knows how to refresh access tokens when needed. |
| 231 | 234 |
| 232 Args: | 235 Args: |
| 233 token_cache_key: string key of a section of the token cache file to use | 236 token_cache_key: string key of a section of the token cache file to use |
| 234 to keep the tokens. See hostname_to_token_cache_key. | 237 to keep the tokens. See hostname_to_token_cache_key. |
| 235 config: AuthConfig object that holds authentication configuration. | 238 config: AuthConfig object that holds authentication configuration. |
| 236 """ | 239 """ |
| 237 | 240 |
| 238 def __init__(self, token_cache_key, config): | 241 def __init__(self, token_cache_key, config, scopes): |
| 239 assert isinstance(config, AuthConfig) | 242 assert isinstance(config, AuthConfig) |
| 240 assert config.use_oauth2 | 243 assert config.use_oauth2 |
| 241 self._access_token = None | 244 self._access_token = None |
| 242 self._config = config | 245 self._config = config |
| 243 self._lock = threading.Lock() | 246 self._lock = threading.Lock() |
| 244 self._token_cache_key = token_cache_key | 247 self._token_cache_key = token_cache_key |
| 245 self._external_token = None | 248 self._external_token = None |
| 249 self._scopes = scopes | |
| 246 if config.refresh_token_json: | 250 if config.refresh_token_json: |
| 247 self._external_token = _read_refresh_token_json(config.refresh_token_json) | 251 self._external_token = _read_refresh_token_json(config.refresh_token_json) |
| 248 logging.debug('Using auth config %r', config) | 252 logging.debug('Using auth config %r', config) |
| 249 | 253 |
| 250 def login(self): | 254 def login(self): |
| 251 """Performs interactive login flow if necessary. | 255 """Performs interactive login flow if necessary. |
| 252 | 256 |
| 253 Raises: | 257 Raises: |
| 254 AuthenticationError on error or if interrupted. | 258 AuthenticationError on error or if interrupted. |
| 255 """ | 259 """ |
| (...skipping 224 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 480 # Refresh token is missing or invalid, go through the full flow. | 484 # Refresh token is missing or invalid, go through the full flow. |
| 481 if not refreshed: | 485 if not refreshed: |
| 482 # Can't refresh externally provided token. | 486 # Can't refresh externally provided token. |
| 483 if self._external_token: | 487 if self._external_token: |
| 484 raise AuthenticationError( | 488 raise AuthenticationError( |
| 485 'Token provided via --auth-refresh-token-json is no longer valid.') | 489 'Token provided via --auth-refresh-token-json is no longer valid.') |
| 486 if not allow_user_interaction: | 490 if not allow_user_interaction: |
| 487 logging.debug('Requesting user to login') | 491 logging.debug('Requesting user to login') |
| 488 raise LoginRequiredError(self._token_cache_key) | 492 raise LoginRequiredError(self._token_cache_key) |
| 489 logging.debug('Launching OAuth browser flow') | 493 logging.debug('Launching OAuth browser flow') |
| 490 credentials = _run_oauth_dance(self._config) | 494 credentials = _run_oauth_dance(self._config, self._scopes) |
| 491 _log_credentials_info('new token', credentials) | 495 _log_credentials_info('new token', credentials) |
| 492 | 496 |
| 493 logging.info( | 497 logging.info( |
| 494 'OAuth access_token refreshed. Expires in %s.', | 498 'OAuth access_token refreshed. Expires in %s.', |
| 495 credentials.token_expiry - datetime.datetime.utcnow()) | 499 credentials.token_expiry - datetime.datetime.utcnow()) |
| 496 storage = self._get_storage() | 500 storage = self._get_storage() |
| 497 credentials.set_store(storage) | 501 credentials.set_store(storage) |
| 498 storage.put(credentials) | 502 storage.put(credentials) |
| 499 return AccessToken(str(credentials.access_token), credentials.token_expiry) | 503 return AccessToken(str(credentials.access_token), credentials.token_expiry) |
| 500 | 504 |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 553 if credentials: | 557 if credentials: |
| 554 logging.debug('%s info: %r', title, { | 558 logging.debug('%s info: %r', title, { |
| 555 'access_token_expired': credentials.access_token_expired, | 559 'access_token_expired': credentials.access_token_expired, |
| 556 'has_access_token': bool(credentials.access_token), | 560 'has_access_token': bool(credentials.access_token), |
| 557 'invalid': credentials.invalid, | 561 'invalid': credentials.invalid, |
| 558 'utcnow': datetime.datetime.utcnow(), | 562 'utcnow': datetime.datetime.utcnow(), |
| 559 'token_expiry': credentials.token_expiry, | 563 'token_expiry': credentials.token_expiry, |
| 560 }) | 564 }) |
| 561 | 565 |
| 562 | 566 |
| 563 def _run_oauth_dance(config): | 567 def _run_oauth_dance(config, scopes): |
| 564 """Perform full 3-legged OAuth2 flow with the browser. | 568 """Perform full 3-legged OAuth2 flow with the browser. |
| 565 | 569 |
| 566 Returns: | 570 Returns: |
| 567 oauth2client.Credentials. | 571 oauth2client.Credentials. |
| 568 | 572 |
| 569 Raises: | 573 Raises: |
| 570 AuthenticationError on errors. | 574 AuthenticationError on errors. |
| 571 """ | 575 """ |
| 572 flow = client.OAuth2WebServerFlow( | 576 flow = client.OAuth2WebServerFlow( |
| 573 OAUTH_CLIENT_ID, | 577 OAUTH_CLIENT_ID, |
| 574 OAUTH_CLIENT_SECRET, | 578 OAUTH_CLIENT_SECRET, |
| 575 OAUTH_SCOPES, | 579 scopes, |
| 576 approval_prompt='force') | 580 approval_prompt='force') |
| 577 | 581 |
| 578 use_local_webserver = config.use_local_webserver | 582 use_local_webserver = config.use_local_webserver |
| 579 port = config.webserver_port | 583 port = config.webserver_port |
| 580 if config.use_local_webserver: | 584 if config.use_local_webserver: |
| 581 success = False | 585 success = False |
| 582 try: | 586 try: |
| 583 httpd = _ClientRedirectServer(('localhost', port), _ClientRedirectHandler) | 587 httpd = _ClientRedirectServer(('localhost', port), _ClientRedirectHandler) |
| 584 except socket.error: | 588 except socket.error: |
| 585 pass | 589 pass |
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 666 self.end_headers() | 670 self.end_headers() |
| 667 query = self.path.split('?', 1)[-1] | 671 query = self.path.split('?', 1)[-1] |
| 668 query = dict(urlparse.parse_qsl(query)) | 672 query = dict(urlparse.parse_qsl(query)) |
| 669 self.server.query_params = query | 673 self.server.query_params = query |
| 670 self.wfile.write('<html><head><title>Authentication Status</title></head>') | 674 self.wfile.write('<html><head><title>Authentication Status</title></head>') |
| 671 self.wfile.write('<body><p>The authentication flow has completed.</p>') | 675 self.wfile.write('<body><p>The authentication flow has completed.</p>') |
| 672 self.wfile.write('</body></html>') | 676 self.wfile.write('</body></html>') |
| 673 | 677 |
| 674 def log_message(self, _format, *args): | 678 def log_message(self, _format, *args): |
| 675 """Do not log messages to stdout while running as command line program.""" | 679 """Do not log messages to stdout while running as command line program.""" |
| OLD | NEW |