# HG changeset patch # User Søren Løvborg # Date 2017-01-02 18:51:37 # Node ID 9cf90371d0f107664ac324eb2ea28063e6172695 # Parent 06398585de039e3767d19bf51c3de5e69b762dd3 auth: add support for "Bearer" auth scheme (API key variant) This allows the API key to be passed in a header instead of the query string, reducing the risk of accidental API key leaks: Authorization: Bearer The Bearer authorization scheme is standardized in RFC 6750, though used here outside the full OAuth 2.0 authorization framework. (Full OAuth can still be added later without breaking existing users.) diff --git a/docs/api/api.rst b/docs/api/api.rst --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -1019,8 +1019,14 @@ For example, to enable API access to pat FilesController:raw, FilesController:archivefile -After this change, a Kallithea view can be accessed without login by adding a -GET parameter ``?api_key=`` to the URL. +After this change, a Kallithea view can be accessed without login using +bearer authentication, by including this header with the request:: + + Authentication: Bearer + +Alternatively, the API key can be passed in the URL query string using +``?api_key=``, though this is not recommended due to the increased +risk of API key leaks, and support will likely be removed in the future. Exposing raw diffs is a good way to integrate with third-party services like code review, or build farms that can download archives. diff --git a/kallithea/lib/base.py b/kallithea/lib/base.py --- a/kallithea/lib/base.py +++ b/kallithea/lib/base.py @@ -365,11 +365,15 @@ class BaseController(WSGIController): self.scm_model = ScmModel(self.sa) @staticmethod - def _determine_auth_user(api_key, session_authuser): + def _determine_auth_user(api_key, bearer_token, session_authuser): + """ + Create an `AuthUser` object given the API key/bearer token + (if any) and the value of the authuser session cookie. """ - Create an `AuthUser` object given the API key (if any) and the - value of the authuser session cookie. - """ + + # Authenticate by bearer token + if bearer_token is not None: + api_key = bearer_token # Authenticate by API key if api_key is not None: @@ -459,8 +463,20 @@ class BaseController(WSGIController): self._basic_security_checks() #set globals for auth user + + bearer_token = None + try: + # Request.authorization may raise ValueError on invalid input + type, params = request.authorization + except (ValueError, TypeError): + pass + else: + if type.lower() == 'bearer': + bearer_token = params + self.authuser = c.authuser = request.user = self._determine_auth_user( request.GET.get('api_key'), + bearer_token, session.get('authuser'), ) diff --git a/kallithea/lib/utils2.py b/kallithea/lib/utils2.py --- a/kallithea/lib/utils2.py +++ b/kallithea/lib/utils2.py @@ -131,7 +131,14 @@ def detect_mode(line, default): def generate_api_key(): """ Generates a random (presumably unique) API key. + + This value is used in URLs and "Bearer" HTTP Authorization headers, + which in practice means it should only contain URL-safe characters + (RFC 3986): + + unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" """ + # Hexadecimal certainly qualifies as URL-safe. return binascii.hexlify(os.urandom(20)) diff --git a/kallithea/tests/functional/test_login.py b/kallithea/tests/functional/test_login.py --- a/kallithea/tests/functional/test_login.py +++ b/kallithea/tests/functional/test_login.py @@ -435,22 +435,31 @@ class TestLoginController(TestController def _api_key_test(self, api_key, status): """Verifies HTTP status code for accessing an auth-requiring page, - using the given api_key URL parameter. If api_key is None, no api_key - parameter is passed at all. If api_key is True, a real, working API key - is used. + using the given api_key URL parameter as well as using the API key + with bearer authentication. + + If api_key is None, no api_key is passed at all. If api_key is True, + a real, working API key is used. """ with fixture.anon_access(False): if api_key is None: params = {} + headers = {} else: if api_key is True: api_key = User.get_first_admin().api_key params = {'api_key': api_key} + headers = {'Authorization': 'Bearer ' + str(api_key)} self.app.get(url(controller='changeset', action='changeset_raw', repo_name=HG_REPO, revision='tip', **params), status=status) + self.app.get(url(controller='changeset', action='changeset_raw', + repo_name=HG_REPO, revision='tip'), + headers=headers, + status=status) + @parametrize('test_name,api_key,code', [ ('none', None, 302), ('empty_string', '', 403),