diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index d009032fe7601c866668a032b1043e0dd18c2418..76450ed9e6fdccb574a17ca23fde70e7ed0dbe7b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,21 +12,25 @@ Unreleased
 Added
 ~~~~~
 
+* Add option to open entry in new tab for sidebar navigation menu.
 * Add preference for configuring the default phone number country code.
-
-Added
-~~~~~
-
+* Persons and groups now have two image fields: official photo and public avatar
+* Admins recieve an mail for celery tasks with status "FAILURE"
 * OpenID Connect RSA keys can now be passed as string in config files
 * Views filtering for person names now also search the username of a linked user
+* OAuth2 applications now take an icon which is shown in the authorization progress.
+* Add support for hiding the main side nav in ``base.html``.
 
 Fixed
 ~~~~~
 
+* Changing the favicon did not result in all icons being replaced in some cases
 * GroupManager.get_queryset() returned an incomplete QuerySet
 * OAuth was broken by a non-semver-adhering django-oauth-toolkit update
 * Too long texts in chips didn't result in a larger chip.
+* The ``Person`` model had an ``is_active`` flag that was used in unclear ways; it is now removed
 * The data check results list view didn't work if a related object had been deleted in the meanwhile.
+* Socialaccount login template was not overriden
 
 Changed
 ~~~~~~~
@@ -41,6 +45,9 @@ Changed
 * [Docker] Base image now contains curl, grep, less, sed, and pspg
 * Views raising a 404 error can now customise the message that is displayed on the error page
 * OpenID Connect is enabled by default now, without RSA support
+* Login and authorization pages for OAuth2/OpenID Connect now indicate that the user is in progress
+  to authorize an external application.
+* Tables can be scrolled horizontally.
 
 `2.5`_ – 2022-01-02
 -------------------
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index 520a5fc435fe5cd8ef55435d3c680f978250abb0..47df1dbda191a75339a20fc6e1d86c032835a216 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -97,15 +97,16 @@ class CoreConfig(AppConfig):
             if name in ("primary", "secondary"):
                 clean_scss()
             elif name in ("favicon", "pwa_icon"):
-                from favicon.models import Favicon  # noqa
+                from favicon.models import Favicon, FaviconImg  # noqa
 
                 is_favicon = name == "favicon"
 
                 if new_value:
-                    Favicon.on_site.update_or_create(
+                    favicon_id = Favicon.on_site.update_or_create(
                         title=name,
                         defaults={"isFavicon": is_favicon, "faviconImage": new_value},
-                    )
+                    )[0]
+                    FaviconImg.objects.filter(faviconFK=favicon_id).delete()
                 else:
                     Favicon.on_site.filter(title=name, isFavicon=is_favicon).delete()
                     if name in settings.DEFAULT_FAVICON_PATHS:
diff --git a/aleksis/core/celery.py b/aleksis/core/celery.py
index 27a67c539babfc61701cc9521b42187ebebc5787..10457ea9b84871da3038cf4712babb601456bfcd 100644
--- a/aleksis/core/celery.py
+++ b/aleksis/core/celery.py
@@ -1,9 +1,36 @@
 import os
 
+from django.conf import settings
+
 from celery import Celery
+from celery.signals import task_failure
+from templated_email import send_templated_mail
+
+from .util.core_helpers import get_site_preferences
 
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings")
 
 app = Celery("aleksis")  # noqa
 app.config_from_object("django.conf:settings", namespace="CELERY")
 app.autodiscover_tasks()
+
+
+@task_failure.connect
+def task_failure_notifier(
+    sender=None, task_id=None, exception=None, args=None, traceback=None, **kwargs
+):
+    recipient_list = [e[1] for e in settings.ADMINS]
+    send_templated_mail(
+        template_name="celery_failure",
+        from_email=get_site_preferences()["mail__address"],
+        recipient_list=recipient_list,
+        context={
+            "task_name": sender.name,
+            "task": str(sender),
+            "task_id": str(task_id),
+            "exception": str(exception),
+            "args": str(args),
+            "kwargs": str(kwargs),
+            "traceback": str(traceback),
+        },
+    )
diff --git a/aleksis/core/filters.py b/aleksis/core/filters.py
index 288e1f899d7b930edb86fab73cf4661bfdd3ce8d..0bccc2664489f3698fc77bbe7299a9210ed81dac 100644
--- a/aleksis/core/filters.py
+++ b/aleksis/core/filters.py
@@ -73,11 +73,11 @@ class PersonFilter(FilterSet):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.form.layout = Layout(Row("name", "contact"), Row("is_active", "sex", "primary_group"))
+        self.form.layout = Layout(Row("name", "contact"), Row("sex", "primary_group"))
 
     class Meta:
         model = Person
-        fields = ["sex", "is_active", "primary_group"]
+        fields = ["sex", "primary_group"]
 
 
 class PermissionFilter(FilterSet):
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index 60a1058636dd23a2c93c94aec3ced4d1eed6e98c..4f4414b1b64219d95bfc4669f5f99cef670cc7c8 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -48,7 +48,6 @@ class PersonForm(ExtensibleForm):
             _("Base data"),
             "short_name",
             Row("user", "primary_group"),
