diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index aeda609326941428e483e05cabd6e5412b2832f7..030e56d6da582a522443a26a912be0af37eca091 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -1,6 +1,8 @@
 from typing import Any, List, Optional, Tuple
 
 import django.apps
+from django.conf import settings
+from django.db import ProgrammingError
 from django.http import HttpRequest
 from django.utils.module_loading import autodiscover_modules
 
@@ -12,7 +14,7 @@ from .registries import (
     site_preferences_registry,
 )
 from .util.apps import AppConfig
-from .util.core_helpers import has_person
+from .util.core_helpers import get_site_preferences, has_person, lazy_preference
 from .util.sass_helpers import clean_scss
 
 
@@ -48,6 +50,26 @@ class CoreConfig(AppConfig):
         preference_models.register(personpreferencemodel, person_preferences_registry)
         preference_models.register(grouppreferencemodel, group_preferences_registry)
 
+        self._refresh_authentication_backends()
+
+    @classmethod
+    def _refresh_authentication_backends(cls):
+        """Refresh config list of enabled authentication backends."""
+        from .preferences import AuthenticationBackends  # noqa
+
+        idx = settings.AUTHENTICATION_BACKENDS.index("django.contrib.auth.backends.ModelBackend")
+
+        try:
+            # Don't set array directly in order to keep object reference
+            settings._wrapped.AUTHENTICATION_BACKENDS.clear()
+            settings._wrapped.AUTHENTICATION_BACKENDS += settings.ORIGINAL_AUTHENTICATION_BACKENDS
+
+            for backend in get_site_preferences()["auth__backends"]:
+                settings._wrapped.AUTHENTICATION_BACKENDS.insert(idx, backend)
+                idx += 1
+        except ProgrammingError:
+            pass
+
     def preference_updated(
         self,
         sender: Any,
@@ -57,6 +79,9 @@ class CoreConfig(AppConfig):
         new_value: Optional[Any] = None,
         **kwargs,
     ) -> None:
+        if section == "auth" and name == "backends":
+            self._refresh_authentication_backends()
+
         if section == "theme":
             if name in ("primary", "secondary"):
                 clean_scss()
diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py
index 6693aff1fba605180b6f83e7741fe1cb005267fe..f0aeb001062b0a1c0fc49d3bf8fcf6f25da67d32 100644
--- a/aleksis/core/preferences.py
+++ b/aleksis/core/preferences.py
@@ -3,7 +3,12 @@ from django.forms import EmailField, ImageField, URLField
 from django.utils.translation import gettext_lazy as _
 
 from dynamic_preferences.preferences import Section
-from dynamic_preferences.types import ChoicePreference, FilePreference, StringPreference
+from dynamic_preferences.types import (
+    ChoicePreference,
+    FilePreference,
+    MultipleChoicePreference,
+    StringPreference,
+)
 
 from .models import Person
 from .registries import person_preferences_registry, site_preferences_registry
@@ -16,6 +21,7 @@ mail = Section("mail")
 notification = Section("notification")
 footer = Section("footer")
 account = Section("account")
+auth = Section("auth", verbose_name=_("Authentication"))
 
 
 @site_preferences_registry.register
@@ -178,3 +184,14 @@ class SchoolNameOfficial(StringPreference):
     default = ""
     required = False
     verbose_name = _("Official name of the school, e.g. as given by supervisory authority")
+
+
+@site_preferences_registry.register
+class AuthenticationBackends(MultipleChoicePreference):
+    section = auth
+    name = "backends"
+    default = None
+    verbose_name = _("Enabled custom authentication backends")
+
+    def get_choices(self):
+        return [(b, b) for b in settings.CUSTOM_AUTHENTICATION_BACKENDS]
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index e6de512b3be12e211fbc7f91453848f326e17da3..cf5adee484c91c5bc96ddf76d24f48a65f331e86 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -282,10 +282,17 @@ if _settings.get("ldap.uri", None):
                 "is_superuser"
             ]
 
+CUSTOM_AUTHENTICATION_BACKENDS = []
+merge_app_settings("AUTHENTICATION_BACKENDS", CUSTOM_AUTHENTICATION_BACKENDS)
+
 # Add ModelBckend last so all other backends get a chance
 # to verify passwords first
 AUTHENTICATION_BACKENDS.append("django.contrib.auth.backends.ModelBackend")
 
+# Structure of items: backend, URL name, icon name, button title
+ALTERNATIVE_LOGIN_VIEWS = []
+merge_app_settings("ALTERNATIVE_LOGIN_VIEWS", ALTERNATIVE_LOGIN_VIEWS, True)
+
 # Internationalization
 # https://docs.djangoproject.com/en/2.1/topics/i18n/
 
@@ -683,3 +690,5 @@ HEALTH_CHECK = {
     "DISK_USAGE_MAX": _settings.get("health.disk_usage_max_percent", 90),
     "MEMORY_MIN": _settings.get("health.memory_min_mb", 500),
 }
+
+ORIGINAL_AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS[:]
diff --git a/aleksis/core/templates/core/partials/alternative_login_options.html b/aleksis/core/templates/core/partials/alternative_login_options.html
new file mode 100644
index 0000000000000000000000000000000000000000..24d36e9ef76f3da11554169106fc1eafd3c7ac46
--- /dev/null
+++ b/aleksis/core/templates/core/partials/alternative_login_options.html
@@ -0,0 +1,10 @@
+{% if ALTERNATIVE_LOGIN_VIEWS %}
+  <p>
+    {% for backend, url, icon, text in ALTERNATIVE_LOGIN_VIEWS %}
+      <a class="btn-large waves-effect waves-light primary" href="{% url url %}">
+        <i class="material-icons left">{{ icon }}</i>
+        {{ text }}
+      </a>
+    {% endfor %}
+  </p>
+{% endif %}
diff --git a/aleksis/core/templates/two_factor/core/login.html b/aleksis/core/templates/two_factor/core/login.html
index 6e0df1a4e4fa35a411f9e71cb41868808c942045..0e62194597024dbb613d2c13669513c33dcb9d05 100644
--- a/aleksis/core/templates/two_factor/core/login.html
+++ b/aleksis/core/templates/two_factor/core/login.html
@@ -16,14 +16,26 @@
         {% blocktrans %}You have no permission to view this page. Please login with an other account.{% endblocktrans %}
       </p>
     </div>
