diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c13f3964e44cab97fb4a26f7e60c2dd5b2e98421..373ae44ccda6be3c72bb809a8ac2821fc69dcecb 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -19,6 +19,12 @@ Changed
 ~~~~~~~
 
 * Rewrite of frontend using Vuetify
+* OIDC scope "profile" now exposes the avatar instead of the official photo
+* Based on Django 4.0
+  * Use built-in Redis cache backend
+  * Introduce PBKDF2-SHA1 password hashing
+* Persistent database connections are now health-checked as to not fail
+  requests
 * Incorporate SPDX license list for app licenses on About page
 * [Dev] The undocumented field `check` on `DataCheckResult` was renamed to `data_check`
 
diff --git a/aleksis/core/__init__.py b/aleksis/core/__init__.py
index 66d1ef788b034aea3d1518bc009e5be0ca05b3f2..df69a63b63a08043dd6de7a3344eee787f3acfd1 100644
--- a/aleksis/core/__init__.py
+++ b/aleksis/core/__init__.py
@@ -6,5 +6,3 @@ try:
     __version__ = metadata.distribution("AlekSIS-Core").version
 except Exception:
     __version__ = "unknown"
-
-default_app_config = "aleksis.core.apps.CoreConfig"
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index 6b54f8e40a1873a9ba35379136bc0c9723889d93..f4ae9a9d0c28f3539b1f4359785db555557c2bbc 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -6,7 +6,7 @@ from typing import Any, Callable, List, Optional, Union
 
 from django.conf import settings
 from django.contrib import messages
-from django.contrib.auth.views import LoginView, SuccessURLAllowedHostsMixin
+from django.contrib.auth.views import LoginView, RedirectURLMixin
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.sites.managers import CurrentSiteManager
 from django.contrib.sites.models import Site
@@ -465,7 +465,7 @@ class SuccessMessageMixin(ModelFormMixin):
         return super().form_valid(form)
 
 
-class SuccessNextMixin(SuccessURLAllowedHostsMixin):
+class SuccessNextMixin(RedirectURLMixin):
     redirect_field_name = "next"
 
     def get_success_url(self) -> str:
@@ -492,8 +492,8 @@ class AdvancedDeleteView(DeleteView):
 
     success_message: Optional[str] = None
 
-    def delete(self, request, *args, **kwargs):
-        r = super().delete(request, *args, **kwargs)
+    def form_valid(self, form):
+        r = super().form_valid(form)
         if self.success_message:
             messages.success(self.request, self.success_message)
         return r
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 435169a49f867cc92d8e9c552eec14ec35d041fd..daafab787c5112d393e5e547e7ba0638149efd64 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -68,6 +68,8 @@ BASE_URL = _settings.get(
     "http.base_url", "http://localhost:8000" if DEBUG else f"https://{ALLOWED_HOSTS[0]}"
 )
 