-            "is_active",
             Row("first_name", "additional_name", "last_name"),
         ),
         Fieldset(_("Address"), Row("street", "housenumber"), Row("postal_code", "place")),
@@ -57,8 +56,7 @@ class PersonForm(ExtensibleForm):
             _("Advanced personal data"),
             Row("date_of_birth", "place_of_birth"),
             Row("sex"),
-            Row("photo", "photo_rect", "photo_square"),
-            Row("avatar", "avatar_rect", "avatar_square"),
+            Row("photo", "avatar"),
             "guardians",
         ),
     )
@@ -67,7 +65,6 @@ class PersonForm(ExtensibleForm):
         model = Person
         fields = [
             "user",
-            "is_active",
             "first_name",
             "last_name",
             "additional_name",
@@ -83,11 +80,7 @@ class PersonForm(ExtensibleForm):
             "place_of_birth",
             "sex",
             "photo",
-            "photo_rect",
-            "photo_square",
             "avatar",
-            "avatar_rect",
-            "avatar_square",
             "guardians",
             "primary_group",
         ]
@@ -164,8 +157,7 @@ class EditGroupForm(SchoolTermRelatedExtensibleForm):
         Fieldset(_("Common data"), "name", "short_name", "group_type"),
         Fieldset(_("Persons"), "members", "owners", "parent_groups"),
         Fieldset(_("Additional data"), "additional_fields"),
-        Fieldset(_("Photo"), "photo", "photo_rect", "photo_square"),
-        Fieldset(_("Avatar"), "avatar", "avatar_rect", "avatar_square"),
+        Fieldset(_("Photo"), "photo", "avatar"),
     )
 
     class Meta:
@@ -783,6 +775,7 @@ class OAuthApplicationForm(forms.ModelForm):
         model = OAuthApplication
         fields = (
             "name",
+            "icon",
             "client_id",
             "client_secret",
             "client_type",
diff --git a/aleksis/core/migrations/0029_photo_avatar_croppable.py b/aleksis/core/migrations/0029_photo_avatar_croppable.py
deleted file mode 100644
index 42deecf9283a2febbaa62689a4074ec14d2ac9d9..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0029_photo_avatar_croppable.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# Generated by Django 3.2.9 on 2021-12-09 14:56
-
-from django.db import migrations
-import image_cropping.fields
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0028_update_photo_avatar'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='group',
-            name='avatar_rect',
-            field=image_cropping.fields.ImageRatioField('photo', '800x600', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=False, verbose_name='avatar rect'),
-        ),
-        migrations.AddField(
-            model_name='group',
-            name='avatar_square',
-            field=image_cropping.fields.ImageRatioField('photo', '400x400', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=False, verbose_name='avatar square'),
-        ),
-        migrations.AddField(
-            model_name='group',
-            name='photo_rect',
-            field=image_cropping.fields.ImageRatioField('photo', '800x600', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=False, verbose_name='photo rect'),
-        ),
-        migrations.AddField(
-            model_name='group',
-            name='photo_square',
-            field=image_cropping.fields.ImageRatioField('photo', '400x400', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=False, verbose_name='photo square'),
-        ),
-        migrations.AddField(
-            model_name='person',
-            name='avatar_rect',
-            field=image_cropping.fields.ImageRatioField('photo', '600x800', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=False, verbose_name='avatar rect'),
-        ),
-        migrations.AddField(
-            model_name='person',
-            name='avatar_square',
-            field=image_cropping.fields.ImageRatioField('photo', '400x400', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=False, verbose_name='avatar square'),
-        ),
-        migrations.AddField(
-            model_name='person',
-            name='photo_rect',
-            field=image_cropping.fields.ImageRatioField('photo', '600x800', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=False, verbose_name='photo rect'),
-        ),
-        migrations.AddField(
-            model_name='person',
-            name='photo_square',
-            field=image_cropping.fields.ImageRatioField('photo', '400x400', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=False, verbose_name='photo square'),
-        ),
-        migrations.AlterField(
-            model_name='group',
-            name='photo',
-            field=image_cropping.fields.ImageCropField(blank=True, help_text='This is an official photo, used for official documents and for internal use cases.', null=True, upload_to='', verbose_name='Photo'),
-        ),
-        migrations.AlterField(
-            model_name='person',
-            name='photo',
-            field=image_cropping.fields.ImageCropField(blank=True, help_text='This is an official photo, used for official documents and for internal use cases.', null=True, upload_to='', verbose_name='Photo'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0031_oauthapplication_icon.py b/aleksis/core/migrations/0031_oauthapplication_icon.py
new file mode 100644
index 0000000000000000000000000000000000000000..26f2d6575e768578e653088c582fd3dadd147dc5
--- /dev/null
+++ b/aleksis/core/migrations/0031_oauthapplication_icon.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.11 on 2022-01-08 13:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0030_user_attributes'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='oauthapplication',
+            name='icon',
+            field=models.ImageField(blank=True, help_text='This image will be shown as icon in the authorization flow. It should be squared.', null=True, upload_to='', verbose_name='Icon'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0032_remove_person_is_active.py b/aleksis/core/migrations/0032_remove_person_is_active.py
new file mode 100644
index 0000000000000000000000000000000000000000..927913f3febb7ec5e791aab26bf56cd7b8aab2df
--- /dev/null
+++ b/aleksis/core/migrations/0032_remove_person_is_active.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.11 on 2022-01-08 10:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0031_oauthapplication_icon'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='person',
+            name='is_active',
+        ),
+    ]
diff --git a/aleksis/core/migrations/0028_update_photo_avatar.py b/aleksis/core/migrations/0033_update_photo_avatar.py
similarity index 96%
rename from aleksis/core/migrations/0028_update_photo_avatar.py
rename to aleksis/core/migrations/0033_update_photo_avatar.py
index 7dd0dba29c41d027e434e07b3f513b96393ab2d8..12dfa4fbc50a0119ed0e7fec0ce73a5e5cbc0f45 100644
--- a/aleksis/core/migrations/0028_update_photo_avatar.py
+++ b/aleksis/core/migrations/0033_update_photo_avatar.py
@@ -6,7 +6,7 @@ from django.db import migrations, models
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('core', '0027_person_place_of_birth'),
+        ('core', '0032_remove_person_is_active'),
     ]
 
     operations = [
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index a3909266b0bdc85d1168be59d3106a051607f24b..9c78211eac9dfabe6e1d0b8de8e2034f0dc9da2c 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -30,7 +30,6 @@ from cache_memoize import cache_memoize
 from django_celery_results.models import TaskResult
 from django_cte import CTEQuerySet, With
 from dynamic_preferences.models import PerInstancePreferenceModel
-from image_cropping import ImageCropField, ImageRatioField
 from invitations import signals
 from invitations.adapters import get_invitations_adapter
 from invitations.base_invitation import AbstractBaseInvitation
@@ -178,7 +177,6 @@ class Person(ExtensibleModel):
         related_name="person",
         verbose_name=_("Linked user"),
     )
