We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Allow setting a custom attribute for oidc provider sub claim
**Is your feature request related to a problem? Please describe.**
I have an external auth source and I'm using authentik as an authentication hub between the source and other applications. That auth source has unique user ids that I save in authentik as a custom attribute. I would like to use it as the oidc subject.

**Describe the solution you'd like**
Add a subject mode option "Based on a user attribute" with a text field where one enter the attribute. Alternatively it could be an expression similar to property mappings.

This would be quite similar to the current "Based on the User's UPN" and it may even make sense to replace it entirely, but that would require migrating existing configurations to the new type with upn as the attribute.

**Describe alternatives you've considered**
I could set the external uid as the username in authentik as I'm not currently using the username for anything

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `authentik/providers/oauth2/views/userinfo.py`
Content:
```
1 """authentik OAuth2 OpenID Userinfo views"""
2 
3 from typing import Any
4 
5 from deepmerge import always_merger
6 from django.http import HttpRequest, HttpResponse
7 from django.http.response import HttpResponseBadRequest
8 from django.utils.decorators import method_decorator
9 from django.utils.translation import gettext_lazy as _
10 from django.views import View
11 from django.views.decorators.csrf import csrf_exempt
12 from structlog.stdlib import get_logger
13 
14 from authentik.core.exceptions import PropertyMappingExpressionException
15 from authentik.events.models import Event, EventAction
16 from authentik.flows.challenge import PermissionDict
17 from authentik.providers.oauth2.constants import (
18     SCOPE_AUTHENTIK_API,
19     SCOPE_GITHUB_ORG_READ,
20     SCOPE_GITHUB_USER,
21     SCOPE_GITHUB_USER_EMAIL,
22     SCOPE_GITHUB_USER_READ,
23     SCOPE_OPENID,
24 )
25 from authentik.providers.oauth2.models import (
26     BaseGrantModel,
27     OAuth2Provider,
28     RefreshToken,
29     ScopeMapping,
30 )
31 from authentik.providers.oauth2.utils import TokenResponse, cors_allow, protected_resource_view
32 
33 LOGGER = get_logger()
34 
35 
36 @method_decorator(csrf_exempt, name="dispatch")
37 @method_decorator(protected_resource_view([SCOPE_OPENID]), name="dispatch")
38 class UserInfoView(View):
39     """Create a dictionary with all the requested claims about the End-User.
40     See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse"""
41 
42     token: RefreshToken | None
43 
44     def get_scope_descriptions(
45         self, scopes: list[str], provider: OAuth2Provider
46     ) -> list[PermissionDict]:
47         """Get a list of all Scopes's descriptions"""
48         scope_descriptions = []
49         for scope in ScopeMapping.objects.filter(scope_name__in=scopes, provider=provider).order_by(
50             "scope_name"
51         ):
52             scope_descriptions.append(PermissionDict(id=scope.scope_name, name=scope.description))
53         # GitHub Compatibility Scopes are handled differently, since they required custom paths
54         # Hence they don't exist as Scope objects
55         special_scope_map = {
56             SCOPE_GITHUB_USER: _("GitHub Compatibility: Access your User Information"),
57             SCOPE_GITHUB_USER_READ: _("GitHub Compatibility: Access your User Information"),
58             SCOPE_GITHUB_USER_EMAIL: _("GitHub Compatibility: Access you Email addresses"),
59             SCOPE_GITHUB_ORG_READ: _("GitHub Compatibility: Access your Groups"),
60             SCOPE_AUTHENTIK_API: _("authentik API Access on behalf of your user"),
61         }
62         for scope in scopes:
63             if scope in special_scope_map:
64                 scope_descriptions.append(
65                     PermissionDict(id=scope, name=str(special_scope_map[scope]))
66                 )
67         return scope_descriptions
68 
69     def get_claims(self, provider: OAuth2Provider, token: BaseGrantModel) -> dict[str, Any]:
70         """Get a dictionary of claims from scopes that the token
71         requires and are assigned to the provider."""
72 
73         scopes_from_client = token.scope
74         final_claims = {}
75         for scope in ScopeMapping.objects.filter(
76             provider=provider, scope_name__in=scopes_from_client
77         ).order_by("scope_name"):
78             scope: ScopeMapping
79             value = None
80             try:
81                 value = scope.evaluate(
82                     user=token.user,
83                     request=self.request,
84                     provider=provider,
85                     token=token,
86                 )
87             except PropertyMappingExpressionException as exc:
88                 Event.new(
89                     EventAction.CONFIGURATION_ERROR,
90                     message=f"Failed to evaluate property-mapping: '{scope.name}'",
91                     provider=provider,
92                     mapping=scope,
93                 ).from_http(self.request)
94                 LOGGER.warning("Failed to evaluate property mapping", exc=exc)
95             if value is None:
96                 continue
97             if not isinstance(value, dict):
98                 LOGGER.warning(
99                     "Scope returned a non-dict value, ignoring",
100                     scope=scope,
101                     value=value,
102                 )
103                 continue
104             LOGGER.debug("updated scope", scope=scope)
105             always_merger.merge(final_claims, value)
106         return final_claims
107 
108     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
109         self.token = kwargs.get("token", None)
110         response = super().dispatch(request, *args, **kwargs)
111         allowed_origins = []
112         if self.token:
113             allowed_origins = self.token.provider.redirect_uris.split("\n")
114         cors_allow(self.request, response, *allowed_origins)
115         return response
116 
117     def options(self, request: HttpRequest) -> HttpResponse:
118         return TokenResponse({})
119 
120     def get(self, request: HttpRequest, **kwargs) -> HttpResponse:
121         """Handle GET Requests for UserInfo"""
122         if not self.token:
123             return HttpResponseBadRequest()
124         claims = self.get_claims(self.token.provider, self.token)
125         claims["sub"] = self.token.id_token.sub
126         if self.token.id_token.nonce:
127             claims["nonce"] = self.token.id_token.nonce
128         response = TokenResponse(claims)
129         return response
130 
131     def post(self, request: HttpRequest, **kwargs) -> HttpResponse:
132         """POST Requests behave the same as GET Requests, so the get handler is called here"""
133         return self.get(request, **kwargs)
134
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/authentik/providers/oauth2/views/userinfo.py b/authentik/providers/oauth2/views/userinfo.py
--- a/authentik/providers/oauth2/views/userinfo.py
+++ b/authentik/providers/oauth2/views/userinfo.py
@@ -101,8 +101,8 @@
                     value=value,
                 )
                 continue
-            LOGGER.debug("updated scope", scope=scope)
             always_merger.merge(final_claims, value)
+            LOGGER.debug("updated scope", scope=scope)
         return final_claims
 
     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
@@ -121,8 +121,9 @@
         """Handle GET Requests for UserInfo"""
         if not self.token:
             return HttpResponseBadRequest()
-        claims = self.get_claims(self.token.provider, self.token)
-        claims["sub"] = self.token.id_token.sub
+        claims = {}
+        claims.setdefault("sub", self.token.id_token.sub)
+        claims.update(self.get_claims(self.token.provider, self.token))
         if self.token.id_token.nonce:
             claims["nonce"] = self.token.id_token.nonce
         response = TokenResponse(claims)

{"golden_diff": "diff --git a/authentik/providers/oauth2/views/userinfo.py b/authentik/providers/oauth2/views/userinfo.py\n--- a/authentik/providers/oauth2/views/userinfo.py\n+++ b/authentik/providers/oauth2/views/userinfo.py\n@@ -101,8 +101,8 @@\n                     value=value,\n                 )\n                 continue\n-            LOGGER.debug(\"updated scope\", scope=scope)\n             always_merger.merge(final_claims, value)\n+            LOGGER.debug(\"updated scope\", scope=scope)\n         return final_claims\n \n     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:\n@@ -121,8 +121,9 @@\n         \"\"\"Handle GET Requests for UserInfo\"\"\"\n         if not self.token:\n             return HttpResponseBadRequest()\n-        claims = self.get_claims(self.token.provider, self.token)\n-        claims[\"sub\"] = self.token.id_token.sub\n+        claims = {}\n+        claims.setdefault(\"sub\", self.token.id_token.sub)\n+        claims.update(self.get_claims(self.token.provider, self.token))\n         if self.token.id_token.nonce:\n             claims[\"nonce\"] = self.token.id_token.nonce\n         response = TokenResponse(claims)\n", "issue": "Allow setting a custom attribute for oidc provider sub claim\n**Is your feature request related to a problem? Please describe.**\r\nI have an external auth source and I'm using authentik as an authentication hub between the source and other applications. That auth source has unique user ids that I save in authentik as a custom attribute. I would like to use it as the oidc subject.\r\n\r\n**Describe the solution you'd like**\r\nAdd a subject mode option \"Based on a user attribute\" with a text field where one enter the attribute. Alternatively it could be an expression similar to property mappings.\r\n\r\nThis would be quite similar to the current \"Based on the User's UPN\" and it may even make sense to replace it entirely, but that would require migrating existing configurations to the new type with upn as the attribute.\r\n\r\n**Describe alternatives you've considered**\r\nI could set the external uid as the username in authentik as I'm not currently using the username for anything\n", "before_files": [{"content": "\"\"\"authentik OAuth2 OpenID Userinfo views\"\"\"\n\nfrom typing import Any\n\nfrom deepmerge import always_merger\nfrom django.http import HttpRequest, HttpResponse\nfrom django.http.response import HttpResponseBadRequest\nfrom django.utils.decorators import method_decorator\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views import View\nfrom django.views.decorators.csrf import csrf_exempt\nfrom structlog.stdlib import get_logger\n\nfrom authentik.core.exceptions import PropertyMappingExpressionException\nfrom authentik.events.models import Event, EventAction\nfrom authentik.flows.challenge import PermissionDict\nfrom authentik.providers.oauth2.constants import (\n    SCOPE_AUTHENTIK_API,\n    SCOPE_GITHUB_ORG_READ,\n    SCOPE_GITHUB_USER,\n    SCOPE_GITHUB_USER_EMAIL,\n    SCOPE_GITHUB_USER_READ,\n    SCOPE_OPENID,\n)\nfrom authentik.providers.oauth2.models import (\n    BaseGrantModel,\n    OAuth2Provider,\n    RefreshToken,\n    ScopeMapping,\n)\nfrom authentik.providers.oauth2.utils import TokenResponse, cors_allow, protected_resource_view\n\nLOGGER = get_logger()\n\n\n@method_decorator(csrf_exempt, name=\"dispatch\")\n@method_decorator(protected_resource_view([SCOPE_OPENID]), name=\"dispatch\")\nclass UserInfoView(View):\n    \"\"\"Create a dictionary with all the requested claims about the End-User.\n    See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse\"\"\"\n\n    token: RefreshToken | None\n\n    def get_scope_descriptions(\n        self, scopes: list[str], provider: OAuth2Provider\n    ) -> list[PermissionDict]:\n        \"\"\"Get a list of all Scopes's descriptions\"\"\"\n        scope_descriptions = []\n        for scope in ScopeMapping.objects.filter(scope_name__in=scopes, provider=provider).order_by(\n            \"scope_name\"\n        ):\n            scope_descriptions.append(PermissionDict(id=scope.scope_name, name=scope.description))\n        # GitHub Compatibility Scopes are handled differently, since they required custom paths\n        # Hence they don't exist as Scope objects\n        special_scope_map = {\n            SCOPE_GITHUB_USER: _(\"GitHub Compatibility: Access your User Information\"),\n            SCOPE_GITHUB_USER_READ: _(\"GitHub Compatibility: Access your User Information\"),\n            SCOPE_GITHUB_USER_EMAIL: _(\"GitHub Compatibility: Access you Email addresses\"),\n            SCOPE_GITHUB_ORG_READ: _(\"GitHub Compatibility: Access your Groups\"),\n            SCOPE_AUTHENTIK_API: _(\"authentik API Access on behalf of your user\"),\n        }\n        for scope in scopes:\n            if scope in special_scope_map:\n                scope_descriptions.append(\n                    PermissionDict(id=scope, name=str(special_scope_map[scope]))\n                )\n        return scope_descriptions\n\n    def get_claims(self, provider: OAuth2Provider, token: BaseGrantModel) -> dict[str, Any]:\n        \"\"\"Get a dictionary of claims from scopes that the token\n        requires and are assigned to the provider.\"\"\"\n\n        scopes_from_client = token.scope\n        final_claims = {}\n        for scope in ScopeMapping.objects.filter(\n            provider=provider, scope_name__in=scopes_from_client\n        ).order_by(\"scope_name\"):\n            scope: ScopeMapping\n            value = None\n            try:\n                value = scope.evaluate(\n                    user=token.user,\n                    request=self.request,\n                    provider=provider,\n                    token=token,\n                )\n            except PropertyMappingExpressionException as exc:\n                Event.new(\n                    EventAction.CONFIGURATION_ERROR,\n                    message=f\"Failed to evaluate property-mapping: '{scope.name}'\",\n                    provider=provider,\n                    mapping=scope,\n                ).from_http(self.request)\n                LOGGER.warning(\"Failed to evaluate property mapping\", exc=exc)\n            if value is None:\n                continue\n            if not isinstance(value, dict):\n                LOGGER.warning(\n                    \"Scope returned a non-dict value, ignoring\",\n                    scope=scope,\n                    value=value,\n                )\n                continue\n            LOGGER.debug(\"updated scope\", scope=scope)\n            always_merger.merge(final_claims, value)\n        return final_claims\n\n    def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:\n        self.token = kwargs.get(\"token\", None)\n        response = super().dispatch(request, *args, **kwargs)\n        allowed_origins = []\n        if self.token:\n            allowed_origins = self.token.provider.redirect_uris.split(\"\\n\")\n        cors_allow(self.request, response, *allowed_origins)\n        return response\n\n    def options(self, request: HttpRequest) -> HttpResponse:\n        return TokenResponse({})\n\n    def get(self, request: HttpRequest, **kwargs) -> HttpResponse:\n        \"\"\"Handle GET Requests for UserInfo\"\"\"\n        if not self.token:\n            return HttpResponseBadRequest()\n        claims = self.get_claims(self.token.provider, self.token)\n        claims[\"sub\"] = self.token.id_token.sub\n        if self.token.id_token.nonce:\n            claims[\"nonce\"] = self.token.id_token.nonce\n        response = TokenResponse(claims)\n        return response\n\n    def post(self, request: HttpRequest, **kwargs) -> HttpResponse:\n        \"\"\"POST Requests behave the same as GET Requests, so the get handler is called here\"\"\"\n        return self.get(request, **kwargs)\n", "path": "authentik/providers/oauth2/views/userinfo.py"}], "after_files": [{"content": "\"\"\"authentik OAuth2 OpenID Userinfo views\"\"\"\n\nfrom typing import Any\n\nfrom deepmerge import always_merger\nfrom django.http import HttpRequest, HttpResponse\nfrom django.http.response import HttpResponseBadRequest\nfrom django.utils.decorators import method_decorator\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views import View\nfrom django.views.decorators.csrf import csrf_exempt\nfrom structlog.stdlib import get_logger\n\nfrom authentik.core.exceptions import PropertyMappingExpressionException\nfrom authentik.events.models import Event, EventAction\nfrom authentik.flows.challenge import PermissionDict\nfrom authentik.providers.oauth2.constants import (\n    SCOPE_AUTHENTIK_API,\n    SCOPE_GITHUB_ORG_READ,\n    SCOPE_GITHUB_USER,\n    SCOPE_GITHUB_USER_EMAIL,\n    SCOPE_GITHUB_USER_READ,\n    SCOPE_OPENID,\n)\nfrom authentik.providers.oauth2.models import (\n    BaseGrantModel,\n    OAuth2Provider,\n    RefreshToken,\n    ScopeMapping,\n)\nfrom authentik.providers.oauth2.utils import TokenResponse, cors_allow, protected_resource_view\n\nLOGGER = get_logger()\n\n\n@method_decorator(csrf_exempt, name=\"dispatch\")\n@method_decorator(protected_resource_view([SCOPE_OPENID]), name=\"dispatch\")\nclass UserInfoView(View):\n    \"\"\"Create a dictionary with all the requested claims about the End-User.\n    See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse\"\"\"\n\n    token: RefreshToken | None\n\n    def get_scope_descriptions(\n        self, scopes: list[str], provider: OAuth2Provider\n    ) -> list[PermissionDict]:\n        \"\"\"Get a list of all Scopes's descriptions\"\"\"\n        scope_descriptions = []\n        for scope in ScopeMapping.objects.filter(scope_name__in=scopes, provider=provider).order_by(\n            \"scope_name\"\n        ):\n            scope_descriptions.append(PermissionDict(id=scope.scope_name, name=scope.description))\n        # GitHub Compatibility Scopes are handled differently, since they required custom paths\n        # Hence they don't exist as Scope objects\n        special_scope_map = {\n            SCOPE_GITHUB_USER: _(\"GitHub Compatibility: Access your User Information\"),\n            SCOPE_GITHUB_USER_READ: _(\"GitHub Compatibility: Access your User Information\"),\n            SCOPE_GITHUB_USER_EMAIL: _(\"GitHub Compatibility: Access you Email addresses\"),\n            SCOPE_GITHUB_ORG_READ: _(\"GitHub Compatibility: Access your Groups\"),\n            SCOPE_AUTHENTIK_API: _(\"authentik API Access on behalf of your user\"),\n        }\n        for scope in scopes:\n            if scope in special_scope_map:\n                scope_descriptions.append(\n                    PermissionDict(id=scope, name=str(special_scope_map[scope]))\n                )\n        return scope_descriptions\n\n    def get_claims(self, provider: OAuth2Provider, token: BaseGrantModel) -> dict[str, Any]:\n        \"\"\"Get a dictionary of claims from scopes that the token\n        requires and are assigned to the provider.\"\"\"\n\n        scopes_from_client = token.scope\n        final_claims = {}\n        for scope in ScopeMapping.objects.filter(\n            provider=provider, scope_name__in=scopes_from_client\n        ).order_by(\"scope_name\"):\n            scope: ScopeMapping\n            value = None\n            try:\n                value = scope.evaluate(\n                    user=token.user,\n                    request=self.request,\n                    provider=provider,\n                    token=token,\n                )\n            except PropertyMappingExpressionException as exc:\n                Event.new(\n                    EventAction.CONFIGURATION_ERROR,\n                    message=f\"Failed to evaluate property-mapping: '{scope.name}'\",\n                    provider=provider,\n                    mapping=scope,\n                ).from_http(self.request)\n                LOGGER.warning(\"Failed to evaluate property mapping\", exc=exc)\n            if value is None:\n                continue\n            if not isinstance(value, dict):\n                LOGGER.warning(\n                    \"Scope returned a non-dict value, ignoring\",\n                    scope=scope,\n                    value=value,\n                )\n                continue\n            always_merger.merge(final_claims, value)\n            LOGGER.debug(\"updated scope\", scope=scope)\n        return final_claims\n\n    def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:\n        self.token = kwargs.get(\"token\", None)\n        response = super().dispatch(request, *args, **kwargs)\n        allowed_origins = []\n        if self.token:\n            allowed_origins = self.token.provider.redirect_uris.split(\"\\n\")\n        cors_allow(self.request, response, *allowed_origins)\n        return response\n\n    def options(self, request: HttpRequest) -> HttpResponse:\n        return TokenResponse({})\n\n    def get(self, request: HttpRequest, **kwargs) -> HttpResponse:\n        \"\"\"Handle GET Requests for UserInfo\"\"\"\n        if not self.token:\n            return HttpResponseBadRequest()\n        claims = {}\n        claims.setdefault(\"sub\", self.token.id_token.sub)\n        claims.update(self.get_claims(self.token.provider, self.token))\n        if self.token.id_token.nonce:\n            claims[\"nonce\"] = self.token.id_token.nonce\n        response = TokenResponse(claims)\n        return response\n\n    def post(self, request: HttpRequest, **kwargs) -> HttpResponse:\n        \"\"\"POST Requests behave the same as GET Requests, so the get handler is called here\"\"\"\n        return self.get(request, **kwargs)\n", "path": "authentik/providers/oauth2/views/userinfo.py"}]}