Repository: goauthentik/authentik

Issue:
Misuse of client_credentials and password grant
**Describe the bug**
According to [Section 4.3](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) of RFC6749, the `client_credentials` grant should only take in `client_id` and `client_secret` (if any) as its authentication method and return an access token, however, this is not the case in authentik.
According to [authentik docs](https://goauthentik.io/docs/providers/oauth2/client_credentials), the "`client_credentials`" grant somehow requires both username and password, which is not what `client_credentials` grant was originally designed for in the standards.
On the other hand, there is this `password` grant defined in [Section 4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) of RFC6749, stating it requires both `username` and `password` fields in the request body, which I believe is what authentik is originally targeting for.

**Expected behavior**
`client_credentials` to not take `username` and `password` as a required field, this grant should be named `password` instead.

**Version and Deployment (please complete the following information):**

-   authentik version: 2023.5.4
-   Deployment: docker-compose

**Additional context**
In authentik docs, there's this line that says
> Note that authentik does treat a grant type of `password` the same as `client_credentials` to support applications which rely on a password grant.

However, after testing, it appears that passing in a `grant_type` of `password` makes authentik throw the following exception
```python
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/asgiref/sync.py", line 472, in thread_handler
    raise exc_info[1]
  File "/usr/local/lib/python3.11/site-packages/django/core/handlers/base.py", line 253, in _get_response_async
    response = await wrapped_callback(
               ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/asgiref/sync.py", line 435, in __call__
    ret = await asyncio.wait_for(future, timeout=None)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/asyncio/tasks.py", line 442, in wait_for
    return await fut
           ^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/asgiref/current_thread_executor.py", line 22, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/asgiref/sync.py", line 476, in thread_handler
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/views/generic/base.py", line 103, in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/utils/decorators.py", line 46, in _wrapper
    return bound_method(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/views/decorators/csrf.py", line 55, in wrapped_view
    return view_func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/authentik/providers/oauth2/views/token.py", line 429, in dispatch
    response = super().dispatch(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/views/generic/base.py", line 142, in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/authentik/providers/oauth2/views/token.py", line 467, in post
    raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
builtins.ValueError: Invalid grant_type: password
```
Which is not expected by the documentations


Discussion:
I think some of the reason for this is due to https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow using client_credentials in conjunction with `client_assertion_type`

I'm not sure how we could implement a spec-accurate version of this, since there would need to be some kind of user in authentik to attribute actions to.

The exception should definitely not happen though
> I think some of the reason for this is due to https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow using client_credentials in conjunction with `client_assertion_type`
> 
> I'm not sure how we could implement a spec-accurate version of this, since there would need to be some kind of user in authentik to attribute actions to.
> 
> The exception should definitely not happen though

https://github.com/goauthentik/authentik/blob/0b5870f16e41d9eb5535b495443dd2b0bb65a19c/authentik/providers/oauth2/views/token.py#L456C1-L469C1
According to these lines ^, password grant is not included in the checks, therefore raising an exception
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
I don’t think this issue is fully fixed yet, can @BeryJu reopen it?
I agree and also was kind of confused to see this way of passing credentials (`client_id + username + password`) to the `/token` endpoint to retrieve an `access_token`. Wouldn't this produce quite some issues in existing oidc/oauth2 libraries existing out there?

One quick suggestion (hack?) to make it RFC6749 compliant: concatenate `username` and `password` with a `:`, base64-decode it and use it as `client_secret`. Then you're able to extract username/password on Authentik server side again.
Hey guys, 

Just came across this issue thanks to CommanderStorm's mention.

> I'm not sure how we could implement a spec-accurate version of this, since there would need to be some kind of user in authentik to attribute actions to.

Would a dynamically created service user + role per outpost (if selected for example) be sufficient or would that be overcomplicating matters? 

Keycloak seems to have a similar-ish method to assign the `Client Credentials` grant, which then allows it to work with services such as uptimekuma.

![image](https://github.com/goauthentik/authentik/assets/4349962/3c9d8e61-8aff-44e1-b513-ce14e472a5ca)
Hey guys, 

Apologies for poking, but is there any progress on this one? 

I'm unable to use Authentik with services like UptimeKuma, which means unfortunately I can't migrate things across. 

Cheers!
We're weighing solution with an aim to fix this in the next (March/April) release in a backwards-compatible way.

Resolving patch:
diff --git a/authentik/crypto/builder.py b/authentik/crypto/builder.py
--- a/authentik/crypto/builder.py
+++ b/authentik/crypto/builder.py
@@ -44,7 +44,7 @@ def save(self) -> CertificateKeyPair:
     def generate_private_key(self) -> PrivateKeyTypes:
         """Generate private key"""
         if self._use_ec_private_key:
-            return ec.generate_private_key(curve=ec.SECP256R1)
+            return ec.generate_private_key(curve=ec.SECP256R1())
         return rsa.generate_private_key(
             public_exponent=65537, key_size=4096, backend=default_backend()
         )
diff --git a/authentik/providers/oauth2/views/token.py b/authentik/providers/oauth2/views/token.py
--- a/authentik/providers/oauth2/views/token.py
+++ b/authentik/providers/oauth2/views/token.py
@@ -1,6 +1,7 @@
 """authentik OAuth2 Token views"""
 
-from base64 import urlsafe_b64encode
+from base64 import b64decode, urlsafe_b64encode
+from binascii import Error
 from dataclasses import InitVar, dataclass
 from datetime import datetime
 from hashlib import sha256
@@ -23,10 +24,12 @@
 from authentik.core.models import (
     USER_ATTRIBUTE_EXPIRES,
     USER_ATTRIBUTE_GENERATED,
+    USER_PATH_SYSTEM_PREFIX,
     Application,
     Token,
     TokenIntents,
     User,
+    UserTypes,
 )
 from authentik.events.models import Event, EventAction
 from authentik.events.signals import get_login_event
@@ -286,11 +289,29 @@ def __post_init_refresh(self, raw_token: str, request: HttpRequest):
             raise TokenError("invalid_grant")
 
     def __post_init_client_credentials(self, request: HttpRequest):
+        # client_credentials flow with client assertion
         if request.POST.get(CLIENT_ASSERTION_TYPE, "") != "":
             return self.__post_init_client_credentials_jwt(request)
+        # authentik-custom-ish client credentials flow
+        if request.POST.get("username", "") != "":
+            return self.__post_init_client_credentials_creds(
+                request, request.POST.get("username"), request.POST.get("password")
+            )
+        # Standard method which creates an automatic user
+        if self.client_secret == self.provider.client_secret:
+            return self.__post_init_client_credentials_generated(request)
+        # Standard workaround method which stores username:password
+        # as client_secret
+        try:
+            user, _, password = b64decode(self.client_secret).decode("utf-8").partition(":")
+            return self.__post_init_client_credentials_creds(request, user, password)
+        except (ValueError, Error):
+            raise TokenError("invalid_grant")
+
+    def __post_init_client_credentials_creds(
+        self, request: HttpRequest, username: str, password: str
+    ):
         # Authenticate user based on credentials
-        username = request.POST.get("username")
-        password = request.POST.get("password")
         user = User.objects.filter(username=username).first()
         if not user:
             raise TokenError("invalid_grant")
@@ -316,7 +337,6 @@ def __post_init_client_credentials(self, request: HttpRequest):
                 PLAN_CONTEXT_APPLICATION: app,
             },
         ).from_http(request, user=user)