-    is_active = models.BooleanField(verbose_name=_("Is person active?"), default=True)
 
     first_name = models.CharField(verbose_name=_("First name"), max_length=255)
     last_name = models.CharField(verbose_name=_("Last name"), max_length=255)
@@ -204,7 +202,7 @@ class Person(ExtensibleModel):
     place_of_birth = models.CharField(verbose_name=_("Place of birth"), max_length=255, blank=True)
     sex = models.CharField(verbose_name=_("Sex"), max_length=1, choices=SEX_CHOICES, blank=True)
 
-    photo = ImageCropField(
+    photo = models.ImageField(
         verbose_name=_("Photo"),
         blank=True,
         null=True,
@@ -212,8 +210,6 @@ class Person(ExtensibleModel):
             "This is an official photo, used for official documents and for internal use cases."
         ),
     )
-    photo_rect = ImageRatioField("photo", "600x800")
-    photo_square = ImageRatioField("photo", "400x400")
 
     avatar = models.ImageField(
         verbose_name=_("Display picture / Avatar"),
@@ -221,8 +217,6 @@ class Person(ExtensibleModel):
         null=True,
         help_text=_("This is a picture or an avatar for public display."),
     )
-    avatar_rect = ImageRatioField("photo", "600x800")
-    avatar_square = ImageRatioField("photo", "400x400")
 
     guardians = models.ManyToManyField(
         "self",
@@ -503,7 +497,7 @@ class Group(SchoolTermRelatedExtensibleModel):
         AdditionalField, verbose_name=_("Additional fields"), blank=True
     )
 
-    photo = ImageCropField(
+    photo = models.ImageField(
         verbose_name=_("Photo"),
         blank=True,
         null=True,
@@ -511,17 +505,12 @@ class Group(SchoolTermRelatedExtensibleModel):
             "This is an official photo, used for official documents and for internal use cases."
         ),
     )
-    photo_rect = ImageRatioField("photo", "800x600")
-    photo_square = ImageRatioField("photo", "400x400")
-
     avatar = models.ImageField(
         verbose_name=_("Display picture / Avatar"),
         blank=True,
         null=True,
         help_text=_("This is a picture or an avatar for public display."),
     )
-    avatar_rect = ImageRatioField("photo", "800x600")
-    avatar_square = ImageRatioField("photo", "400x400")
 
     def get_absolute_url(self) -> str:
         return reverse("group_by_id", args=[self.id])
@@ -1292,6 +1281,15 @@ class OAuthApplication(AbstractApplication):
         blank=True,
     )
 
+    icon = models.ImageField(
+        verbose_name=_("Icon"),
+        blank=True,
+        null=True,
+        help_text=_(
+            "This image will be shown as icon in the authorization flow. It should be squared."
+        ),
+    )
+
     def allows_grant_type(self, *grant_types: set[str]) -> bool:
         allowed_grants = get_site_preferences()["auth__oauth_allowed_grants"]
 
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 8e1207d490b839bbbdc9a7bda328451724779a83..05a460d13da4560a2aa8d07b18647e962800b925 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -6,7 +6,6 @@ from socket import getfqdn
 from django.utils.translation import gettext_lazy as _
 
 from dynaconf import LazySettings
