Repository: goauthentik/authentik

Issue:
Not sure that OAuth2 client source should use authorization header
I've been testing authentik using an Auth0 openIDC source as well as a google source. I have gotten both to work, but Auth0 was not working before a fix to prevent adding the access token to the authorizaton Bearer headers. Google auth works fine with or without this fix. 

https://auth0.com/blog/id-token-access-token-what-is-the-difference/ suggests that many endpoints should not be given the access token. Not sure this is relevant.

I think Auth0 is less permissive and prefers the access_token param rather than the Authorization Bearer token

in sources/oauth/clients/oauth2.py
```
class OAuth2Client(BaseOAuthClient):
    """OAuth2 Client"""

   ...

    def do_request(self, method: str, url: str, **kwargs) -> Response:
        """Build remote url request. Constructs necessary auth."""
        if "token" in kwargs:
            token = kwargs.pop("token")

            params = kwargs.get("params", {})
            params["access_token"] = token["access_token"]
            kwargs["params"] = params

            headers = kwargs.get("headers", {})
            # Note this fix
            # headers["Authorization"] = f"{token['token_type']} {token['access_token']}"
            kwargs["headers"] = headers
        return super().do_request(method, url, **kwargs)
```




Discussion:
It seems authentik sets both the `access_token` query parameter and the `Authorization` header, which makes my oauth2 provider (https://wordpress.org/plugins/oauth2-provider/) return 400 invalid request error. With just one or the other set it works. So this should be configurable or at least follow the spec.
We do indeed send both by default for better compatibility, there is https://github.com/goauthentik/authentik/blob/main/authentik/sources/oauth/clients/oauth2.py#L138 which sends only the header (which is better in theory as its much harder to accidentally log), and that client is used for OIDC and a couple other providers.

OIDC actually specifies to use the Authorization header only https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest
it would be totally fine if it only sent the authorization header but it is sending both to the profile url with an OAuth Source with provider type "Generic OpenID Connect".
```diff
diff --git a/authentik/sources/oauth/types/oidc.py b/authentik/sources/oauth/types/oidc.py
index 189209c43..7ebd24579 100644
--- a/authentik/sources/oauth/types/oidc.py
+++ b/authentik/sources/oauth/types/oidc.py
@@ -20,7 +20,7 @@ class OpenIDConnectOAuthRedirect(OAuthRedirect):
 class OpenIDConnectOAuth2Callback(OAuthCallback):
     """OpenIDConnect OAuth2 Callback"""

-    client_class: UserprofileHeaderAuthClient
+    client_class = UserprofileHeaderAuthClient

     def get_user_id(self, info: dict[str, str]) -> str:
         return info.get("sub", "")
```

d'oh after looking at it again, yeah, I can see why not lol

Resolving patch:
diff --git a/authentik/sources/oauth/types/oidc.py b/authentik/sources/oauth/types/oidc.py
--- a/authentik/sources/oauth/types/oidc.py
+++ b/authentik/sources/oauth/types/oidc.py
@@ -20,7 +20,7 @@ def get_additional_parameters(self, source: OAuthSource):  # pragma: no cover
 class OpenIDConnectOAuth2Callback(OAuthCallback):
     """OpenIDConnect OAuth2 Callback"""
 
-    client_class: UserprofileHeaderAuthClient
+    client_class = UserprofileHeaderAuthClient
 
     def get_user_id(self, info: dict[str, str]) -> str:
         return info.get("sub", "")


Test patch:
diff --git a/authentik/sources/oauth/tests/test_type_openid.py b/authentik/sources/oauth/tests/test_type_openid.py
--- a/authentik/sources/oauth/tests/test_type_openid.py
+++ b/authentik/sources/oauth/tests/test_type_openid.py
@@ -1,6 +1,8 @@
 """OpenID Type tests"""
-from django.test import TestCase
+from django.test import RequestFactory, TestCase
+from requests_mock import Mocker
 
+from authentik.lib.generators import generate_id
 from authentik.sources.oauth.models import OAuthSource
 from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
 
@@ -24,9 +26,10 @@ def setUp(self):
             slug="test",
             provider_type="openidconnect",
             authorization_url="",
-            profile_url="",
+            profile_url="http://localhost/userinfo",
             consumer_key="",
         )
+        self.factory = RequestFactory()
 
     def test_enroll_context(self):
         """Test OpenID Enrollment context"""
@@ -34,3 +37,19 @@ def test_enroll_context(self):
         self.assertEqual(ak_context["username"], OPENID_USER["nickname"])
         self.assertEqual(ak_context["email"], OPENID_USER["email"])
         self.assertEqual(ak_context["name"], OPENID_USER["name"])
+
+    @Mocker()
+    def test_userinfo(self, mock: Mocker):
+        """Test userinfo API call"""
+        mock.get("http://localhost/userinfo", json=OPENID_USER)
+        token = generate_id()
+        OpenIDConnectOAuth2Callback(request=self.factory.get("/")).get_client(
+            self.source
+        ).get_profile_info(
+            {
+                "token_type": "foo",
+                "access_token": token,
+            }
+        )
+        self.assertEqual(mock.last_request.query, "")
+        self.assertEqual(mock.last_request.headers["Authorization"], f"foo {token}")