| 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 30 matching lines...) Expand all Loading... |
| 41 # anyway. | 41 # anyway. |
| 42 # This particular set is managed by API Console project "chrome-infra-auth". | 42 # This particular set is managed by API Console project "chrome-infra-auth". |
| 43 OAUTH_CLIENT_ID = ( | 43 OAUTH_CLIENT_ID = ( |
| 44 '446450136466-2hr92jrq8e6i4tnsa56b52vacp7t3936.apps.googleusercontent.com') | 44 '446450136466-2hr92jrq8e6i4tnsa56b52vacp7t3936.apps.googleusercontent.com') |
| 45 OAUTH_CLIENT_SECRET = 'uBfbay2KCy9t4QveJ-dOqHtp' | 45 OAUTH_CLIENT_SECRET = 'uBfbay2KCy9t4QveJ-dOqHtp' |
| 46 | 46 |
| 47 # List of space separated OAuth scopes for generated tokens. GAE apps usually | 47 # List of space separated OAuth scopes for generated tokens. GAE apps usually |
| 48 # use userinfo.email scope for authentication. | 48 # use userinfo.email scope for authentication. |
| 49 OAUTH_SCOPES = 'https://www.googleapis.com/auth/userinfo.email' | 49 OAUTH_SCOPES = 'https://www.googleapis.com/auth/userinfo.email' |
| 50 | 50 |
| 51 # Additional OAuth scopes. |
| 52 ADDITIONAL_SCOPES = { |
| 53 'code.google.com': 'https://www.googleapis.com/auth/projecthosting', |
| 54 } |
| 55 |
| 51 # Path to a file with cached OAuth2 credentials used by default relative to the | 56 # Path to a file with cached OAuth2 credentials used by default relative to the |
| 52 # home dir (see _get_token_cache_path). It should be a safe location accessible | 57 # home dir (see _get_token_cache_path). It should be a safe location accessible |
| 53 # only to a current user: knowing content of this file is roughly equivalent to | 58 # only to a current user: knowing content of this file is roughly equivalent to |
| 54 # knowing account password. Single file can hold multiple independent tokens | 59 # knowing account password. Single file can hold multiple independent tokens |
| 55 # identified by token_cache_key (see Authenticator). | 60 # identified by token_cache_key (see Authenticator). |
| 56 OAUTH_TOKENS_CACHE = '.depot_tools_oauth2_tokens' | 61 OAUTH_TOKENS_CACHE = '.depot_tools_oauth2_tokens' |
| 57 | 62 |
| 58 | 63 |
| 59 # Authentication configuration extracted from command line options. | 64 # Authentication configuration extracted from command line options. |
| 60 # See doc string for 'make_auth_config' for meaning of fields. | 65 # See doc string for 'make_auth_config' for meaning of fields. |
| (...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 212 a cache key for token cache. | 217 a cache key for token cache. |
| 213 config: AuthConfig instance. | 218 config: AuthConfig instance. |
| 214 | 219 |
| 215 Returns: | 220 Returns: |
| 216 Authenticator object. | 221 Authenticator object. |
| 217 """ | 222 """ |
| 218 hostname = hostname.lower().rstrip('/') | 223 hostname = hostname.lower().rstrip('/') |
| 219 # Append some scheme, otherwise urlparse puts hostname into parsed.path. | 224 # Append some scheme, otherwise urlparse puts hostname into parsed.path. |
| 220 if '://' not in hostname: | 225 if '://' not in hostname: |
| 221 hostname = 'https://' + hostname | 226 hostname = 'https://' + hostname |
| 227 scopes = OAUTH_SCOPES |
| 222 parsed = urlparse.urlparse(hostname) | 228 parsed = urlparse.urlparse(hostname) |
| 229 if parsed.netloc in ADDITIONAL_SCOPES: |
| 230 scopes = "%s %s" % (scopes, ADDITIONAL_SCOPES[parsed.netloc]) |
| 231 |
| 223 if parsed.path or parsed.params or parsed.query or parsed.fragment: | 232 if parsed.path or parsed.params or parsed.query or parsed.fragment: |
| 224 raise AuthenticationError( | 233 raise AuthenticationError( |
| 225 'Expecting a hostname or root host URL, got %s instead' % hostname) | 234 'Expecting a hostname or root host URL, got %s instead' % hostname) |
| 226 return Authenticator(parsed.netloc, config) | 235 return Authenticator(parsed.netloc, config, scopes) |
| 227 | 236 |
| 228 | 237 |
| 229 class Authenticator(object): | 238 class Authenticator(object): |
| 230 """Object that knows how to refresh access tokens when needed. | 239 """Object that knows how to refresh access tokens when needed. |
| 231 | 240 |
| 232 Args: | 241 Args: |
| 233 token_cache_key: string key of a section of the token cache file to use | 242 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. | 243 to keep the tokens. See hostname_to_token_cache_key. |
| 235 config: AuthConfig object that holds authentication configuration. | 244 config: AuthConfig object that holds authentication configuration. |
| 236 """ | 245 """ |
| 237 | 246 |
| 238 def __init__(self, token_cache_key, config): | 247 def __init__(self, token_cache_key, config, scopes): |
| 239 assert isinstance(config, AuthConfig) | 248 assert isinstance(config, AuthConfig) |
| 240 assert config.use_oauth2 | 249 assert config.use_oauth2 |
| 241 self._access_token = None | 250 self._access_token = None |
| 242 self._config = config | 251 self._config = config |
| 243 self._lock = threading.Lock() | 252 self._lock = threading.Lock() |
| 244 self._token_cache_key = token_cache_key | 253 self._token_cache_key = token_cache_key |
| 245 self._external_token = None | 254 self._external_token = None |
| 255 self._scopes = scopes |
| 246 if config.refresh_token_json: | 256 if config.refresh_token_json: |
| 247 self._external_token = _read_refresh_token_json(config.refresh_token_json) | 257 self._external_token = _read_refresh_token_json(config.refresh_token_json) |
| 248 logging.debug('Using auth config %r', config) | 258 logging.debug('Using auth config %r', config) |
| 249 | 259 |
| 250 def login(self): | 260 def login(self): |
| 251 """Performs interactive login flow if necessary. | 261 """Performs interactive login flow if necessary. |
| 252 | 262 |
| 253 Raises: | 263 Raises: |
| 254 AuthenticationError on error or if interrupted. | 264 AuthenticationError on error or if interrupted. |
| 255 """ | 265 """ |
| (...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. | 490 # Refresh token is missing or invalid, go through the full flow. |
| 481 if not refreshed: | 491 if not refreshed: |
| 482 # Can't refresh externally provided token. | 492 # Can't refresh externally provided token. |
| 483 if self._external_token: | 493 if self._external_token: |
| 484 raise AuthenticationError( | 494 raise AuthenticationError( |
| 485 'Token provided via --auth-refresh-token-json is no longer valid.') | 495 'Token provided via --auth-refresh-token-json is no longer valid.') |
| 486 if not allow_user_interaction: | 496 if not allow_user_interaction: |
| 487 logging.debug('Requesting user to login') | 497 logging.debug('Requesting user to login') |
| 488 raise LoginRequiredError(self._token_cache_key) | 498 raise LoginRequiredError(self._token_cache_key) |
| 489 logging.debug('Launching OAuth browser flow') | 499 logging.debug('Launching OAuth browser flow') |
| 490 credentials = _run_oauth_dance(self._config) | 500 credentials = _run_oauth_dance(self._config, self._scopes) |
| 491 _log_credentials_info('new token', credentials) | 501 _log_credentials_info('new token', credentials) |
| 492 | 502 |
| 493 logging.info( | 503 logging.info( |
| 494 'OAuth access_token refreshed. Expires in %s.', | 504 'OAuth access_token refreshed. Expires in %s.', |
| 495 credentials.token_expiry - datetime.datetime.utcnow()) | 505 credentials.token_expiry - datetime.datetime.utcnow()) |
| 496 storage = self._get_storage() | 506 storage = self._get_storage() |
| 497 credentials.set_store(storage) | 507 credentials.set_store(storage) |
| 498 storage.put(credentials) | 508 storage.put(credentials) |
| 499 return AccessToken(str(credentials.access_token), credentials.token_expiry) | 509 return AccessToken(str(credentials.access_token), credentials.token_expiry) |
| 500 | 510 |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 553 if credentials: | 563 if credentials: |
| 554 logging.debug('%s info: %r', title, { | 564 logging.debug('%s info: %r', title, { |
| 555 'access_token_expired': credentials.access_token_expired, | 565 'access_token_expired': credentials.access_token_expired, |
| 556 'has_access_token': bool(credentials.access_token), | 566 'has_access_token': bool(credentials.access_token), |
| 557 'invalid': credentials.invalid, | 567 'invalid': credentials.invalid, |
| 558 'utcnow': datetime.datetime.utcnow(), | 568 'utcnow': datetime.datetime.utcnow(), |
| 559 'token_expiry': credentials.token_expiry, | 569 'token_expiry': credentials.token_expiry, |
| 560 }) | 570 }) |
| 561 | 571 |
| 562 | 572 |
| 563 def _run_oauth_dance(config): | 573 def _run_oauth_dance(config, scopes): |
| 564 """Perform full 3-legged OAuth2 flow with the browser. | 574 """Perform full 3-legged OAuth2 flow with the browser. |
| 565 | 575 |
| 566 Returns: | 576 Returns: |
| 567 oauth2client.Credentials. | 577 oauth2client.Credentials. |
| 568 | 578 |
| 569 Raises: | 579 Raises: |
| 570 AuthenticationError on errors. | 580 AuthenticationError on errors. |
| 571 """ | 581 """ |
| 572 flow = client.OAuth2WebServerFlow( | 582 flow = client.OAuth2WebServerFlow( |
| 573 OAUTH_CLIENT_ID, | 583 OAUTH_CLIENT_ID, |
| 574 OAUTH_CLIENT_SECRET, | 584 OAUTH_CLIENT_SECRET, |
| 575 OAUTH_SCOPES, | 585 scopes, |
| 576 approval_prompt='force') | 586 approval_prompt='force') |
| 577 | 587 |
| 578 use_local_webserver = config.use_local_webserver | 588 use_local_webserver = config.use_local_webserver |
| 579 port = config.webserver_port | 589 port = config.webserver_port |
| 580 if config.use_local_webserver: | 590 if config.use_local_webserver: |
| 581 success = False | 591 success = False |
| 582 try: | 592 try: |
| 583 httpd = _ClientRedirectServer(('localhost', port), _ClientRedirectHandler) | 593 httpd = _ClientRedirectServer(('localhost', port), _ClientRedirectHandler) |
| 584 except socket.error: | 594 except socket.error: |
| 585 pass | 595 pass |
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 666 self.end_headers() | 676 self.end_headers() |
| 667 query = self.path.split('?', 1)[-1] | 677 query = self.path.split('?', 1)[-1] |
| 668 query = dict(urlparse.parse_qsl(query)) | 678 query = dict(urlparse.parse_qsl(query)) |
| 669 self.server.query_params = query | 679 self.server.query_params = query |
| 670 self.wfile.write('<html><head><title>Authentication Status</title></head>') | 680 self.wfile.write('<html><head><title>Authentication Status</title></head>') |
| 671 self.wfile.write('<body><p>The authentication flow has completed.</p>') | 681 self.wfile.write('<body><p>The authentication flow has completed.</p>') |
| 672 self.wfile.write('</body></html>') | 682 self.wfile.write('</body></html>') |
| 673 | 683 |
| 674 def log_message(self, _format, *args): | 684 def log_message(self, _format, *args): |
| 675 """Do not log messages to stdout while running as command line program.""" | 685 """Do not log messages to stdout while running as command line program.""" |
| OLD | NEW |