-from easy_thumbnails.conf import Settings as thumbnail_settings
 
 from .util.core_helpers import get_app_packages, merge_app_settings, monkey_patch
 
@@ -147,8 +146,6 @@ INSTALLED_APPS = [
     "django_filters",
     "oauth2_provider",
     "rest_framework",
-    "easy_thumbnails",
-    "image_cropping",
 ]
 
 merge_app_settings("INSTALLED_APPS", INSTALLED_APPS, True)
@@ -978,12 +975,6 @@ if SENTRY_ENABLED:
         **SENTRY_SETTINGS,
     )
 
-THUMBNAIL_PROCESSORS = (
-    "image_cropping.thumbnail_processors.crop_corners",
-) + thumbnail_settings.THUMBNAIL_PROCESSORS
-
-IMAGE_CROPPING_JQUERY_URL = None
-
 SHELL_PLUS_MODEL_IMPORTS_RESOLVER = "django_extensions.collision_resolvers.AppLabelPrefixCR"
 SHELL_PLUS_APP_PREFIXES = {
     "auth": "auth",
diff --git a/aleksis/core/static/public/style.scss b/aleksis/core/static/public/style.scss
index f7d5b4ebc1f8a750e7caf913c6aa3d4c8e4ede9c..9d3a7f5609c20400d3addbd86e4e0e6efe965393 100644
--- a/aleksis/core/static/public/style.scss
+++ b/aleksis/core/static/public/style.scss
@@ -64,6 +64,10 @@ header, main, footer {
   margin-left: 300px;
 }
 
+.without-menu header, .without-menu main, .without-menu footer {
+  margin-left: 0;
+}
+
 @media only screen and (max-width: 992px) {
   header, main, footer {
     margin-left: 0;
@@ -421,6 +425,10 @@ span.badge .material-icons {
 
 /* Table*/
 
+.table-container {
+  overflow-x: auto;
+}
+
 table.striped > tbody > tr:nth-child(odd), table tr.striped, table tbody.striped tr {
   background-color: rgba(208, 208, 208, 0.5);
 }
@@ -909,4 +917,15 @@ $person-logo-size: 20vh;
       object-fit: contain;
     }
   }
+
+.application-circle {
+  border-radius: 50%;
+  width: 20vh;
+  height: 20vh;
+}
+
+
+.application-circle img {
+  @extend .application-circle;
+    object-fit: cover;
 }
diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html
index 06aebc4533369e9b7af39051dcb80713193f50d5..7fd4e38f8449b0a649fcd01e08051a2332070a15 100644
--- a/aleksis/core/templates/core/base.html
+++ b/aleksis/core/templates/core/base.html
@@ -58,7 +58,7 @@
 
   {% block extra_head %}{% endblock %}
 </head>
-<body>
+<body {% if no_menu %}class="without-menu"{% endif %}>
 
 <header>
   <!-- Menu button (sidenav) -->
@@ -88,32 +88,36 @@
   </nav>
 
   <!-- Main nav (sidenav) -->
-  <ul id="slide-out" class="sidenav sidenav-fixed">
-    <li class="logo">
-      {% static "img/aleksis-banner.svg" as aleksis_banner %}
-      <a id="logo-container" href="/" class="brand-logo">
-        <img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}"
-             alt="{{ request.site.preferences.general__title }} – Logo">
-      </a>
-    </li>
-    {% has_perm 'core.search_rule' user as search %}
-    {% if search %}
-      <li class="search">
-        <form method="get" action="{% url "haystack_search" %}" id="search-form" class="autocomplete">
-          <div class="search-wrapper">
-            <input id="search" name="q" type="search" enterkeyhint="search" placeholder="{% trans "Search" %}">
-            <button class="btn btn-flat search-button" type="submit" aria-label="{% trans "Search" %}">
-              <i class="material-icons">search</i>
-            </button>
-            <div class="progress" id="search-loader"><div class="indeterminate"></div></div>
-          </div>
-        </form>
+  {% if not no_menu %}
+    <ul id="slide-out" class="sidenav sidenav-fixed">
+      <li class="logo">
+        {% static "img/aleksis-banner.svg" as aleksis_banner %}
+        <a id="logo-container" href="/" class="brand-logo">
+          <img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}"
+               alt="{{ request.site.preferences.general__title }} – Logo">
+        </a>
       </li>
-    {% endif %}
-    <li class="no-padding">
-      {% include "core/partials/sidenav.html" %}
-    </li>
-  </ul>
+      {% has_perm 'core.search_rule' user as search %}
+      {% if search %}
+        <li class="search">
+          <form method="get" action="{% url "haystack_search" %}" id="search-form" class="autocomplete">
+            <div class="search-wrapper">
+              <input id="search" name="q" type="search" enterkeyhint="search" placeholder="{% trans "Search" %}">
+              <button class="btn btn-flat search-button" type="submit" aria-label="{% trans "Search" %}">
+                <i class="material-icons">search</i>
+              </button>
+              <div class="progress" id="search-loader">
+                <div class="indeterminate"></div>
+              </div>
+            </div>
+          </form>
+        </li>
+      {% endif %}
+      <li class="no-padding">
+        {% include "core/partials/sidenav.html" %}
+      </li>
+    </ul>
+  {% endif %}
 </header>
 
 
