prompt: 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()
  
  ```
  
golden_diff: 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)
  
verification_info: {"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"}]}