-        return None
 
     # pylint: disable=too-many-locals
     def __post_init_client_credentials_jwt(self, request: HttpRequest):
@@ -409,6 +429,35 @@ def __post_init_client_credentials_jwt(self, request: HttpRequest):
             },
         ).from_http(request, user=self.user)
 
+    def __post_init_client_credentials_generated(self, request: HttpRequest):
+        # Authorize user access
+        app = Application.objects.filter(provider=self.provider).first()
+        if not app or not app.provider:
+            raise TokenError("invalid_grant")
+        self.user, _ = User.objects.update_or_create(
+            # trim username to ensure the entire username is max 150 chars
+            # (22 chars being the length of the "template")
+            username=f"ak-{self.provider.name[:150-22]}-client_credentials",
+            defaults={
+                "attributes": {
+                    USER_ATTRIBUTE_GENERATED: True,
+                },
+                "last_login": timezone.now(),
+                "name": f"Autogenerated user from application {app.name} (client credentials)",
+                "path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
+                "type": UserTypes.SERVICE_ACCOUNT,
+            },
+        )
+        self.__check_policy_access(app, request)
+
+        Event.new(
+            action=EventAction.LOGIN,
+            **{
+                PLAN_CONTEXT_METHOD: "oauth_client_secret",
+                PLAN_CONTEXT_APPLICATION: app,
+            },
+        ).from_http(request, user=self.user)
+
     def __post_init_device_code(self, request: HttpRequest):
         device_code = request.POST.get("device_code", "")
         code = DeviceToken.objects.filter(device_code=device_code, provider=self.provider).first()
