diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3b4a70be33ebf01ed8c37cdff7e93ceb0471aef2..151b99434bcb822d4d8ea812b60d7b7d9928438b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,11 +9,23 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Added
+~~~~~
+
+* [OAuth] Expired tokens are now cleared in a periodic task
+
+Changed
+~~~~~~~
+
+* PWA icons can now be marked maskable
+
 Fixed
 ~~~~~
 
 * PDF generation failed with S3 storage due to incompatibility with boto3
+* PWA theme colour defaulted to red
 * Form for editing group type displayed irrelevant fields
+* Permission groups could get outdated if re-assigning a user account to a different person
 
 `2.7`_ - 2022-01-24
 -------------------
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index f46a803b687cc9cde9a893c642586eac232fd84d..d5dc7d7befe1395b222ff492f1a9f8eed3994f69 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -316,7 +316,7 @@ class Person(ExtensibleModel):
     def initials(self):
         return f"{self.first_name[0]}{self.last_name[0]}".upper()
 
-    user_info_tracker = FieldTracker(fields=("first_name", "last_name", "email"))
+    user_info_tracker = FieldTracker(fields=("first_name", "last_name", "email", "user"))
 
     @property
     def member_of_recursive(self) -> QuerySet:
@@ -336,16 +336,29 @@ class Person(ExtensibleModel):
 
     def save(self, *args, **kwargs):
         # Determine all fields that were changed since last load
-        dirty = self.pk is None or bool(self.user_info_tracker.changed())
+        changed = self.user_info_tracker.changed()
 
         super().save(*args, **kwargs)
 
-        if self.user and dirty:
-            # Synchronise user fields to linked User object to keep it up to date
-            self.user.first_name = self.first_name
-            self.user.last_name = self.last_name
-            self.user.email = self.email
-            self.user.save()
+        if self.pk is None or bool(changed):
+            if "user" in changed:
+                # Clear groups of previous Django user
+                previous_user = changed["user"]
+                if previous_user is not None:
+                    get_user_model().objects.get(pk=previous_user).groups.clear()
+
+            if self.user:
+                if "first_name" in changed or "last_name" in changed or "email" in changed:
+                    # Synchronise user fields to linked User object to keep it up to date
+                    self.user.first_name = self.first_name
+                    self.user.last_name = self.last_name
+                    self.user.email = self.email
+                    self.user.save()
+
+                if "user" in changed:
+                    # Synchronise groups to Django groups
+                    for group in self.member_of.union(self.owner_of.all()).all():
+                        group.save(force=True)
 
         # Select a primary group if none is set
         self.auto_select_primary_group()
diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py
index fe29f220f211616bf57d08c04e87a95b60c84365..f1fb0227eab16a152f29918e91b37d0808471812 100644
--- a/aleksis/core/preferences.py
+++ b/aleksis/core/preferences.py
@@ -112,6 +112,17 @@ class PWAIcon(PublicFilePreferenceMixin, FilePreference):
     required = False
 
 