diff --git a/aleksis/core/templates/core/partials/sidenav.html b/aleksis/core/templates/core/partials/sidenav.html
index 54df4fe73e4642a693b63bbad76b7941fe142e76..9b3be97059ca834238d39a4dd6f6bfe35205a9d2 100644
--- a/aleksis/core/templates/core/partials/sidenav.html
+++ b/aleksis/core/templates/core/partials/sidenav.html
@@ -9,7 +9,7 @@
   {% for item in core_menu %}
     {% if not item.submenu %}
       <li class="{% if item.selected %} active {% endif %}">
-        <a class="truncate" href="{{ item.url }}">
+        <a class="truncate" {% if item.new_tab %} target="_blank" {% endif %} href="{{ item.url }}">
           {% if item.icon_class %}
             <i class="{{ item.icon_class }}"></i>
           {% elif item.icon %}
@@ -25,7 +25,7 @@
     {% endif %}
     {% if item.submenu %}
       <li class="bold {% if item.selected %} active {% endif %}">
-        <a class="collapsible-header waves-effect waves-primary truncate" href="{{ item.url|default:"#" }}">
+        <a class="collapsible-header waves-effect waves-primary truncate" {% if item.new_tab %} target="_blank" {% endif %} href="{{ item.url|default:"#" }}">
           {% if item.icon_class %}
             <i class="{{ item.icon_class }}"></i>
           {% elif item.icon %}
diff --git a/aleksis/core/templates/oauth2_provider/application/create.html b/aleksis/core/templates/oauth2_provider/application/create.html
index d81489e922a76de8d7a8e92a7f48686714a3b3a7..73b94677206d38fca163858551536d575b2dce3d 100644
--- a/aleksis/core/templates/oauth2_provider/application/create.html
+++ b/aleksis/core/templates/oauth2_provider/application/create.html
@@ -6,7 +6,7 @@
 {% block page_title %}{% blocktrans %}Register OAuth2 Application{% endblocktrans %}{% endblock %}
 
 {% block content %}
-  <form method="post">
+  <form method="post" enctype="multipart/form-data">
     {% csrf_token %}
     {% form form=form %}{% endform %}
     {% include "core/partials/save_button.html" %}
diff --git a/aleksis/core/templates/oauth2_provider/application/detail.html b/aleksis/core/templates/oauth2_provider/application/detail.html
index da6b8abf81ddc1916de7b0c7feb5a7262f453037..28e2af7d70ff4b6dd5d93b46b8ec52d31bc36538 100644
--- a/aleksis/core/templates/oauth2_provider/application/detail.html
+++ b/aleksis/core/templates/oauth2_provider/application/detail.html
@@ -22,6 +22,18 @@
   </a>
   <table class="responsive-table">
     <tbody>
+    <tr>
+      <th>{% trans "Icon" %}</th>
+      <td>
+        {% if application.icon %}
+          <div class="application-circle materialboxed z-depth-2">
+            <img src="{{ application.icon.url }}" alt="{{ oauth_application.name }}" class="hundred-percent">
+          </div>
+        {% else %}
+          –
+        {% endif %}
+      </td>
+    </tr>
     <tr>
       <th>
         {% trans "Client id" %}
diff --git a/aleksis/core/templates/oauth2_provider/application/edit.html b/aleksis/core/templates/oauth2_provider/application/edit.html
index 6755d2420fb6a181f671b825475afb4ec9581521..30f50fff94e330e941d7b4730fe7d875b039d74e 100644
--- a/aleksis/core/templates/oauth2_provider/application/edit.html
+++ b/aleksis/core/templates/oauth2_provider/application/edit.html
@@ -6,7 +6,7 @@
 {% block page_title %}{% blocktrans %}Edit OAuth2 Application{% endblocktrans %}{% endblock %}
 
 {% block content %}
-  <form method="post">
+  <form method="post" enctype="multipart/form-data">
     {% csrf_token %}
     {% form form=form %}{% endform %}
     {% include "core/partials/save_button.html" %}
diff --git a/aleksis/core/templates/oauth2_provider/application/list.html b/aleksis/core/templates/oauth2_provider/application/list.html
index 06f1a95c4e05c14dcb5efe69f2416040d184f0bb..ced7d718dfe1b561c2ea8253e25658a9f449024d 100644
--- a/aleksis/core/templates/oauth2_provider/application/list.html
+++ b/aleksis/core/templates/oauth2_provider/application/list.html
@@ -12,8 +12,13 @@
   </a>
   <div class="collection">
     {% for application in applications %}
-      <a class="collection-item" href="{% url "oauth2_application" application.id %}">
-        {{ application.name }}
+      <a class="collection-item avatar" href="{% url "oauth2_application" application.id %}">
+        {% if application.icon %}
+          <img src="{{ application.icon.url }}" alt="{{ application.name }}" class="circle">
+        {% endif %}
+        <span class="title">
+          {{ application.name }}
+        </span>
       </a>
       {% empty %}
       <div class="collection-item flow-text">