+CSRF_TRUSTED_ORIGINS = _settings.get("http.trusted_origins", [])
+
 # Application definition
 INSTALLED_APPS = [
     "django.contrib.admin",
@@ -128,7 +130,6 @@ INSTALLED_APPS = [
     "material",
     "ckeditor",
     "ckeditor_uploader",
-    "django_js_reverse",
     "colorfield",
     "django_bleach",
     "favicon",
@@ -212,6 +213,7 @@ DATABASES = {
         "HOST": _settings.get("database.host", "127.0.0.1"),
         "PORT": _settings.get("database.port", "5432"),
         "CONN_MAX_AGE": _settings.get("database.conn_max_age", None),
+        "CONN_HEALTH_CHECK": True,
         "OPTIONS": _settings.get("database.options", {}),
     }
 }
@@ -225,6 +227,12 @@ DATABASE_OOT_LABELS = ["django_celery_results"]
 
 merge_app_settings("DATABASES", DATABASES, False)
 
+PASSWORD_HASHERS = [
+    "django.contrib.auth.hashers.ScryptPasswordHasher",
+    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
+    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
+]
+
 REDIS_HOST = _settings.get("redis.host", "localhost")
 REDIS_PORT = _settings.get("redis.port", 6379)
 REDIS_DB = _settings.get("redis.database", 0)
@@ -239,15 +247,10 @@ REDIS_URL = (
 if _settings.get("caching.redis.enabled", not IN_PYTEST):
     CACHES = {
         "default": {
-            "BACKEND": "django_redis.cache.RedisCache",
+            "BACKEND": "django.core.cache.backends.redis.RedisCache",
             "LOCATION": _settings.get("caching.redis.address", REDIS_URL),
-            "OPTIONS": {
-                "CLIENT_CLASS": "django_redis.client.DefaultClient",
-            },
         }
     }
-    if REDIS_PASSWORD:
-        CACHES["default"]["OPTIONS"]["PASSWORD"] = REDIS_PASSWORD
 else:
     CACHES = {
         "default": {
@@ -529,16 +532,14 @@ LANGUAGES = [
 ]
 LANGUAGE_CODE = _settings.get("l10n.lang", "en")
 TIME_ZONE = _settings.get("l10n.tz", "UTC")
-USE_I18N = True
-USE_L10N = True
 USE_TZ = True
 
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/2.1/howto/static-files/
 
 
-STATIC_URL = _settings.get("static.url", "/static/")
-MEDIA_URL = _settings.get("media.url", "/media/")
+STATIC_URL = _settings.get("static.url", "static/")
+MEDIA_URL = _settings.get("media.url", "media/")
 
 LOGIN_REDIRECT_URL = "index"
 LOGOUT_REDIRECT_URL = "index"
diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html
index 582a709d2f3c0a6935ebb218de7ae117336fa7bc..6b9ff71ab8ca354166ec5436ac682f498974f798 100644
--- a/aleksis/core/templates/core/base.html
+++ b/aleksis/core/templates/core/base.html
@@ -26,9 +26,6 @@
   {% include_css "Roboto900" %}
   <link rel="stylesheet" href="{% sass_src 'public/style.scss' %}">
 
-  {# Add JS URL resolver #}
-  <script src="{% url "js_reverse" %}" type="text/javascript"></script>
-
   {# Add i18n names for calendar (for use in datepicker) #}
   {# Passing the locale is not necessary for the scripts to work, but prevents caching issues #}
   <script src="{% url "javascript-catalog" %}?locale={{ LANGUAGE_CODE }}" type="text/javascript"></script>
diff --git a/aleksis/core/templates/core/vue_base.html b/aleksis/core/templates/core/vue_base.html
index 85abd8e79d4752aa79e771c5d251dfb1fe450f53..261c3bb81e9fd24b33726afe369b239546e31faf 100644
--- a/aleksis/core/templates/core/vue_base.html
+++ b/aleksis/core/templates/core/vue_base.html
@@ -26,9 +26,6 @@
   {% include_css "Roboto700" %}
   {% include_css "Roboto900" %}
 
-  {# Add JS URL resolver #}
-  <script src="{% url "js_reverse" %}" type="text/javascript"></script>
-
   {# Add i18n names for calendar (for use in datepicker) #}
   {# Passing the locale is not necessary for the scripts to work, but prevents caching issues #}
   <script src="{% url "javascript-catalog" %}?locale={{ LANGUAGE_CODE }}" type="text/javascript"></script>
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index b23b6f1d98a5b54322a6fec86b67d4bf21b900eb..806a284ac9626dbe516f8910e72e6b38775b6435 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -8,7 +8,6 @@ from django.views.i18n import JavaScriptCatalog
 
 import calendarweek.django
 from ckeditor_uploader import views as ckeditor_uploader_views
-from django_js_reverse.views import urls_js
 from health_check.urls import urlpatterns as health_urls
 from oauth2_provider.views import ConnectDiscoveryInfoView
 from rules.contrib.views import permission_required
@@ -155,7 +154,6 @@ urlpatterns = [
         name="ckeditor_browse",
     ),
     path("select2/", include("django_select2.urls")),
-    path("jsreverse.js", urls_js, name="js_reverse"),
     path("calendarweek_i18n.js", calendarweek.django.i18n_js, name="calendarweek_i18n_js"),
     path("gettext.js", JavaScriptCatalog.as_view(), name="javascript-catalog"),
     path(
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index 7bb7f7bdab7ac4675747f3b5fc4e75ab3760f697..0889280858a64a61cd8857a0c6eef4f872012cfc 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -20,8 +20,13 @@ if TYPE_CHECKING:
 class AppConfig(django.apps.AppConfig):
     """An extended version of DJango's AppConfig container."""
 
+    default = False
     default_auto_field = "django.db.models.BigAutoField"
 
+    def __init_subclass__(cls):
+        super().__init_subclass__()
+        cls.default = True
+
     def ready(self):
         super().ready()
 
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 6f01d8e53b4ccad0cf9bccbce8fc4bc2b2b65008..bbbe7cc1871735e006c64f0fd8726d39312ec6a1 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -1380,9 +1380,8 @@ class SocialAccountDeleteView(DeleteView):
     def get_queryset(self):
         return SocialAccount.objects.filter(user=self.request.user)
 
-    def delete(self, request, *args, **kwargs):
+    def form_valid(self, form):
         self.object = self.get_object()
-        success_url = self.get_success_url()
         try:
             get_adapter(self.request).validate_disconnect(
                 self.object, SocialAccount.objects.filter(user=self.request.user)
@@ -1400,7 +1399,7 @@ class SocialAccountDeleteView(DeleteView):
             messages.success(
                 self.request, _("The third-party account has been successfully disconnected.")
             )
-        return HttpResponseRedirect(success_url)
+        return super().form_valid()
 
 
 def server_error(
diff --git a/pyproject.toml b/pyproject.toml
index 499a246b828687e3ccce563073a1219642266297..c9a5f25b70fabae18809434113aeed5cdef4a1d7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,7 +40,7 @@ keywords = ["SIS", "education", "school", "digitisation", "school apps"]
 classifiers = [
     "Development Status :: 5 - Production/Stable",
     "Environment :: Web Environment",
-    "Framework :: Django :: 3.0",
+    "Framework :: Django :: 4.0",
     "Intended Audience :: Developers",
     "Intended Audience :: Education",
     "Topic :: Education",
@@ -56,7 +56,7 @@ secondary = true
 
 [tool.poetry.dependencies]
 python = "^3.9"
-Django = "^3.2.5"
+Django = "^4.1"
 django-any-js = "^1.1"
 django-menu-generator-ng = "^1.2.3"
 django-tables2 = "^2.1"
@@ -80,7 +80,6 @@ django-filter = "^2.2.0"
 django-templated-email = "^3.0.0"
 html2text = "^2020.0.0"
 django-ckeditor = "^6.0.0"
-django-js-reverse = "^0.9.1"
 calendarweek = "^0.5.0"
 Celery = {version="^5.2", extras=["django", "redis"]}
 django-celery-results = "^2.0.1"
@@ -95,7 +94,7 @@ rules = "^3.0"
 django-cache-memoize = "^0.1.6"
 django-haystack = "^3.1"
 celery-haystack-ng = "^2.0"
-django-dbbackup = "^3.3.0"
+django-dbbackup = "^4.0.0"
 license-expression = "^30.0"
 django-reversion = "^5.0.0"
 django-favicon-plus-reloaded = "^1.1.5"
@@ -109,7 +108,7 @@ bs4 = "^0.0.1"
 django-invitations = "^2.0.0"
 django-cleavejs = "^0.1.0"
 django-allauth = "^0.51.0"
-django-uwsgi-ng = "^1.1.0"
+django-uwsgi-ng = "^2.0"
 django-extensions = "^3.1.1"
 ipython = "^8.0.0"
 django-oauth-toolkit = "^2.0.0"