+@site_preferences_registry.register
+class PWAIconMaskable(BooleanPreference):
+    """PWA icon is maskable."""
+
+    section = theme
+    name = "pwa_icon_maskable"
+    verbose_name = _("PWA-Icon is maskable")
+    default = True
+    required = False
+
+
 @site_preferences_registry.register
 class MailOutName(StringPreference):
     """Mail out name of your AlekSIS instance."""
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 2defba33645df5cf2f16be4a195a6385d00e0ce4..d0547512292223c05771e7d3152c9f91764e2c8c 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -704,7 +704,7 @@ if _settings.get("dev.uwsgi.celery", DEBUG):
     UWSGI["attach-daemon"].append("celery -A aleksis.core beat")
 
 DEFAULT_FAVICON_PATHS = {
-    "pwa_icon": os.path.join(STATIC_ROOT, "img/aleksis-icon.png"),
+    "pwa_icon": os.path.join(STATIC_ROOT, "img/aleksis-icon-maskable.png"),
     "favicon": os.path.join(STATIC_ROOT, "img/aleksis-favicon.png"),
 }
 PWA_ICONS_CONFIG = {
diff --git a/aleksis/core/static/img/aleksis-icon-maskable.png b/aleksis/core/static/img/aleksis-icon-maskable.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ec3bb2c04392e646c7d00c5ba3a316ebf99486e
Binary files /dev/null and b/aleksis/core/static/img/aleksis-icon-maskable.png differ
diff --git a/aleksis/core/static/img/aleksis-icon-maskable.svg b/aleksis/core/static/img/aleksis-icon-maskable.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e37f3c3606d748039e4dc03cc4b0f27f5de491e1
--- /dev/null
+++ b/aleksis/core/static/img/aleksis-icon-maskable.svg
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   id="svg8"
+   width="256"
+   height="256"
+   version="1.1"
+   viewBox="0 0 67.73 67.73"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <title
+     id="title32">AlekSIS icon</title>
+  <defs
+     id="defs2">
+    <linearGradient
+       id="shadow-gradient"
+       x1="-19.53"
+       x2="165.4"
+       y1="-19.53"
+       y2="165.4"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.2646,0,0,0.2646,109.82466,-4.9393086)">
+      <stop
+         id="stop9444"
+         offset="0" />
+      <stop
+         id="stop9446"
+         stop-opacity="0"
+         offset="1" />
+    </linearGradient>
+  </defs>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title>AlekSIS icon</dc:title>
+        <cc:license
+           rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
+        <dc:date>2020-02-29</dc:date>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Dominik George</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:contributor>
+          <cc:Agent>
+            <dc:title>Julian Leucker</dc:title>
+          </cc:Agent>
+        </dc:contributor>
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:requires
+           rdf:resource="http://creativecommons.org/ns#Notice" />
+        <cc:requires
+           rdf:resource="http://creativecommons.org/ns#Attribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+        <cc:requires
+           rdf:resource="http://creativecommons.org/ns#ShareAlike" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="background-with-glow">
+    <rect
+       id="background"
+       y="-3.357e-6"
+       width="67.73"
+       height="67.73"
+       rx="3.307"
+       ry="3.307"
+       fill="#0d5eaf"
+       stroke-width=".4104" />
+    <path
+       id="glow"
+       transform="scale(.2646)"
+       d="m9.959 0.2578c-5.698 1.168-9.959 6.189-9.959 12.24v52.31a234.1 86.8 0 0 0 188.9 35.71 234.1 86.8 0 0 0 67.09-3.715v-84.3c0-4.319-2.17-8.112-5.482-10.36-0.01554-0.01049-0.03129-0.02083-0.04688-0.03125-0.2971-0.1993-0.6034-0.3849-0.918-0.5586-0.05211-0.02867-0.1037-0.05799-0.1562-0.08594-0.2939-0.1568-0.5968-0.3001-0.9043-0.4336-0.05654-0.02442-0.111-0.05257-0.168-0.07617-0.3308-0.1378-0.6709-0.2577-1.016-0.3672-0.03502-0.01105-0.06836-0.02636-0.1035-0.03711-0.3806-0.1172-0.7687-0.2158-1.164-0.2969h-236.1z"
+       fill="#fff"
+       opacity=".2"
+       stroke-width="1.551" />
+  </g>
+  <g
+     id="favicon-bag"
+     transform="translate(-46.809258,-7.6499461)"
+     display="none">
+    <use
+       xlink:href="#schoolbag"
+       transform="matrix(2.25,0,0,2.25,-30.62,0)"
+       fill="#ffffff"
+       x="0"
+       y="0"
+       width="100%"
+       height="100%"
+       id="use18" />
+  </g>
+  <g
+     id="widgets-with-shadow"
+     transform="matrix(0.62859773,0,0,0.63035889,12.536993,12.511931)"
+     style="stroke-width:1.58861">
+    <g
+       id="widgets"
+       fill="#ffffff"
+       style="stroke-width:2.18867">
+      <path
+         id="schoolbag"
+         d="m 44.52,19.7 h 10.45 a 0.4353,0.4353 0 0 1 0.4353,0.4353 v 3.917 A 0.4353,0.4353 0 0 1 54.97,24.4876 H 44.52 a 0.4353,0.4353 0 0 1 -0.4353,-0.4353 v -3.917 A 0.4353,0.4353 0 0 1 44.52,19.7 Z M 55.4,18.394 v -0.8705 a 0.4353,0.4353 0 0 0 -0.4353,-0.4353 h -10.45 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 v 0.8705 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 10.45 A 0.4353,0.4353 0 0 0 55.4,18.394 Z m -14.8,10.01 v 0.8705 a 2.176,2.176 0 0 0 2.176,2.176 h 13.93 a 2.176,2.176 0 0 0 2.176,-2.176 V 28.404 a 0.4353,0.4353 0 0 0 -0.4353,-0.4353 h -17.41 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 z m -0.8705,-7.4 v -1.741 a 0.4353,0.4353 0 0 0 -0.4353,-0.4353 h -0.8705 a 1.306,1.306 0 0 0 -1.306,1.306 v 0.8706 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 1.741 a 0.4353,0.4353 0 0 0 0.4353,-0.4353 z m -2.612,1.741 v 4.788 a 1.306,1.306 0 0 0 1.306,1.306 h 0.8705 a 0.4353,0.4353 0 0 0 0.4353,-0.4353 v -5.659 A 0.4353,0.4353 0 0 0 39.294,22.3094 h -1.741 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 z m 22.63,0 v 5.659 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 0.8705 a 1.306,1.306 0 0 0 1.306,-1.306 v -4.788 A 0.4353,0.4353 0 0 0 61.924,22.31 h -1.741 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 z m 2.612,-1.741 v -0.8706 a 1.306,1.306 0 0 0 -1.306,-1.306 H 60.183 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 v 1.741 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 1.741 a 0.4353,0.4353 0 0 0 0.4353,-0.4353 z m -3.482,-6.203 v 11.86 a 0.4353,0.4353 0 0 1 -0.4353,0.4353 h -17.41 A 0.4353,0.4353 0 0 1 40.5969,26.661 v -11.86 a 6.42,6.42 0 0 1 6.42,-6.42 h 0.3809 A 0.1632,0.1632 0 0 0 47.561,8.2178 V 7.5105 a 2.179,2.179 0 0 1 2.229,-2.176 c 1.188,0.0282 2.124,1.028 2.124,2.217 v 0.6664 a 0.1632,0.1632 0 0 0 0.1632,0.1632 h 0.3809 a 6.42,6.42 0 0 1 6.42,6.42 z m -10.45,-6.583 a 0.1632,0.1632 0 0 0 0.1632,0.1632 h 2.285 A 0.1632,0.1632 0 0 0 51.0389,8.218 V 7.5404 c 0,-0.7306 -0.5978,-1.348 -1.328,-1.336 a 1.307,1.307 0 0 0 -1.283,1.306 z m -0.4353,4.081 a 1.741,1.741 0 1 0 1.741,-1.741 1.741,1.741 0 0 0 -1.741,1.741 z m 8.27,5.223 a 1.307,1.307 0 0 0 -1.306,-1.306 h -10.45 a 1.307,1.307 0 0 0 -1.306,1.306 v 6.529 a 1.307,1.307 0 0 0 1.306,1.306 h 10.45 a 1.307,1.307 0 0 0 1.306,-1.306 z"
+         stroke-width="0.0864368" />
+      <path
+         id="puzzle-ll"
+         d="m 8.484,37.13 c -1.759,0 -3.175,1.416 -3.175,3.175 v 18.89 c 0,1.759 1.416,3.175 3.175,3.175 h 18.93 c 1.759,0 3.175,-1.416 3.175,-3.175 v -18.89 c 0,-1.759 -1.416,-3.175 -3.175,-3.175 h -5.842 v 2.798 h -0.01188 c 0.0052,0.0528 0.01281,0.1048 0.01447,0.1586 0,2.002 -1.623,3.625 -3.625,3.625 -2.002,0 -3.625,-1.623 -3.625,-3.625 0.01013,-0.0551 0.02494,-0.1053 0.03669,-0.1586 H 14.32614 V 37.13 Z m 28.69,12.62 c 3e-6,2.002 -1.623,3.625 -3.625,3.625 -4.515,-0.8302 -3.163,-7.154 0,-7.251 2.002,0 3.625,1.623 3.625,3.625 z m -6.582,-3.623 h 2.798 v 7.247 h -2.798 z"
+         style="stroke-width:1.58861" />
+      <path
+         id="puzzle-lr"
+         d="m 37.31,59.24 c 0,1.759 1.416,3.175 3.175,3.175 h 18.89 c 1.759,0 3.175,-1.416 3.175,-3.175 V 40.31 c 0,-1.759 -1.416,-3.175 -3.175,-3.175 h -18.89 c -1.759,0 -3.175,1.416 -3.175,3.175 v 5.842 h 2.798 v 0.0119 c 0.0528,-0.005 0.1048,-0.0128 0.1586,-0.0145 2.002,0 3.625,1.623 3.625,3.625 0,2.002 -1.623,3.625 -3.625,3.625 -0.0551,-0.0101 -0.1053,-0.0249 -0.1586,-0.0367 v 0.0351 H 37.31 Z"
+         style="stroke-width:1.58861" />
+      <path
+         id="puzzle-ul"
+         d="m 31.48,20.23 c 0.9818,-0.9818 0.9818,-2.563 0,-3.544 L 20.94,6.146 c -0.9818,-0.9819 -2.563,-0.9819 -3.545,0 l -10.57,10.57 c -0.9818,0.9818 -0.9818,2.563 6e-6,3.544 l 10.54,10.54 c 0.9818,0.9819 2.563,0.9819 3.545,0 l 3.261,-3.261 -1.562,-1.562 0.0064,-0.0069 c -0.03229,-0.02696 -0.06563,-0.05145 -0.09668,-0.08057 -1.118,-1.118 -1.118,-2.93 0,-4.048 1.118,-1.118 2.93,-1.118 4.048,0 0.02514,0.0363 0.04485,0.07241 0.0681,0.109 l 0.0196,-0.01948 1.562,1.562 z M 8.42,29.199 c -1.118,-1.118 -1.118,-2.93 -6.1e-6,-4.048 2.9840001,-2.057 5.7590001,2.228 4.0480001,4.048 -1.118,1.118 -2.9300001,1.118 -4.0480001,0 z m 5.697,-1.652 -1.562,1.562 -4.045,-4.045 1.562,-1.562 z"
+         style="stroke-width:1.58861" />
+    </g>
+  </g>
+</svg>
diff --git a/aleksis/core/tasks.py b/aleksis/core/tasks.py
index 13eb76444b77e84ca7509958f02517c98b678296..97ccfa2270b9d87ab8698414ce87c53f06446d22 100644
--- a/aleksis/core/tasks.py
+++ b/aleksis/core/tasks.py
@@ -40,3 +40,11 @@ def backup_data() -> None:
     # Hand off to dbbackup's management commands
     management.call_command("dbbackup", *db_options)
     management.call_command("mediabackup", *media_options)
