Repository: sissbruecker/linkding

Issue:
Accept Authorization: Bearer <Token> as well as Authorization: Token <Token>
Accepting `Authorization: Bearer <Token>` as well as `Authorization: Token <Token>` would make using API testing tools easier, as they generally have presets for Auth type Bearer. See #470 and https://learning.postman.com/docs/sending-requests/authorization/#bearer-token

> I'm not an expert, I believe using Bearer is a standard related to OAuth, and maybe JSON Web Token (JWT).

It would also allow easier use of tools and frameworks setup for that type of Auth. Example: I'm currently working with a modified version of Shiori bookmark app replacing Delicious support with Linkding support.

I guess this is somewhat related to Delicious API standard #207 

## Screenshot of Postman Auth choices
![image](https://github.com/sissbruecker/linkding/assets/49612/8a18664b-ba42-4b59-9457-b1d1db6274ea)

![image](https://github.com/sissbruecker/linkding/assets/49612/374a8784-b531-4804-85a6-33d6942d4cb0)

![image](https://github.com/sissbruecker/linkding/assets/49612/1c6ee6be-3c5d-48ab-b847-7f26d00bb668)



Discussion:
> I'm currently working with a modified version of Shiori bookmark app replacing Delicious support with Linkding support.

My work on this is now complete. Pinboard/Delicious > Linkding API Translator (plus a hacked app to prove concept)

@sissbruecker is there a good place to discuss it? does discussion of this even belong here?


@gingerbeardman Discuss what specifically 🙂? If you have a new project, it sounds like you would want to put it in a repo, and then maybe link to it from the community section.

I did take a quick look at the header issue, and while you can configure Django Rest framework to use a specific keyword, you can not make it use multiple keywords, at least not without overriding stuff.
Yes 😀 I'll put my code in a repo soon. 



Patch:
diff --git a/bookmarks/api/auth.py b/bookmarks/api/auth.py
new file mode 100644
index 00000000..019e9d8d
--- /dev/null
+++ b/bookmarks/api/auth.py
@@ -0,0 +1,34 @@
+from django.utils.translation import gettext_lazy as _
+from rest_framework import exceptions
+from rest_framework.authentication import TokenAuthentication, get_authorization_header
+
+
+class LinkdingTokenAuthentication(TokenAuthentication):
+    """
+    Extends DRF TokenAuthentication to add support for multiple keywords
+    """
+
+    keywords = [keyword.lower().encode() for keyword in ["Token", "Bearer"]]
+
+    def authenticate(self, request):
+        auth = get_authorization_header(request).split()
+
+        if not auth or auth[0].lower() not in self.keywords:
+            return None
+
+        if len(auth) == 1:
+            msg = _("Invalid token header. No credentials provided.")
+            raise exceptions.AuthenticationFailed(msg)
+        elif len(auth) > 2:
+            msg = _("Invalid token header. Token string should not contain spaces.")
+            raise exceptions.AuthenticationFailed(msg)
+
+        try:
+            token = auth[1].decode()
+        except UnicodeError:
+            msg = _(
+                "Invalid token header. Token string should not contain invalid characters."
+            )
+            raise exceptions.AuthenticationFailed(msg)
+
+        return self.authenticate_credentials(token)
diff --git a/siteroot/settings/base.py b/siteroot/settings/base.py
index a9487a9f..8324d85e 100644
--- a/siteroot/settings/base.py
+++ b/siteroot/settings/base.py
@@ -131,7 +131,7 @@
 # REST framework
 REST_FRAMEWORK = {
     "DEFAULT_AUTHENTICATION_CLASSES": [
-        "rest_framework.authentication.TokenAuthentication",
+        "bookmarks.api.auth.LinkdingTokenAuthentication",
         "rest_framework.authentication.SessionAuthentication",
     ],
     "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],


Test patch:
diff --git a/bookmarks/tests/test_auth_api.py b/bookmarks/tests/test_auth_api.py
new file mode 100644
index 00000000..8e1076c0
--- /dev/null
+++ b/bookmarks/tests/test_auth_api.py
@@ -0,0 +1,32 @@
+from django.urls import reverse
+from rest_framework import status
+from rest_framework.authtoken.models import Token
+
+from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
+
+
+class AuthApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
+
+    def authenticate(self, keyword):
+        self.api_token = Token.objects.get_or_create(
+            user=self.get_or_create_test_user()
+        )[0]
+        self.client.credentials(HTTP_AUTHORIZATION=f"{keyword} {self.api_token.key}")
+
+    def test_auth_with_token_keyword(self):
+        self.authenticate("Token")
+
+        url = reverse("bookmarks:user-profile")
+        self.get(url, expected_status_code=status.HTTP_200_OK)
+
+    def test_auth_with_bearer_keyword(self):
+        self.authenticate("Bearer")
+
+        url = reverse("bookmarks:user-profile")
+        self.get(url, expected_status_code=status.HTTP_200_OK)
+
+    def test_auth_with_unknown_keyword(self):
+        self.authenticate("Key")
+
+        url = reverse("bookmarks:user-profile")
+        self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)