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 |