Repository: adfinis/timed-backend

Issue:
bug(auth): requests to the api with an invalid token receive a response status 500 instead of 401



Discussion:

Resolving patch:
diff --git a/timed/authentication.py b/timed/authentication.py
--- a/timed/authentication.py
+++ b/timed/authentication.py
@@ -8,6 +8,7 @@
 from django.core.exceptions import SuspiciousOperation
 from django.utils.encoding import force_bytes
 from mozilla_django_oidc.auth import LOGGER, OIDCAuthenticationBackend
+from rest_framework.exceptions import AuthenticationFailed
 
 
 class TimedOIDCAuthenticationBackend(OIDCAuthenticationBackend):
@@ -37,20 +38,29 @@ def get_userinfo_or_introspection(self, access_token):
             claims = self.cached_request(
                 self.get_userinfo, access_token, "auth.userinfo"
             )
+            return claims
         except requests.HTTPError as e:
-            if not (
-                e.response.status_code in [401, 403] and settings.OIDC_CHECK_INTROSPECT
-            ):
+            if e.response.status_code not in [401, 403]:
                 raise e
-
-            # check introspection if userinfo fails (confidental client)
-            claims = self.cached_request(
-                self.get_introspection, access_token, "auth.introspection"
-            )
-            if "client_id" not in claims:
-                raise SuspiciousOperation("client_id not present in introspection")
-
-        return claims
+            if settings.OIDC_CHECK_INTROSPECT:
+                try:
+                    # check introspection if userinfo fails (confidential client)
+                    claims = self.cached_request(
+                        self.get_introspection, access_token, "auth.introspection"
+                    )
+                    if "client_id" not in claims:
+                        raise SuspiciousOperation(
+                            "client_id not present in introspection"
+                        )
+                    return claims
+                except requests.HTTPError as e:
+                    # if the authorization fails it's not a valid client or
+                    # the token is expired and permission is denied.
+                    # Handing on the 401 Client Error would be transformed into
+                    # a 500 by Django's exception handling. But that's not what we want.
+                    if e.response.status_code not in [401, 403]:  # pragma: no cover
+                        raise e
+            raise AuthenticationFailed()
 
     def get_or_create_user(self, access_token, id_token, payload):
         """Verify claims and return user, otherwise raise an Exception."""


Test patch:
diff --git a/timed/tests/test_authentication.py b/timed/tests/test_authentication.py
--- a/timed/tests/test_authentication.py
+++ b/timed/tests/test_authentication.py
@@ -8,6 +8,7 @@
 from requests.exceptions import HTTPError
 from rest_framework import exceptions, status
 from rest_framework.exceptions import AuthenticationFailed
+from rest_framework.reverse import reverse
 
 from timed.employment.factories import UserFactory
 
@@ -144,3 +145,22 @@ def test_authentication_no_client(db, rf, requests_mock, settings):
     request = rf.get("/openid", HTTP_AUTHORIZATION="Bearer Token")
     with pytest.raises(AuthenticationFailed):
         OIDCAuthentication().authenticate(request)
+
+
+@pytest.mark.parametrize("check_introspect", [True, False])
+def test_userinfo_introspection_failure(
+    db, client, rf, requests_mock, settings, check_introspect
+):
+    settings.OIDC_CHECK_INTROSPECT = check_introspect
+    requests_mock.get(
+        settings.OIDC_OP_USER_ENDPOINT, status_code=status.HTTP_401_UNAUTHORIZED
+    )
+    requests_mock.post(
+        settings.OIDC_OP_INTROSPECT_ENDPOINT, status_code=status.HTTP_403_FORBIDDEN
+    )
+    resp = client.get(reverse("user-me"), HTTP_AUTHORIZATION="Bearer Token")
+    assert resp.status_code == status.HTTP_401_UNAUTHORIZED
+    request = rf.get("/openid", HTTP_AUTHORIZATION="Bearer Token")
+    with pytest.raises(AuthenticationFailed):
+        OIDCAuthentication().authenticate(request)
+    cache.clear()