+
+
+@app.task(run_every=timedelta(days=1))
+def clear_oauth_tokens():
+    """Clear expired OAuth2 tokens."""
+    from oauth2_provider.models import clear_tokens  # noqa
+
+    return clear_tokens()
diff --git a/aleksis/core/templates/core/partials/meta.html b/aleksis/core/templates/core/partials/meta.html
index cae0f93b801ab0157f61bbe319067ac49979002c..30b2ef099f0914f8ab68df34c047732293d91d5b 100644
--- a/aleksis/core/templates/core/partials/meta.html
+++ b/aleksis/core/templates/core/partials/meta.html
@@ -6,7 +6,7 @@
 <meta name="description" content="{{ SITE_PREFERENCES.general__description }}"/>
 <meta name="generator" content="AlekSIS School Information System"/>
 
-<meta name="theme-color" content="red">
+<meta name="theme-color" content="{{ SITE_PREFERENCES.theme__primary }}">
 <meta name="mobile-web-app-capable" content="yes">
 
 <meta name="apple-mobile-web-app-title" content="{{ SITE_PREFERENCES.general__title }}">
diff --git a/aleksis/core/tests/regression/test_regression.py b/aleksis/core/tests/regression/test_regression.py
index 7ac541531db385ea8e28b454dc487458a45d7a66..b9d33e6981312817324b92d4fe67176b96763cda 100644
--- a/aleksis/core/tests/regression/test_regression.py
+++ b/aleksis/core/tests/regression/test_regression.py
@@ -1,3 +1,12 @@
+from django.contrib.auth import get_user_model
+
+import pytest
+
+from aleksis.core.models import Group, Person
+
+pytestmark = pytest.mark.django_db
+
+
 def test_all_settigns_registered():
     """Tests for regressions of preferences not being registered.
 
@@ -33,3 +42,43 @@ def test_custom_managers_return_correct_qs():
         assert isinstance(Manager.from_queryset(QuerySet)().get_queryset(), QuerySet)
 
     _check_get_queryset(managers.GroupManager, managers.GroupQuerySet)
+
+
+def test_reassign_user_to_person():
+    """Tests that on re-assigning a user, groups are correctly synced.
+
+    https://edugit.org/AlekSIS/official/AlekSIS-Core/-/issues/628
+    """
+
+    User = get_user_model()
+
+    group1 = Group.objects.create(name="Group 1")
+    group2 = Group.objects.create(name="Group 2")
+
+    user1 = User.objects.create(username="user1")
+    user2 = User.objects.create(username="user2")
+
+    person1 = Person.objects.create(first_name="Person", last_name="1", user=user1)
+    person2 = Person.objects.create(first_name="Person", last_name="2", user=user2)
+
+    person1.member_of.set([group1])
+    person2.member_of.set([group2])
+
+    assert user1.groups.count() == 1
+    assert user2.groups.count() == 1
+    assert user1.groups.first().name == "Group 1"
+    assert user2.groups.first().name == "Group 2"
+
+    person1.user = None
+    person1.save()
+    assert user1.groups.count() == 0
+
+    person2.user = user1
+    person2.save()
+    person1.user = user2
+    person1.save()
+
+    assert user1.groups.count() == 1
+    assert user2.groups.count() == 1
+    assert user1.groups.first().name == "Group 2"
+    assert user2.groups.first().name == "Group 1"
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index ec38875ad876f6cc7f6b8943a70254a28704195b..539807bf19434b3ae812afe9fa8f317063be8654 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -175,6 +175,7 @@ class ManifestView(View):
             {
                 "src": favicon_img.faviconImage.url,
                 "sizes": f"{favicon_img.size}x{favicon_img.size}",
+                "purpose": "maskable" if prefs["theme__pwa_icon_maskable"] else "any",
             }
             for favicon_img in pwa_imgs
         ]
diff --git a/pyproject.toml b/pyproject.toml
index 9c6f2e14986568e10b42597cbb10948681006aec..0587d6373842c71e486667f13036971d2cd9cb72 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -113,7 +113,7 @@ django-allauth = "^0.47.0"
 django-uwsgi-ng = "^1.1.0"
 django-extensions = "^3.1.1"
 ipython = "^8.0.0"
-django-oauth-toolkit = "^1.6.2"
+django-oauth-toolkit = "^1.7.0"
 django-redis = "^5.0.0"
 django-storages = {version = "^1.11.1", optional = true}
 boto3 = {version = "^1.17.33", optional = true}