diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index e12f336fd5b173aff9b272f1ff9ca0ce3ae2a689..709b59e44a5b5bb9170d57d593fe1ea7930f612d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,11 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Added
+~~~~~
+
+* [OAuth] Allow apps to fill in their own claim data matching their scopes
+
 `2.2.1_ – 2021-12-02
 --------------------
 
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index 2af102477a08571dfe5f3968eefcc8b3e805e0ff..520a5fc435fe5cd8ef55435d3c680f978250abb0 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -9,6 +9,7 @@ from django.utils.translation import gettext as _
 
 from dynamic_preferences.registries import preference_models
 from health_check.plugins import plugin_dir
+from oauthlib.common import Request as OauthlibRequest
 
 from .registries import (
     group_preferences_registry,
@@ -156,3 +157,49 @@ class CoreConfig(AppConfig):
                 "groups": _("Groups"),
             }
         return scopes
+
+    @classmethod
+    def get_additional_claims(cls, scopes: list[str], request: OauthlibRequest) -> dict[str, Any]:
+        django_request = HttpRequest()
+        django_request.META = request.headers
+
+        claims = {
+            "preferred_username": request.user.username,
+        }
+
+        if "profile" in scopes:
+            if has_person(request.user):
+                claims["given_name"] = request.user.person.first_name
+                claims["family_name"] = request.user.person.last_name
+                claims["profile"] = django_request.build_absolute_uri(
+                    request.user.person.get_absolute_url()
+                )
+                if request.user.person.photo:
+                    claims["picture"] = django_request.build_absolute_uri(
+                        request.user.person.photo.url
+                    )
+            else:
+                claims["given_name"] = request.user.first_name
+                claims["family_name"] = request.user.last_name
+
+        if "email" in scopes:
+            if has_person(request.user):
+                claims["email"] = request.user.person.email
+            else:
+                claims["email"] = request.user.email
+
+        if "address" in scopes and has_person(request.user):
+            claims["address"] = {
+                "street_address": request.user.person.street
+                + " "
+                + request.user.person.housenumber,
+                "locality": request.user.person.place,
+                "postal_code": request.user.person.postal_code,
+            }
+
+        if "groups" in scopes and has_person(request.user):
+            claims["groups"] = list(
+                request.user.person.member_of.values_list("name", flat=True).all()
+            )
+
+        return claims
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index 38b5d27946ad4542694298571a6bd4f36da325ae..5b3898dd835506ade932ebccf30e4cf0de15486e 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -8,6 +8,7 @@ from django.http import HttpRequest
 
 from dynamic_preferences.signals import preference_updated
 from license_expression import Licensing
+from oauthlib.common import Request as OauthlibRequest
 from spdx_license_list import LICENSES
 
 from .core_helpers import copyright_years
@@ -244,6 +245,11 @@ class AppConfig(django.apps.AppConfig):
         """Return a list of all OAuth scopes to always include for this request and application."""
         return []
 
+    @classmethod
+    def get_additional_claims(cls, scopes: list[str], request: OauthlibRequest) -> dict[str, Any]:
+        """Get claim data for requested scopes."""
+        return {}
+
     def _maintain_default_data(self):
         from django.contrib.auth.models import Permission
         from django.contrib.contenttypes.models import ContentType
diff --git a/aleksis/core/util/auth_helpers.py b/aleksis/core/util/auth_helpers.py
index e0cfcc778a55563e91ecf4d0d1027227e01a99b3..056b5156da68233d3b3ee2378ccffdff95cfbcd6 100644
--- a/aleksis/core/util/auth_helpers.py
+++ b/aleksis/core/util/auth_helpers.py
@@ -1,6 +1,6 @@
 """Helpers/overrides for django-allauth."""
 
-from typing import Optional
+from typing import Any, Optional
 
 from django.conf import settings
 from django.http import HttpRequest
@@ -16,7 +16,6 @@ from oauth2_provider.views.mixins import (
 from oauthlib.common import Request as OauthlibRequest
 
 from .apps import AppConfig
-from .core_helpers import get_site_preferences, has_person
 
 
 class OurSocialAccountAdapter(DefaultSocialAccountAdapter):
@@ -43,52 +42,16 @@ class OurAccountAdapter(DefaultAccountAdapter):
 
 
 class CustomOAuth2Validator(OAuth2Validator):
-    def get_additional_claims(self, request):
-        django_request = HttpRequest()
-        django_request.META = request.headers
-
+    def get_additional_claims(self, request: OauthlibRequest) -> dict[str, Any]:
+        # Pull together scopes from request and from access token
         scopes = request.scopes.copy()
         if request.access_token:
             scopes += request.access_token.scope.split(" ")
 
-        claims = {
-            "preferred_username": request.user.username,
-        }
-
-        if "profile" in scopes:
-            if has_person(request.user):
-                claims["given_name"] = request.user.person.first_name
-                claims["family_name"] = request.user.person.last_name
-                claims["profile"] = django_request.build_absolute_uri(
-                    request.user.person.get_absolute_url()
-                )
-                if request.user.person.photo:
-                    claims["picture"] = django_request.build_absolute_uri(
-                        request.user.person.photo.url
-                    )
-            else:
-                claims["given_name"] = request.user.first_name
-                claims["family_name"] = request.user.last_name
-
-        if "email" in scopes:
-            if has_person(request.user):
-                claims["email"] = request.user.person.email
-            else:
-                claims["email"] = request.user.email
-
-        if "address" in scopes and has_person(request.user):
-            claims["address"] = {
-                "street_address": request.user.person.street
-                + " "
-                + request.user.person.housenumber,
-                "locality": request.user.person.place,
-                "postal_code": request.user.person.postal_code,
-            }
-
-        if "groups" in scopes and has_person(request.user):
-            claims["groups"] = list(
-                request.user.person.member_of.values_list("name", flat=True).all()
-            )
+        claims = {}
+        # Pull together claim data from all apps
+        for app in AppConfig.__subclasses__():
+            claims.update(app.get_additional_claims(scopes, request))
 
         return claims