diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3c285046a78b08623512ad8799bf1f7823d6a2e4..6c95206429702edd60ea1d68ba7cb72852299c57 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -14,6 +14,7 @@ Added
 
 * Provide an ``ExtensiblePolymorphicModel`` to support the features of extensible models for polymorphic models and vice-versa.
 * Implement optional Sentry integration for error and performance tracing.
+* Option to limit allowed scopes per application, including mixin to enforce that limit on OAuth resource views
 * Support trusted OAuth applications that leave out the authorisation screen.
 
 Changed
@@ -28,6 +29,12 @@ Fixed
 
 * Fix default admin contacts
 
+Credits
+~~~~~~~
+
+* We welcome new contributor 🐧 Jonathan Krüger!
+* We welcome new contributor 🐭 Lukas Weichelt!
+
 `2.0`_ - 2021-10-29
 -------------------
 
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index 6468e6793012e4f4fe4e5c9835a7b5e6e665bf86..dd695cd06ea2c0dbc862be8c9989047d9934c0c3 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -31,6 +31,7 @@ from .registries import (
     person_preferences_registry,
     site_preferences_registry,
 )
+from .util.auth_helpers import AppScopes
 from .util.core_helpers import get_site_preferences
 
 
@@ -594,6 +595,12 @@ class ListActionForm(ActionForm):
 
 
 class OAuthApplicationForm(forms.ModelForm):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields["allowed_scopes"].widget = forms.SelectMultiple(
+            choices=list(AppScopes().get_all_scopes().items())
+        )
+
     class Meta:
         model = OAuthApplication
         fields = (
@@ -601,6 +608,7 @@ class OAuthApplicationForm(forms.ModelForm):
             "client_id",
             "client_secret",
             "client_type",
+            "allowed_scopes",
             "redirect_uris",
             "skip_authorization",
         )
diff --git a/aleksis/core/migrations/0026_oauthapplication_allowed_scopes.py b/aleksis/core/migrations/0026_oauthapplication_allowed_scopes.py
new file mode 100644
index 0000000000000000000000000000000000000000..23e1b40b56a2ce2fbe1082b8728adea642f3e82b
--- /dev/null
+++ b/aleksis/core/migrations/0026_oauthapplication_allowed_scopes.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.8 on 2021-11-05 10:37
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0025_oauth_align_user_fk'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='oauthapplication',
+            name='allowed_scopes',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=32), blank=True, null=True, size=None, verbose_name='Allowed scopes that clients can request'),
+        ),
+    ]
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 3a7e123597e235a43662959df72614ed7e1ca38f..a78bfcc141d5539479983a50bbd257e6b7b7e5b1 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -9,6 +9,7 @@ from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group as DjangoGroup
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.postgres.fields import ArrayField
 from django.contrib.sites.models import Site
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator
@@ -1107,6 +1108,14 @@ class OAuthApplication(AbstractApplication):
         max_length=32, choices=AbstractApplication.GRANT_TYPES, blank=True, null=True
     )
 
+    # Optional list of alloewd scopes
+    allowed_scopes = ArrayField(
+        models.CharField(max_length=32),
+        verbose_name=_("Allowed scopes that clients can request"),
+        null=True,
+        blank=True,
+    )
+
     def allows_grant_type(self, *grant_types: set[str]) -> bool:
         allowed_grants = get_site_preferences()["auth__oauth_allowed_grants"]
 
diff --git a/aleksis/core/templates/oauth2_provider/application/detail.html b/aleksis/core/templates/oauth2_provider/application/detail.html
index 4e896dad70d5860516e7f3caedd9b424305b8025..da6b8abf81ddc1916de7b0c7feb5a7262f453037 100644
--- a/aleksis/core/templates/oauth2_provider/application/detail.html
+++ b/aleksis/core/templates/oauth2_provider/application/detail.html
@@ -46,6 +46,14 @@
         {{ application.client_type }}
       </td>
     </tr>
