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),