Mercurial > kallithea
changeset 6384:9cf90371d0f1
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 <api key>
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.)
author | Søren Løvborg <sorenl@unity3d.com> |
---|---|
date | Mon, 02 Jan 2017 18:51:37 +0100 |
parents | 06398585de03 |
children | 1341be63734a |
files | docs/api/api.rst kallithea/lib/base.py kallithea/lib/utils2.py kallithea/tests/functional/test_login.py |
diffstat | 4 files changed, 47 insertions(+), 9 deletions(-) [+] |
line wrap: on
line diff
--- a/docs/api/api.rst Thu Nov 10 20:38:40 2016 +0100 +++ b/docs/api/api.rst Mon Jan 02 18:51:37 2017 +0100 @@ -1019,8 +1019,14 @@ FilesController:raw, FilesController:archivefile -After this change, a Kallithea view can be accessed without login by adding a -GET parameter ``?api_key=<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 <api_key> + +Alternatively, the API key can be passed in the URL query string using +``?api_key=<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.
--- a/kallithea/lib/base.py Thu Nov 10 20:38:40 2016 +0100 +++ b/kallithea/lib/base.py Mon Jan 02 18:51:37 2017 +0100 @@ -365,11 +365,15 @@ 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 @@ 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'), )
--- a/kallithea/lib/utils2.py Thu Nov 10 20:38:40 2016 +0100 +++ b/kallithea/lib/utils2.py Mon Jan 02 18:51:37 2017 +0100 @@ -131,7 +131,14 @@ 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))
--- a/kallithea/tests/functional/test_login.py Thu Nov 10 20:38:40 2016 +0100 +++ b/kallithea/tests/functional/test_login.py Mon Jan 02 18:51:37 2017 +0100 @@ -435,22 +435,31 @@ 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),