diff --git a/aleksis/core/templates/oauth2_provider/authorize.html b/aleksis/core/templates/oauth2_provider/authorize.html
index 48b996837b8721ef6ba74337d286236e91729f5b..c90d5e8dd9d3d72cff9e19ce167487900e904636 100644
--- a/aleksis/core/templates/oauth2_provider/authorize.html
+++ b/aleksis/core/templates/oauth2_provider/authorize.html
@@ -12,8 +12,15 @@
     <div class="col s12 m10 l8 xl6">
       <div class="card">
         <div class="card-content">
-          <div class="card-title">
-            {% trans "Authorize" %} {{ application.name }}
+          {% if application.icon %}
+            <div class="center-via-flex margin-bottom">
+              <div class="application-circle materialboxed z-depth-2">
+                <img src="{{ application.icon.url }}" alt="{{ application.name }}" class="hundred-percent">
+              </div>
+            </div>
+          {% endif %}
+          <div class="card-title {% if application.icon %}center{% endif %}">
+            {% blocktrans with name=application.name %}Authorize {{ name }}{% endblocktrans %}
           </div>
           <p class="margin-bottom">{% trans "The application requests access to the following scopes:" %}</p>
           {% for scope in scopes_descriptions %}
diff --git a/aleksis/core/templates/socialaccount/login.html b/aleksis/core/templates/socialaccount/login.html
new file mode 100644
index 0000000000000000000000000000000000000000..1630b01fa4104f563a92663bc7fd386a2abd5266
--- /dev/null
+++ b/aleksis/core/templates/socialaccount/login.html
@@ -0,0 +1,33 @@
+{% extends "core/base.html" %}
+
+{% load i18n material_form account %}
+
+{% block browser_title %}{% trans "Authorize" %}{% endblock %}
+{% block page_title %}{% trans "Authorize" %}{% endblock %}
+
+{% block content %}
+{% if process == "connect" %}
+    <p class="flow-text">
+       <i class="material-icons left">info</i>
+       {% blocktrans with provider.name as provider %}You are about to connect a new third party account from {{ provider }}.{% endblocktrans %}
+    </p>
+    <form method="post">
+      {% csrf_token %}
+      {% form form=form %}{% endform %}
+      {% trans "Confirm" as caption %}
+      {% include "core/partials/save_button.html" with caption=caption icon="how_to_reg" %}
+    </form>
+{% else %}
+    <p class="flow-text">
+       <i class="material-icons left small">info</i>
+       {% blocktrans with provider.name as provider %}You are about to sign in using a third party account from {{ provider }}.{% endblocktrans %}
+    </p>
+    <form method="post">
+      {% csrf_token %}
+      {% form form=form %}{% endform %}
+      {% trans "Continue" as caption %}
+      {% include "core/partials/save_button.html" with caption=caption icon="how_to_reg" %}
+    </form>
+
+{% endif %}
+{% endblock %}
diff --git a/aleksis/core/templates/templated_email/celery_failure.email b/aleksis/core/templates/templated_email/celery_failure.email
new file mode 100644
index 0000000000000000000000000000000000000000..d355823eb70e58f393c6a9006ed2045ac1882171
--- /dev/null
+++ b/aleksis/core/templates/templated_email/celery_failure.email
@@ -0,0 +1,44 @@
+{% load i18n %}
+
+{% block subject %}
+ {% blocktrans with task_name=task_name%} Celery task {{ task_name }} failed!{% endblocktrans %}
+{% endblock %}
+
+{% block plain %}
+ {% trans "Hello," %}
+ {% blocktrans with task_name=task_name %}
+   the celery task {{ task_name }} failed with following information:
+ {% endblocktrans %}
+
+
+{% blocktrans with task_name=task_name task=task task_id=task_id exception=exception args=args kwargs=kwargs traceback=traceback %}
+ * Task name: {{task_name}}
+ * Task: {{task}}
+ * Id of the task: {{task_id}}
+ * Exception instance raised: {{ exception }}
+ * Positional arguments the task was called with: {{ args }}
+ * Keyword arguments the task was called with: {{ kwargs }}
+ * Stack trace object: {{ traceback }}
+{% endblocktrans %}
+{% endblock %}
+
+{% block html %}
+ <p>{% trans "Hello," %}</p>
+ <p>
+  {% blocktrans with task_name=task_name %}
+    the celery task {{ task_name }} failed with following information:
+  {% endblocktrans %}
+ </p>
+
+ <ul>
+ {% blocktrans with task_name=task_name task=task task_id=task_id exception=exception args=args kwargs=kwargs traceback=traceback %}
+   <li>Task name: {{task_name}}</li>
+   <li>Task: {{task}}</li>
+   <li>Id of the task: {{task_id}}</li>
+   <li>Exception instance raised: {{ exception }}</li>
+   <li>Positional arguments the task was called with: {{ args }}</li>
+   <li>Keyword arguments the task was called with: {{ kwargs }}</li>
+   <li>Stack trace object: {{ traceback }}</li>
+ </ul>
+ {% endblocktrans %}
+{% endblock %}
diff --git a/aleksis/core/templates/two_factor/core/login.html b/aleksis/core/templates/two_factor/core/login.html
index 9ea4bc6c24b7d9e764742bbddf75ceb86b785868..834c4b98b543bd994542b105aec1b99932f7d186 100644
--- a/aleksis/core/templates/two_factor/core/login.html
+++ b/aleksis/core/templates/two_factor/core/login.html
@@ -16,7 +16,17 @@
       <div class="col s12 m10 l8 xl6">
         <div class="card">
           <div class="card-content">