-  {% else %}
+  {% elif wizard.steps.current == 'auth' %}
     <div class="alert primary">
       <p>
         <i class="material-icons left">info</i>
 
-        {% if wizard.steps.current == 'auth' %}
-          {% blocktrans %}Please login to see this page.{% endblocktrans %}
-        {% elif wizard.steps.current == 'token' %}
+        {% blocktrans %}Please login to see this page.{% endblocktrans %}
+      </p>
+    </div>
+  {% endif %}
+
+  {% if wizard.steps.current == 'auth' and ALTERNATIVE_LOGIN_VIEWS %}
+    <h5>{% trans "Login with username and password" %}</h5>
+  {% endif %}
+
+  {% if not  wizard.steps.current == "auth" %}
+    <div class="alert primary">
+      <p>
+        <i class="material-icons left">info</i>
+
+        {% if wizard.steps.current == 'token' %}
           {% if device.method == 'call' %}
             {% blocktrans %}We are calling your phone right now, please enter the
               digits you hear.{% endblocktrans %}
@@ -70,7 +82,16 @@
       </p>
     {% endif %}
 
-    <button type="submit" class="btn green waves-effect waves-light">{% trans "Login" %}</button>
+    <button type="submit" class="btn green waves-effect waves-light">
+      {% trans "Login" %}
+      <i class="material-icons right">send</i>
+    </button>
 
   </form>
+
+  {% if wizard.steps.current == 'auth' and ALTERNATIVE_LOGIN_VIEWS %}
+    <h5>{% trans "Use alternative login options" %}</h5>
+    {% include "core/partials/alternative_login_options.html" %}
+  {% endif %}
+
 {% endblock %}
diff --git a/aleksis/core/tests/test_authentication_backends.py b/aleksis/core/tests/test_authentication_backends.py
new file mode 100644
index 0000000000000000000000000000000000000000..227f48c1469d97f246132a4bfbef2f5af5f4dbb9
--- /dev/null
+++ b/aleksis/core/tests/test_authentication_backends.py
@@ -0,0 +1,64 @@
+from typing import Optional
+
+from django.contrib.auth import authenticate
+from django.contrib.auth.backends import BaseBackend
+from django.contrib.auth.base_user import AbstractBaseUser
+from django.contrib.auth.models import User
+
+import pytest
+
+from aleksis.core.apps import CoreConfig
+from aleksis.core.util.core_helpers import get_site_preferences
+
+pytestmark = pytest.mark.django_db
+
+
+class DummyBackend(BaseBackend):
+    def authenticate(
+        self, request, username: str, password: str, **kwargs
+    ) -> Optional[AbstractBaseUser]:
+        if username == "foo" and password == "baz":
+            return User.objects.get_or_create(username="foo")[0]
+
+
+backend_name = "aleksis.core.tests.test_authentication_backends.DummyBackend"
+
+
+def test_backends_simple(settings):
+
+    assert not authenticate(username="foo", password="baz")
+
+    assert backend_name not in settings.AUTHENTICATION_BACKENDS
+
+    settings.AUTHENTICATION_BACKENDS.append(backend_name)
+    assert backend_name in settings.AUTHENTICATION_BACKENDS
+
+    assert authenticate(username="foo", password="baz")
+
+    settings.AUTHENTICATION_BACKENDS.remove(backend_name)
+
+    assert not authenticate(username="foo", password="baz")
+
+
+def test_backends_with_activation(settings):
+    assert not authenticate(username="foo", password="baz")
+
+    settings.CUSTOM_AUTHENTICATION_BACKENDS.append(backend_name)
+
+    assert backend_name not in get_site_preferences()["auth__backends"]
+    assert backend_name not in settings.AUTHENTICATION_BACKENDS
+    assert not authenticate(username="foo", password="baz")
+
+    print(get_site_preferences()["auth__backends"])
+    print(get_site_preferences()["auth__backends"].append(backend_name))
+
+    get_site_preferences()["auth__backends"] = [backend_name]
+
+    assert backend_name in get_site_preferences()["auth__backends"]
+    assert backend_name in settings.AUTHENTICATION_BACKENDS
+    assert authenticate(username="foo", password="baz")
+
+    get_site_preferences()["auth__backends"] = []
+
+    assert backend_name not in get_site_preferences()["auth__backends"]
+    assert backend_name not in settings.AUTHENTICATION_BACKENDS
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 29711935cf420f1af690aeae6a13c1bcc49f97b5..d188f6cf29c6909809ec03f81a7601dbcc41c4f9 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -323,6 +323,14 @@ def custom_information_processor(request: HttpRequest) -> dict:
 
     return {
         "FOOTER_MENU": CustomMenu.get_default("footer"),
+        "ALTERNATIVE_LOGIN_VIEWS_LIST": [
+            a[0]
+            for a in settings.ALTERNATIVE_LOGIN_VIEWS
+            if a[0] in settings.AUTHENTICATION_BACKENDS
+        ],
+        "ALTERNATIVE_LOGIN_VIEWS": [
+            a for a in settings.ALTERNATIVE_LOGIN_VIEWS if a[0] in settings.AUTHENTICATION_BACKENDS
+        ],
     }