@@ -418,7 +467,6 @@ def __post_init_device_code(self, request: HttpRequest):
 
     def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source: OAuthSource):
         """Create user from JWT"""
-        exp = token.get("exp")
         self.user, created = User.objects.update_or_create(
             username=f"{self.provider.name}-{token.get('sub')}",
             defaults={
@@ -428,8 +476,10 @@ def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source
                 "last_login": timezone.now(),
                 "name": f"Autogenerated user from application {app.name} (client credentials JWT)",
                 "path": source.get_user_path(),
+                "type": UserTypes.SERVICE_ACCOUNT,
             },
         )
+        exp = token.get("exp")
         if created and exp:
             self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp
             self.user.save()


Test patch:
diff --git a/authentik/providers/oauth2/tests/test_token_cc_standard.py b/authentik/providers/oauth2/tests/test_token_cc_standard.py
new file mode 100644
--- /dev/null
+++ b/authentik/providers/oauth2/tests/test_token_cc_standard.py
@@ -0,0 +1,170 @@
+"""Test token view"""
+
+from json import loads
+
+from django.test import RequestFactory
+from django.urls import reverse
+from jwt import decode
+
+from authentik.blueprints.tests import apply_blueprint
+from authentik.core.models import Application, Group, Token, TokenIntents, UserTypes
+from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
+from authentik.policies.models import PolicyBinding
+from authentik.providers.oauth2.constants import (
+    GRANT_TYPE_CLIENT_CREDENTIALS,
+    GRANT_TYPE_PASSWORD,
+    SCOPE_OPENID,
+    SCOPE_OPENID_EMAIL,
+    SCOPE_OPENID_PROFILE,
+    TOKEN_TYPE,
+)
+from authentik.providers.oauth2.errors import TokenError
+from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.tests.utils import OAuthTestCase
+
+
+class TestTokenClientCredentialsStandard(OAuthTestCase):
+    """Test token (client_credentials) view"""
+
+    @apply_blueprint("system/providers-oauth2.yaml")
+    def setUp(self) -> None:
+        super().setUp()
+        self.factory = RequestFactory()
+        self.provider = OAuth2Provider.objects.create(
+            name="test",
+            authorization_flow=create_test_flow(),
+            redirect_uris="http://testserver",
+            signing_key=create_test_cert(),
+        )
+        self.provider.property_mappings.set(ScopeMapping.objects.all())
+        self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
+        self.user = create_test_admin_user("sa")
+        self.user.type = UserTypes.SERVICE_ACCOUNT
+        self.user.save()
+        self.token = Token.objects.create(
+            identifier="sa-token",
+            user=self.user,
+            intent=TokenIntents.INTENT_APP_PASSWORD,
+            expiring=False,
+        )
+
+    def test_wrong_user(self):
+        """test invalid username"""
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+                "scope": SCOPE_OPENID,
+                "client_id": self.provider.client_id,
+                "client_secret": self.provider.client_secret + "foo",
+            },
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertJSONEqual(
+            response.content.decode(),
+            {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
+        )
+
+    def test_no_provider(self):
+        """test no provider"""
+        self.app.provider = None
+        self.app.save()
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+                "scope": SCOPE_OPENID,
+                "client_id": self.provider.client_id,
+                "client_secret": self.provider.client_secret,
+            },
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertJSONEqual(
+            response.content.decode(),
+            {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
+        )
+
+    def test_permission_denied(self):
+        """test permission denied"""
+        group = Group.objects.create(name="foo")
+        PolicyBinding.objects.create(
+            group=group,
+            target=self.app,
+            order=0,
+        )
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+                "scope": SCOPE_OPENID,
+                "client_id": self.provider.client_id,
+                "client_secret": self.provider.client_secret,
+            },
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertJSONEqual(
+            response.content.decode(),
+            {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
+        )
+
+    def test_successful(self):
+        """test successful"""
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+                "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
+                "client_id": self.provider.client_id,
+                "client_secret": self.provider.client_secret,
+            },
+        )
+        self.assertEqual(response.status_code, 200)
+        body = loads(response.content.decode())
+        self.assertEqual(body["token_type"], TOKEN_TYPE)
+        _, alg = self.provider.jwt_key
+        jwt = decode(
+            body["access_token"],
+            key=self.provider.signing_key.public_key,
+            algorithms=[alg],
+            audience=self.provider.client_id,
+        )
+        self.assertEqual(
+            jwt["given_name"], "Autogenerated user from application test (client credentials)"
+        )
+        self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")
+        jwt = decode(
+            body["id_token"],
+            key=self.provider.signing_key.public_key,
+            algorithms=[alg],
+            audience=self.provider.client_id,
+        )
+        self.assertEqual(
+            jwt["given_name"], "Autogenerated user from application test (client credentials)"
+        )
+        self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")
+
+    def test_successful_password(self):
+        """test successful (password grant)"""
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_PASSWORD,
+                "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
+                "client_id": self.provider.client_id,
+                "client_secret": self.provider.client_secret,
+            },
+        )
+        self.assertEqual(response.status_code, 200)
+        body = loads(response.content.decode())
+        self.assertEqual(body["token_type"], TOKEN_TYPE)
+        _, alg = self.provider.jwt_key
+        jwt = decode(
+            body["access_token"],
+            key=self.provider.signing_key.public_key,
+            algorithms=[alg],
+            audience=self.provider.client_id,
+        )
+        self.assertEqual(
+            jwt["given_name"], "Autogenerated user from application test (client credentials)"
+        )
+        self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")
diff --git a/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py b/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py
new file mode 100644
--- /dev/null
+++ b/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py
@@ -0,0 +1,182 @@
+"""Test token view"""
+
+from base64 import b64encode
+from json import loads
+
+from django.test import RequestFactory
+from django.urls import reverse
+from jwt import decode
+
+from authentik.blueprints.tests import apply_blueprint
+from authentik.core.models import Application, Group, Token, TokenIntents, UserTypes
+from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
+from authentik.policies.models import PolicyBinding
+from authentik.providers.oauth2.constants import (
+    GRANT_TYPE_CLIENT_CREDENTIALS,
+    GRANT_TYPE_PASSWORD,
+    SCOPE_OPENID,
+    SCOPE_OPENID_EMAIL,
+    SCOPE_OPENID_PROFILE,
+    TOKEN_TYPE,
+)
+from authentik.providers.oauth2.errors import TokenError
+from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
+from authentik.providers.oauth2.tests.utils import OAuthTestCase
+
+
+class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
+    """Test token (client_credentials) view"""
+
+    @apply_blueprint("system/providers-oauth2.yaml")
+    def setUp(self) -> None:
+        super().setUp()
+        self.factory = RequestFactory()
+        self.provider = OAuth2Provider.objects.create(
+            name="test",
+            authorization_flow=create_test_flow(),
+            redirect_uris="http://testserver",
+            signing_key=create_test_cert(),
+        )
+        self.provider.property_mappings.set(ScopeMapping.objects.all())
+        self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
+        self.user = create_test_admin_user("sa")
+        self.user.type = UserTypes.SERVICE_ACCOUNT
+        self.user.save()
+        self.token = Token.objects.create(
+            identifier="sa-token",
+            user=self.user,
+            intent=TokenIntents.INTENT_APP_PASSWORD,
+            expiring=False,
+        )
+
+    def test_wrong_user(self):
+        """test invalid username"""
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+                "scope": SCOPE_OPENID,
+                "client_id": self.provider.client_id,
+                "client_secret": b64encode(f"saa:{self.token.key}".encode()).decode(),
+            },
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertJSONEqual(
+            response.content.decode(),
+            {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
+        )
+
+    def test_wrong_token(self):
+        """test invalid token"""
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+                "scope": SCOPE_OPENID,
+                "client_id": self.provider.client_id,
+                "client_secret": b64encode(f"sa:{self.token.key}foo".encode()).decode(),
+            },
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertJSONEqual(
+            response.content.decode(),
+            {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
+        )
+
+    def test_no_provider(self):
+        """test no provider"""
+        self.app.provider = None
+        self.app.save()
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+                "scope": SCOPE_OPENID,
+                "client_id": self.provider.client_id,
+                "client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(),
+            },
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertJSONEqual(
+            response.content.decode(),
+            {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
+        )
+
+    def test_permission_denied(self):
+        """test permission denied"""
+        group = Group.objects.create(name="foo")
+        PolicyBinding.objects.create(
+            group=group,
+            target=self.app,
+            order=0,
+        )
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+                "scope": SCOPE_OPENID,
+                "client_id": self.provider.client_id,
+                "client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(),
+            },
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertJSONEqual(
+            response.content.decode(),
+            {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
+        )
+
+    def test_successful(self):
+        """test successful"""
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
+                "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
+                "client_id": self.provider.client_id,
+                "client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(),
+            },
+        )
+        self.assertEqual(response.status_code, 200)
+        body = loads(response.content.decode())
+        self.assertEqual(body["token_type"], TOKEN_TYPE)
+        _, alg = self.provider.jwt_key
+        jwt = decode(
+            body["access_token"],
+            key=self.provider.signing_key.public_key,
+            algorithms=[alg],
+            audience=self.provider.client_id,
+        )
+        self.assertEqual(jwt["given_name"], self.user.name)
+        self.assertEqual(jwt["preferred_username"], self.user.username)
+        jwt = decode(
+            body["id_token"],
+            key=self.provider.signing_key.public_key,
+            algorithms=[alg],
+            audience=self.provider.client_id,
+        )
+        self.assertEqual(jwt["given_name"], self.user.name)
+        self.assertEqual(jwt["preferred_username"], self.user.username)
+
+    def test_successful_password(self):
+        """test successful (password grant)"""
+        response = self.client.post(
+            reverse("authentik_providers_oauth2:token"),
+            {
+                "grant_type": GRANT_TYPE_PASSWORD,
+                "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
+                "client_id": self.provider.client_id,
+                "client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(),
+            },
+        )
+        self.assertEqual(response.status_code, 200)
+        body = loads(response.content.decode())
+        self.assertEqual(body["token_type"], TOKEN_TYPE)
+        _, alg = self.provider.jwt_key
+        jwt = decode(
+            body["access_token"],
+            key=self.provider.signing_key.public_key,
+            algorithms=[alg],
+            audience=self.provider.client_id,
+        )
+        self.assertEqual(jwt["given_name"], self.user.name)
+        self.assertEqual(jwt["preferred_username"], self.user.username)
diff --git a/authentik/providers/oauth2/tests/test_token_cc.py b/authentik/providers/oauth2/tests/test_token_cc_user_pw.py
similarity index 99%
rename from authentik/providers/oauth2/tests/test_token_cc.py
rename to authentik/providers/oauth2/tests/test_token_cc_user_pw.py
--- a/authentik/providers/oauth2/tests/test_token_cc.py
+++ b/authentik/providers/oauth2/tests/test_token_cc_user_pw.py
@@ -23,7 +23,7 @@
 from authentik.providers.oauth2.tests.utils import OAuthTestCase
 
 
-class TestTokenClientCredentials(OAuthTestCase):
+class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
     """Test token (client_credentials) view"""
 
     @apply_blueprint("system/providers-oauth2.yaml")