diff --git a/authentik/providers/oauth2/tests/test_userinfo.py b/authentik/providers/oauth2/tests/test_userinfo.py new file mode 100644 index 000000000..462e748da --- /dev/null +++ b/authentik/providers/oauth2/tests/test_userinfo.py @@ -0,0 +1,111 @@ +"""Test userinfo view""" +import json +from dataclasses import asdict + +from django.urls import reverse +from django.utils.encoding import force_str + +from authentik.core.models import Application, User +from authentik.crypto.models import CertificateKeyPair +from authentik.events.models import Event, EventAction +from authentik.flows.models import Flow +from authentik.managed.manager import ObjectManager +from authentik.providers.oauth2.generators import ( + generate_client_id, + generate_client_secret, +) +from authentik.providers.oauth2.models import ( + IDToken, + OAuth2Provider, + RefreshToken, + ScopeMapping, +) +from authentik.providers.oauth2.tests.utils import OAuthTestCase + + +class TestUserinfo(OAuthTestCase): + """Test token view""" + + def setUp(self) -> None: + super().setUp() + ObjectManager().run() + self.app = Application.objects.create(name="test", slug="test") + self.provider: OAuth2Provider = OAuth2Provider.objects.create( + name="test", + client_id=generate_client_id(), + client_secret=generate_client_secret(), + authorization_flow=Flow.objects.first(), + redirect_uris="", + rsa_key=CertificateKeyPair.objects.first(), + ) + self.provider.property_mappings.set(ScopeMapping.objects.all()) + # Needs to be assigned to an application for iss to be set + self.app.provider = self.provider + self.app.save() + self.user = User.objects.get(username="akadmin") + self.token: RefreshToken = RefreshToken.objects.create( + provider=self.provider, + user=self.user, + access_token=generate_client_id(), + refresh_token=generate_client_id(), + _scope="openid user profile", + _id_token=json.dumps( + asdict( + IDToken("foo", "bar"), + ) + ), + ) + + def test_userinfo_normal(self): + """test user info with all normal scopes""" + res = self.client.get( + reverse("authentik_providers_oauth2:userinfo"), + HTTP_AUTHORIZATION=f"Bearer {self.token.access_token}", + ) + self.assertJSONEqual( + force_str(res.content), + { + "name": "authentik Default Admin", + "given_name": "authentik Default Admin", + "family_name": "", + "preferred_username": "akadmin", + "nickname": "akadmin", + "groups": ["authentik Admins"], + "sub": "bar", + }, + ) + self.assertEqual(res.status_code, 200) + + def test_userinfo_invalid_scope(self): + """test user info with a broken scope""" + scope = ScopeMapping.objects.create( + name="test", scope_name="openid", expression="q" + ) + self.provider.property_mappings.add(scope) + + res = self.client.get( + reverse("authentik_providers_oauth2:userinfo"), + HTTP_AUTHORIZATION=f"Bearer {self.token.access_token}", + ) + self.assertJSONEqual( + force_str(res.content), + { + "name": "authentik Default Admin", + "given_name": "authentik Default Admin", + "family_name": "", + "preferred_username": "akadmin", + "nickname": "akadmin", + "groups": ["authentik Admins"], + "sub": "bar", + }, + ) + self.assertEqual(res.status_code, 200) + + events = Event.objects.filter( + action=EventAction.CONFIGURATION_ERROR, + ) + self.assertTrue(events.exists()) + self.assertEqual( + events.first().context["message"], + "Failed to evaluate property-mapping: name 'q' is not defined", + ) diff --git a/authentik/providers/oauth2/views/userinfo.py b/authentik/providers/oauth2/views/userinfo.py index 3c544b425..119ec79c2 100644 --- a/authentik/providers/oauth2/views/userinfo.py +++ b/authentik/providers/oauth2/views/userinfo.py @@ -7,6 +7,8 @@ from django.http.response import HttpResponseBadRequest from django.views import View from structlog.stdlib import get_logger +from authentik.core.exceptions import PropertyMappingExpressionException +from authentik.events.models import Event, EventAction from authentik.providers.oauth2.constants import ( SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER, @@ -63,12 +65,20 @@ class UserInfoView(View): for scope in ScopeMapping.objects.filter( provider=token.provider, scope_name__in=scopes_from_client ).order_by("scope_name"): - value = scope.evaluate( - user=token.user, - request=self.request, - provider=token.provider, - token=token, - ) + value = None + try: + value = scope.evaluate( + user=token.user, + request=self.request, + provider=token.provider, + token=token, + ) + except PropertyMappingExpressionException as exc: + Event.new( + EventAction.CONFIGURATION_ERROR, + message=f"Failed to evaluate property-mapping: {str(exc)}", + mapping=scope, + ).from_http(self.request) if value is None: continue if not isinstance(value, dict):