-            {% if wizard.steps.current == 'auth' and socialaccount_providers %}
+            {% if oauth and oauth_application.icon %}
+              <div class="center-via-flex margin-bottom">
+                <div class="application-circle materialboxed z-depth-2">
+                  <img src="{{ oauth_application.icon.url }}" alt="{{ oauth_application.name }}"
+                       class="hundred-percent">
+                </div>
+              </div>
+              <div class="card-title center">
+                {% blocktrans with name=oauth_application.name %}Login for {{ name }}{% endblocktrans %}
+              </div>
+            {% elif wizard.steps.current == 'auth' and socialaccount_providers %}
               <div class="card-title">{% trans "Login with username and password" %}</div>
             {% else %}
               <div class="card-title">{% trans "Login" %}</div>
@@ -30,12 +40,21 @@
                 </p>
               </div>
             {% elif wizard.steps.current == 'auth' %}
-              <div class="alert primary">
-                <p>
-                  <i class="material-icons left">info</i>
-                  {% blocktrans %}Please login to see this page.{% endblocktrans %}
-                </p>
-              </div>
+              {% if oauth %}
+                <div class="alert primary">
+                  <p>
+                    <i class="material-icons left">info</i>
+                    {% blocktrans %}Please login with your account to use the external application.{% endblocktrans %}
+                  </p>
+                </div>
+              {% else %}
+                <div class="alert primary">
+                  <p>
+                    <i class="material-icons left">info</i>
+                    {% blocktrans %}Please login to see this page.{% endblocktrans %}
+                  </p>
+                </div>
+              {% endif %}
             {% endif %}
             {% if not wizard.steps.current == "auth" %}
               <div class="alert primary">
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 753d95085b6c8812927f2db376a6f3e2e6429c4b..b5dcb004d040b64b22242b977702a104fb2ea33a 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -134,6 +134,11 @@ urlpatterns = [
         views.OAuth2EditView.as_view(),
         name="edit_oauth2_application",
     ),
