auth: Use luci-auth to get credentials.
Bug: 1001756
Change-Id: Ieab5391662e92ec9e2715a81fce2cef41717c2e3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1790607
Commit-Queue: Edward Lesmes <ehmaldonado@chromium.org>
Reviewed-by: Andrii Shyshkalov <tandrii@google.com>
diff --git a/auth.py b/auth.py
index 7baa1d0..0da997c 100644
--- a/auth.py
+++ b/auth.py
@@ -6,26 +6,22 @@
from __future__ import print_function
-import BaseHTTPServer
import collections
import datetime
import functools
-import hashlib
import json
import logging
import optparse
import os
-import socket
import sys
import threading
-import time
import urllib
import urlparse
-import webbrowser
+
+import subprocess2
from third_party import httplib2
from third_party.oauth2client import client
-from third_party.oauth2client import multistore_file
# depot_tools/.
@@ -105,11 +101,10 @@
class LoginRequiredError(AuthenticationError):
"""Interaction with the user is required to authenticate."""
- def __init__(self, token_cache_key):
- # HACK(vadimsh): It is assumed here that the token cache key is a hostname.
+ def __init__(self, scopes=OAUTH_SCOPE_EMAIL):
msg = (
'You are not logged in. Please login first by running:\n'
- ' depot-tools-auth login %s' % token_cache_key)
+ ' luci-auth login -scopes %s' % scopes)
super(LoginRequiredError, self).__init__(msg)
@@ -454,15 +449,9 @@
"""
with self._lock:
self._access_token = None
- storage = self._get_storage()
- credentials = storage.get()
- had_creds = bool(credentials)
- if credentials and credentials.refresh_token and credentials.revoke_uri:
- try:
- credentials.revoke(httplib2.Http())
- except client.TokenRevokeError as e:
- logging.warning('Failed to revoke refresh token: %s', e)
- storage.delete()
+ had_creds = bool(_get_luci_auth_credentials(self._scopes))
+ subprocess2.check_call(
+ ['luci-auth', 'logout', '-scopes', self._scopes])
return had_creds
def has_cached_credentials(self):
@@ -587,23 +576,9 @@
## Private methods.
- def _get_storage(self):
- """Returns oauth2client.Storage with cached tokens."""
- # Do not mix cache keys for different externally provided tokens.
- if self._external_token:
- token_hash = hashlib.sha1(self._external_token.refresh_token).hexdigest()
- cache_key = '%s:refresh_tok:%s' % (self._token_cache_key, token_hash)
- else:
- cache_key = self._token_cache_key
- path = _get_token_cache_path()
- logging.debug('Using token storage %r (cache key %r)', path, cache_key)
- return multistore_file.get_credential_storage_custom_string_key(
- path, cache_key)
-
def _get_cached_credentials(self):
- """Returns oauth2client.Credentials loaded from storage."""
- storage = self._get_storage()
- credentials = storage.get()
+ """Returns oauth2client.Credentials loaded from luci-auth."""
+ credentials = _get_luci_auth_credentials(self._scopes)
if not credentials:
logging.debug('No cached token')
@@ -636,8 +611,6 @@
token_uri='https://accounts.google.com/o/oauth2/token',
user_agent=None,
revoke_uri=None)
- credentials.set_store(storage)
- storage.put(credentials)
return credentials
# Not using external refresh token -> return whatever is cached.
@@ -697,17 +670,14 @@
'Token provided via --auth-refresh-token-json is no longer valid.')
if not allow_user_interaction:
logging.debug('Requesting user to login')
- raise LoginRequiredError(self._token_cache_key)
+ raise LoginRequiredError(self._scopes)
logging.debug('Launching OAuth browser flow')
- credentials = _run_oauth_dance(self._config, self._scopes)
+ credentials = _run_oauth_dance(self._scopes)
_log_credentials_info('new token', credentials)
logging.info(
'OAuth access_token refreshed. Expires in %s.',
credentials.token_expiry - datetime.datetime.utcnow())
- storage = self._get_storage()
- credentials.set_store(storage)
- storage.put(credentials)
return AccessToken(str(credentials.access_token), credentials.token_expiry)
@@ -762,116 +732,29 @@
})
-def _run_oauth_dance(config, scopes):
+def _get_luci_auth_credentials(scopes):
+ try:
+ token_info = json.loads(subprocess2.check_output(
+ ['luci-auth', 'token', '-scopes', scopes, '-json-output', '-'],
+ stderr=subprocess2.VOID))
+ except subprocess2.CalledProcessError:
+ return None
+
+ return client.OAuth2Credentials(
+ access_token=token_info['token'],
+ client_id=OAUTH_CLIENT_ID,
+ client_secret=OAUTH_CLIENT_SECRET,
+ refresh_token=None,
+ token_expiry=datetime.datetime.utcfromtimestamp(token_info['expiry']),
+ token_uri=None,
+ user_agent=None,
+ revoke_uri=None)
+
+def _run_oauth_dance(scopes):
"""Perform full 3-legged OAuth2 flow with the browser.
Returns:
oauth2client.Credentials.
-
- Raises:
- AuthenticationError on errors.
"""
- flow = client.OAuth2WebServerFlow(
- OAUTH_CLIENT_ID,
- OAUTH_CLIENT_SECRET,
- scopes,
- approval_prompt='force')
-
- use_local_webserver = config.use_local_webserver
- port = config.webserver_port
- if config.use_local_webserver:
- success = False
- try:
- httpd = _ClientRedirectServer(('localhost', port), _ClientRedirectHandler)
- except socket.error:
- pass
- else:
- success = True
- use_local_webserver = success
- if not success:
- print(
- 'Failed to start a local webserver listening on port %d.\n'
- 'Please check your firewall settings and locally running programs that '
- 'may be blocking or using those ports.\n\n'
- 'Falling back to --auth-no-local-webserver and continuing with '
- 'authentication.\n' % port)
-
- if use_local_webserver:
- oauth_callback = 'http://localhost:%s/' % port
- else:
- oauth_callback = client.OOB_CALLBACK_URN
- flow.redirect_uri = oauth_callback
- authorize_url = flow.step1_get_authorize_url()
-
- if use_local_webserver:
- webbrowser.open(authorize_url, new=1, autoraise=True)
- print(
- 'Your browser has been opened to visit:\n\n'
- ' %s\n\n'
- 'If your browser is on a different machine then exit and re-run this '
- 'application with the command-line parameter\n\n'
- ' --auth-no-local-webserver\n' % authorize_url)
- else:
- print(
- 'Go to the following link in your browser:\n\n'
- ' %s\n' % authorize_url)
-
- try:
- code = None
- if use_local_webserver:
- httpd.handle_request()
- if 'error' in httpd.query_params:
- raise AuthenticationError(
- 'Authentication request was rejected: %s' %
- httpd.query_params['error'])
- if 'code' not in httpd.query_params:
- raise AuthenticationError(
- 'Failed to find "code" in the query parameters of the redirect.\n'
- 'Try running with --auth-no-local-webserver.')
- code = httpd.query_params['code']
- else:
- code = raw_input('Enter verification code: ').strip()
- except KeyboardInterrupt:
- raise AuthenticationError('Authentication was canceled.')
-
- try:
- return flow.step2_exchange(code)
- except client.FlowExchangeError as e:
- raise AuthenticationError('Authentication has failed: %s' % e)
-
-
-class _ClientRedirectServer(BaseHTTPServer.HTTPServer):
- """A server to handle OAuth 2.0 redirects back to localhost.
-
- Waits for a single request and parses the query parameters
- into query_params and then stops serving.
- """
- query_params = {}
-
-
-class _ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
- """A handler for OAuth 2.0 redirects back to localhost.
-
- Waits for a single request and parses the query parameters
- into the servers query_params and then stops serving.
- """
-
- def do_GET(self):
- """Handle a GET request.
-
- Parses the query parameters and prints a message
- if the flow has completed. Note that we can't detect
- if an error occurred.
- """
- self.send_response(200)
- self.send_header('Content-type', 'text/html')
- self.end_headers()
- query = self.path.split('?', 1)[-1]
- query = dict(urlparse.parse_qsl(query))
- self.server.query_params = query
- self.wfile.write('<html><head><title>Authentication Status</title></head>')
- self.wfile.write('<body><p>The authentication flow has completed.</p>')
- self.wfile.write('</body></html>')
-
- def log_message(self, _format, *args):
- """Do not log messages to stdout while running as command line program."""
+ subprocess2.check_call(['luci-auth', 'login', '-scopes', scopes])
+ return _get_luci_auth_credentials(scopes)