+    <tr>
+      <th>
+        {% trans "Allowed scopes" %}
+      </th>
+      <td>
+        {{ application.allowed_scopes|join:", " }}
+      </td>
+    </tr>
     <tr>
       <th>
         {% trans "Redirect URIs" %}
@@ -54,6 +62,14 @@
         {{ application.redirect_uris }}
       </td>
     </tr>
+    <tr>
+      <th>
+        {% trans "Skip Authorisation" %}
+      </th>
+      <td>
+        <i class="material-icons">{{ application.skip_authorization|yesno:"check,close" }}</i>
+      </td>
+    </tr>
     </tbody>
   </table>
 {% endblock %}
diff --git a/aleksis/core/util/auth_helpers.py b/aleksis/core/util/auth_helpers.py
index 4f6e403cb678ff4a39f0199b0670ff6b3d8cdb45..21acddda5acef95d6fa7c9636e999717d899ca0b 100644
--- a/aleksis/core/util/auth_helpers.py
+++ b/aleksis/core/util/auth_helpers.py
@@ -10,6 +10,10 @@ from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
 from oauth2_provider.models import AbstractApplication
 from oauth2_provider.oauth2_validators import OAuth2Validator
 from oauth2_provider.scopes import BaseScopes
+from oauth2_provider.views.mixins import (
+    ClientProtectedResourceMixin as _ClientProtectedResourceMixin,
+)
+from oauthlib.common import Request as OauthlibRequest
 
 from .apps import AppConfig
 from .core_helpers import get_site_preferences, has_person
@@ -106,6 +110,9 @@ class AppScopes(BaseScopes):
         scopes = []
         for app in AppConfig.__subclasses__():
             scopes += app.get_available_scopes()
+        # Filter by allowed scopes of requesting application
+        if application and application.allowed_scopes:
+            scopes = list(filter(lambda scope: scope in application.allowed_scopes, scopes))
         return scopes
 
     def get_default_scopes(
@@ -118,4 +125,43 @@ class AppScopes(BaseScopes):
         scopes = []
         for app in AppConfig.__subclasses__():
             scopes += app.get_default_scopes()
+        # Filter by allowed scopes of requesting application
+        if application and application.allowed_scopes:
+            scopes = list(filter(lambda scope: scope in application.allowed_scopes, scopes))
         return scopes
+
+
+class ClientProtectedResourceMixin(_ClientProtectedResourceMixin):
+    """Mixin for protecting resources with client authentication as mentioned in rfc:`3.2.1`.
+
+    This involves authenticating with any of: HTTP Basic Auth, Client Credentials and
+    Access token in that order. Breaks off after first validation.
+
+    This sub-class extends the functionality of Django OAuth Toolkit's mixin with support
+    for AlekSIS's `allowed_scopes` feature. For applications that have configured allowed
+    scopes, the required scopes for the view are checked to be a subset of the application's
+    allowed scopes (best to be combined with ScopedResourceMixin).
+    """
+
+    def authenticate_client(self, request: HttpRequest) -> bool:
+        """Return a boolean representing if client is authenticated with client credentials.
+
+        If the view has configured required scopes, they are verified against the application's
+        allowed scopes.
+        """
+        # Build an OAuth request so we can handle client information
+        core = self.get_oauthlib_core()
+        uri, http_method, body, headers = core._extract_params(request)
+        oauth_request = OauthlibRequest(uri, http_method, body, headers)
+
+        # Verify general authentication of the client
+        if not core.server.request_validator.authenticate_client(oauth_request):
+            # Client credentials were invalid
+            return False
+
+        # Verify scopes of configured application
+        # The OAuth request was enriched with a reference to the Application when using the
+        #  validator above.
+        required_scopes = set(self.get_scopes() or [])
+        allowed_scopes = set(AppScopes().get_available_scopes(oauth_request.client) or [])
+        return required_scopes.issubset(allowed_scopes)