diff --git a/aleksis/apps/kort/apps.py b/aleksis/apps/kort/apps.py
index 76097c7f3879586ff423f4eb4b4cf534ecea253e..797ede1bc9d1a9db4456910879452dccfc6bdf85 100644
--- a/aleksis/apps/kort/apps.py
+++ b/aleksis/apps/kort/apps.py
@@ -1,3 +1,8 @@
+from django.apps import apps
+from django.db import models
+from django.db.models import functions
+from django.utils.translation import gettext as _
+
 from aleksis.core.util.apps import AppConfig
 
 
@@ -16,3 +21,22 @@ class DefaultConfig(AppConfig):
         "Jonathan Weth",
         "dev@jonathanweth.de",
     )
+
+    @classmethod
+    def get_all_scopes(cls) -> dict[str, str]:
+        """Return all OAuth scopes and their descriptions for this app."""
+        Card = apps.get_model("kort", "Card")
+        label_prefix = _("Access and manage printer status and print jobs")
+        scopes = dict(
+            Card.objects.annotate(
+                scope=functions.Concat(
+                    models.Value(f"{Card.SCOPE_PREFIX}_"),
+                    models.F("pk"),
+                    output_field=models.CharField(),
+                ),
+                label=functions.Concat(models.Value(f"{label_prefix}: "), models.F("name")),
+            )
+            .values_list("scope", "label")
+            .distinct()
+        )
+        return scopes
diff --git a/aleksis/apps/kort/migrations/0006_auto_20220310_2003.py b/aleksis/apps/kort/migrations/0006_auto_20220310_2003.py
new file mode 100644
index 0000000000000000000000000000000000000000..52e853d5d9e06971b8b5804720bbcef8b3a8b7b0
--- /dev/null
+++ b/aleksis/apps/kort/migrations/0006_auto_20220310_2003.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.2.12 on 2022-03-10 19:03
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
+        ('kort', '0005_card_pdf_file'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='cardprinter',
+            name='oauth2_application',
+            field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='core.oauthapplication', verbose_name='OAuth2 application'),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='cardprinter',
+            name='location',
+            field=models.CharField(blank=True, max_length=255, verbose_name='Location'),
+        ),
+        migrations.AlterField(
+            model_name='cardprinter',
+            name='status',
+            field=models.CharField(choices=[('online', 'Online'), ('offline', 'Offline'), ('with_errors', 'With errors'), ('not_registered', 'Not registered')], default='not_registered', max_length=255, verbose_name='Status'),
+        ),
+    ]
diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py
index e9123d8ae81f12cc3a4ea1ffe987fbb543a49119..64b2d36941336a588e2af31a83ad23f38f6bb2ac 100644
--- a/aleksis/apps/kort/models.py
+++ b/aleksis/apps/kort/models.py
@@ -10,13 +10,14 @@ from django.utils.translation import gettext as _
 from celery.result import AsyncResult
 
 from aleksis.core.mixins import ExtensibleModel
-from aleksis.core.models import Person
+from aleksis.core.models import OAuthApplication, Person
 
 
 class CardPrinterStatus(models.TextChoices):
     ONLINE = "online", _("Online")
     OFFLINE = "offline", _("Offline")
     WITH_ERRORS = "with_errors", _("With errors")
+    NOT_REGISTERED = "not_registered", _("Not registered")
 
 
 class PrintStatus(models.TextChoices):
@@ -26,16 +27,40 @@ class PrintStatus(models.TextChoices):
 
 
 class CardPrinter(ExtensibleModel):
+    SCOPE_PREFIX = "card_printer"
     name = models.CharField(max_length=255, verbose_name=_("Name"))
     description = models.TextField(verbose_name=_("Description"), blank=True)
-    location = models.CharField(max_length=255, verbose_name=_("Location"))
+    location = models.CharField(max_length=255, verbose_name=_("Location"), blank=True)
 
     status = models.CharField(
-        max_length=255, verbose_name=_("Status"), choices=CardPrinterStatus.choices
+        max_length=255,
+        verbose_name=_("Status"),
+        choices=CardPrinterStatus.choices,
+        default=CardPrinterStatus.NOT_REGISTERED,
     )
     status_text = models.TextField(verbose_name=_("Status text"), blank=True)
     last_seen_at = models.DateTimeField(verbose_name=_("Last seen at"), blank=True, null=True)
 
+    oauth2_application = models.ForeignKey(
+        to=OAuthApplication,
+        on_delete=models.CASCADE,
+        verbose_name=_("OAuth2 application"),
+        blank=True,
+        null=True,
+    )
+
+    def save(self, *args, **kwargs):
+        if not self.oauth2_application:
+            application = OAuthApplication(
+                client_type=OAuthApplication.CLIENT_CONFIDENTIAL,
+                authorization_grant_type=OAuthApplication.GRANT_AUTHORIZATION_CODE,
+                name=f"Card printer: {self.name}",
+            )
+            application.save()
+            self.oauth2_application = application
+
+        super().save(*args, **kwargs)
+
     def __str__(self):
         return self.name