+    path(
+        "oauth/authorize/",
+        views.CustomAuthorizationView.as_view(),
+        name="oauth2_provider:authorize",
+    ),
     path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")),
     path("__i18n__/", include("django.conf.urls.i18n")),
     path(
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 23a4f35ded4e425a3ff0aa8342ec3266766f4a6f..2b261536d4bc2716fd082ae722284244ed8382cd 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -1,6 +1,6 @@
 from textwrap import wrap
 from typing import Any, Dict, Optional, Type
-from urllib.parse import urlencode
+from urllib.parse import urlencode, urlparse, urlunparse
 
 from django.apps import apps
 from django.conf import settings
@@ -18,6 +18,7 @@ from django.http import (
     HttpResponseRedirect,
     HttpResponseServerError,
     JsonResponse,
+    QueryDict,
 )
 from django.shortcuts import get_object_or_404, redirect, render
 from django.template import loader
@@ -50,6 +51,9 @@ from haystack.query import SearchQuerySet
 from haystack.utils.loading import UnifiedIndex
 from health_check.views import MainView
 from invitations.views import SendInvite, accept_invitation
+from oauth2_provider.exceptions import OAuthToolkitError
+from oauth2_provider.models import get_application_model
+from oauth2_provider.views import AuthorizationView
 from reversion import set_user
 from reversion.views import RevisionMixin
 from rules import test_rule
@@ -297,9 +301,7 @@ def persons(request: HttpRequest) -> HttpResponse:
     context = {}
 
     # Get all persons
-    persons = get_objects_for_user(
-        request.user, "core.view_person", Person.objects.filter(is_active=True)
-    )
+    persons = get_objects_for_user(request.user, "core.view_person", Person.objects.all())
 
     # Get filter
     persons_filter = PersonFilter(request.GET, queryset=persons)
@@ -341,7 +343,7 @@ def group(request: HttpRequest, id_: int) -> HttpResponse:
     group = Group.objects.get(pk=id_)
 
     # Get members
-    members = group.members.filter(is_active=True)
+    members = group.members.all()
 
     # Build table
     members_table = PersonsTable(members)
@@ -349,7 +351,7 @@ def group(request: HttpRequest, id_: int) -> HttpResponse:
     context["members_table"] = members_table
 
     # Get owners
-    owners = group.owners.filter(is_active=True)
+    owners = group.owners.all()
 
     # Build table
     owners_table = PersonsTable(owners)
@@ -1455,3 +1457,42 @@ class LoginView(AllAuthLoginView):
                 return render(self.request, "account/verification_sent.html")
 
         return super().done(form_list, **kwargs)
+
+    def get_context_data(self, form, **kwargs):
+        """Override context data to hide side menu and include OAuth2 application if given."""
+        context = super().get_context_data(form, **kwargs)
+        if self.request.GET.get("oauth"):
+            context["no_menu"] = True
+
+            if self.request.GET.get("client_id"):
+                application = get_application_model().objects.get(
+                    client_id=self.request.GET["client_id"]
+                )
+                context["oauth_application"] = application
+        return context
+
+
+class CustomAuthorizationView(AuthorizationView):
+    def handle_no_permission(self):
+        """Override handle_no_permission to provide OAuth2 information to login page."""
+        redirect_obj = super().handle_no_permission()
+
+        try:
+            scopes, credentials = self.validate_authorization_request(self.request)
+        except OAuthToolkitError as error:
+            # Application is not available at this time.
+            return self.error_response(error, application=None)
+
+        login_url_parts = list(urlparse(redirect_obj.url))
+        querystring = QueryDict(login_url_parts[4], mutable=True)
+        querystring["oauth"] = "yes"
+        querystring["client_id"] = credentials["client_id"]
+        login_url_parts[4] = querystring.urlencode(safe="/")
+
+        return HttpResponseRedirect(urlunparse(login_url_parts))
+
+    def get_context_data(self, **kwargs):
+        """Override context data to hide side menu."""
+        context = super().get_context_data(**kwargs)
+        context["no_menu"] = True
+        return context
diff --git a/docs/_static/create_social_application.png b/docs/_static/create_social_application.png
new file mode 100644
index 0000000000000000000000000000000000000000..c28c5c30a6d71f8aa0f1177b92048449c688d113
Binary files /dev/null and b/docs/_static/create_social_application.png differ
diff --git a/docs/admin/03_socialaccounts.rst b/docs/admin/03_socialaccounts.rst
new file mode 100644
index 0000000000000000000000000000000000000000..97b023947a27768b825c693b7c5cd8bff938a49e
--- /dev/null
+++ b/docs/admin/03_socialaccounts.rst
@@ -0,0 +1,34 @@
+Social accounts
+===============
+
+AlekSIS can authenticate users against third party applications using OAuth2
+or OpenID.
+
+
+.. warning::
+  Social accounts are **not** working with two factor authentication! If a user
+  authenticates with a social account, the two factor authentication is
+  ignored on login (but enforced for views that require two factor authentication later).
+
+Configuring social account provider
+-----------------------------------
+
+For available providers, see documentation of `django-allauth
+<https://django-allauth.readthedocs.io/en/latest/providers.html>`_.
+
+A new social account provider can be configured in your configuration file
+(located in ``/etc/aleksis/``).
+
+Configuration example::
+
+  [auth.providers.gitlab]
+  GITLAB_URL = "https://gitlab.exmaple.com"
+
+After configuring a new auth provider, you have to restart AlekSIS and configure client id and secret in the Backend Admin interface.
+Click "Social applications" and add a new application. Choose your
+provider and enter client id and secret from your application and choose
+your site:
+
+.. image:: ../_static/create_social_application.png
+  :width: 400
+  :alt: Create social application
diff --git a/docs/dev/04_materialize_templates.rst b/docs/dev/04_materialize_templates.rst
index 3e7979cf1d39c317bb02832d43c84ab970b9d7a1..882604168d04ae6fd6085626305844a8648405e7 100644
--- a/docs/dev/04_materialize_templates.rst
+++ b/docs/dev/04_materialize_templates.rst
@@ -111,7 +111,6 @@ In your ``forms.py`` you can configure the layout of the fields like in the Edit
             _("Base data"),
             "short_name",
             Row("user", "primary_group"),
-            "is_active",
             Row("first_name", "additional_name", "last_name"),
         ),
         Fieldset(_("Address"), Row("street", "housenumber"), Row("postal_code", "place")),
@@ -136,4 +135,4 @@ After you've loaded the template tag, you can simply generate the table like thi
 
 .. _MaterializeCSS: https://materializecss.com/
 .. _django-material: https://pypi.org/project/django-material/
-.. _Extended Navbar with Tabs: https://materializecss.com/navbar.html#navbar-tabs
\ No newline at end of file
+.. _Extended Navbar with Tabs: https://materializecss.com/navbar.html#navbar-tabs
diff --git a/pyproject.toml b/pyproject.toml
index 70390be32989431319a6a0970e45966b608252c3..cd6804b8e8f908de61abea814e7ed27087f2594d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -98,7 +98,7 @@ django-dbbackup = "^3.3.0"
 spdx-license-list = "^0.5.0"
 license-expression = "^21.6"
 django-reversion = "^4.0.0"
-django-favicon-plus-reloaded = "^1.1.2"
+django-favicon-plus-reloaded = "^1.1.5"
 django-health-check = "^3.12.1"
 psutil = "^5.7.0"
 celery-progress = "^0.1.0"
@@ -125,8 +125,6 @@ python-gnupg = "^0.4.7"
 sentry-sdk = {version = "^1.4.3", optional = true}
 django-cte = "^1.1.5"
 pycountry = "^20.7.3"
-django-image-cropping = "^1.6.1"
-easy-thumbnails = "^2.8"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]