diff --git a/examples/config.py b/examples/config.py
index f3cc2d0102c07c52075bda428119524811de163e..ae122ffbb16f2d78095fa4085d5732f7fde6c491 100644
--- a/examples/config.py
+++ b/examples/config.py
@@ -1,3 +1,11 @@
+# Import as needed
+from onelogin.saml2.idp_metadata_parser import (
+    OneLogin_Saml2_IdPMetadataParser as IdPMetadataParser,
+)
+from deepmerge import always_merger
+from pathlib import Path
+
+
 SQLALCHEMY_DATABASE_URI = "postgresql+psycopg:///schilder2000"
 # To generate a secret key:
 #   % python -c 'import secrets; print(secrets.token_hex())'
@@ -11,3 +19,107 @@ PRINTERS = {
     "Office": "ipps://printserver.example.com:443/printers/Office",
     "Kitchen": "ipp://kitchenprinter.local:631/ipp/print",
 }
+
+REQUIRE_LOGIN = True
+
+
+# See upstream documentation for reference:
+# https://flask-multipass.readthedocs.io
+
+_ldap_config = {
+    "uri": "ldaps://dc.example.org:636",
+    "bind_dn": "CN=schilder2000,CN=Service Accounts,CN=Users,DC=example,DC=org",
+    "bind_password": "hunter2",
+    "timeout": 30,
+    "verify_cert": True,
+    # optional: if not present, uses certifi's CA bundle (if installed)
+    # "cert_file": "path/to/server/cert",
+    "starttls": False,
+    "page_size": 1000,
+    "uid": "sAMAccountName",
+    "user_base": "CN=Users,DC=example,DC=org",
+    "user_filter": "(objectCategory=person)",
+}
+
+_saml_config = always_merger.merge(
+    IdPMetadataParser.parse_remote(
+        "https://idp.example.org/realms/owca/protocol/saml/descriptor"
+    ),
+    {
+        "debug": False,
+        "sp": {
+            "entityId": "https://schilder2000.example.org/multipass/saml/fsmpi-saml/metadata",
+            "x509cert": (Path(__file__).parent / "saml-cert.pem").read_text(),
+            "privateKey": (Path(__file__).parent / "saml-key.pem").read_text(),
+            # We don’t use the name anyway
+            "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
+        },
+        "security": {
+            # Keycloak wants this, even though it doesn’t say so
+            "logoutRequestSigned": True,
+        },
+    },
+)
+
+_oidc_config = {
+    "client_id": "schilder2000",
+    "client_secret": "hunter2",
+    "server_metadata_url": "https://idp.example.org/realms/owca/.well-known/openid-configuration",
+    "client_kwargs": {
+        "scope": "openid",
+    },
+}
+
+MULTIPASS_AUTH_PROVIDERS = {
+    "test_auth_provider": {
+        "type": "static",
+        "title": "Insecure dummy auth",
+        "identities": {
+            "gustav": "hunter2",
+        },
+    },
+    "fsmpi-ldap": {
+        "type": "ldap",
+        "title": "O.W.C.A. LDAP",
+        "ldap": _ldap_config,
+    },
+    "fsmpi-saml": {
+        "type": "saml",
+        "title": "O.W.C.A. SAML",
+        "saml_config": _saml_config,
+    },
+    "fsmpi-oidc": {
+        "type": "authlib",
+        "title": "O.W.C.A. OIDC",
+        "authlib_args": _oidc_config,
+    },
+}
+
+MULTIPASS_IDENTITY_PROVIDERS = {
+    "test_identity_provider": {
+        "type": "static",
+        "identities": {
+            "gustav": {},
+        },
+    },
+    "ldap": {
+        "type": "ldap",
+        "ldap": _ldap_config,
+    },
+    "saml": {
+        "type": "saml",
+    },
+    "oidc": {
+        "type": "authlib",
+        "title": "OIDC",
+    },
+}
+
+MULTIPASS_PROVIDER_MAP = {
+    "test_auth_provider": "test_identity_provider",
+    "ldap": "ldap",
+    "saml": "saml",
+    "oidc": "oidc",
+}
+
+MULTIPASS_IDENTITY_INFO_KEYS = []
diff --git a/frontend/src/main.less b/frontend/src/main.less
index 56468733ef5fce78e6445e901caaf24e5691c9d4..dd1d5f89a94ee66e61bb32aaa4b50424decec4fa 100644
--- a/frontend/src/main.less
+++ b/frontend/src/main.less
@@ -32,7 +32,7 @@ body {
 	}
 }
 
-@inputs: ~'input[type = "text"], input[type = "number"], textarea, select';
+@inputs: ~'input[type = "text"], input[type = "number"], input[type = "password"], textarea, select';
 @buttons: ~'input[type = "submit"], button, a.button';
 
 body, @{inputs}, @{buttons} {
@@ -135,6 +135,13 @@ nav {
 
 	#nav-links {
 		padding: 0;
+		flex-grow: 1;
+	}
+
+	#logout {
+		@media screen and (min-width: @mobile_threshold) {
+			margin-left: auto;
+		}
 	}
 
 	@media screen and (max-width: @mobile_threshold) {
@@ -214,7 +221,7 @@ main {
 		font-size: 1.1rem;
 	}
 
-	input[type = "text"], textarea {
+	input[type = "text"], input[type = "password"], textarea {
 		justify-self: stretch;
 	}
 
diff --git a/pdm.lock b/pdm.lock
index db2f26c0657aaa4fce64d9ec68bb2854b96a79c0..412b0e3138b9bceb7103085bd1a7fa6617070f77 100644
--- a/pdm.lock
+++ b/pdm.lock
@@ -2,10 +2,10 @@
 # It is not intended for manual editing.
 
 [metadata]
-groups = ["default", "dev"]
+groups = ["default", "auth-ldap", "auth-oauth", "auth-saml", "dev"]
 strategy = ["inherit_metadata"]
 lock_version = "4.5.0"
-content_hash = "sha256:63f5da6995402ed21545e227c2419cafb694852ed08e87053164e13708530363"
+content_hash = "sha256:a9b84acbfcc9d879802fa8f81de993b8b24c2594642836253ea0b885a33132a4"
 
 [[metadata.targets]]
 requires_python = ">=3.12"
@@ -141,6 +141,20 @@ files = [
     {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
 ]
 
+[[package]]
+name = "authlib"
+version = "1.3.2"
+requires_python = ">=3.8"
+summary = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
+groups = ["auth-oauth"]
+dependencies = [
+    "cryptography",
+]
+files = [
+    {file = "Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc"},
+    {file = "authlib-1.3.2.tar.gz", hash = "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2"},
+]
+
 [[package]]
 name = "awesomeversion"
 version = "24.6.0"
@@ -261,7 +275,7 @@ name = "cffi"
 version = "1.17.1"
 requires_python = ">=3.8"
 summary = "Foreign Function Interface for Python calling C code."
-groups = ["default"]
+groups = ["default", "auth-oauth"]
 dependencies = [
     "pycparser",
 ]
@@ -376,6 +390,37 @@ files = [
     {file = "cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c"},
 ]
 
+[[package]]
+name = "cryptography"
+version = "43.0.1"
+requires_python = ">=3.7"
+summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+groups = ["auth-oauth"]
+dependencies = [
+    "cffi>=1.12; platform_python_implementation != \"PyPy\"",
+]
+files = [
+    {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
+    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
+    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
+    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
+    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
+    {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
+    {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
+    {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
+    {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
+    {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
+    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
+    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
+    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
+    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
+    {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
+    {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
+    {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
+    {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
+    {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
+]
+
 [[package]]
 name = "cssselect2"
 version = "0.7.0"
@@ -457,6 +502,21 @@ files = [
     {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"},
 ]
 
+[[package]]
+name = "flask-multipass"
+version = "0.5.5"
+requires_python = "~=3.8"
+summary = "A pluggable solution for multi-backend authentication with Flask"
+groups = ["default"]
+dependencies = [
+    "blinker",
+    "flask",
+]
+files = [
+    {file = "flask_multipass-0.5.5-py3-none-any.whl", hash = "sha256:ee7cb3d3e2b92ca7864ac824ae6b3943091c58bb01678b55707dde069209f59c"},
+    {file = "flask_multipass-0.5.5.tar.gz", hash = "sha256:2ea8a0a8b7171e40a5fce575e99ec796d6af84888970ab44ecb2ab06ef89d86c"},
+]
+
 [[package]]
 name = "flask-shell-ipython"
 version = "0.5.3"
@@ -676,6 +736,19 @@ files = [
     {file = "ipython-8.27.0.tar.gz", hash = "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e"},
 ]
 
+[[package]]
+name = "isodate"
+version = "0.6.1"
+summary = "An ISO 8601 date/time/duration parser and formatter"
+groups = ["auth-saml"]
+dependencies = [
+    "six",
+]
+files = [
+    {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"},
+    {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"},
+]
+
 [[package]]
 name = "itsdangerous"
 version = "2.2.0"
@@ -730,6 +803,50 @@ files = [
     {file = "lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d"},
 ]
 
+[[package]]
+name = "lxml"
+version = "5.3.0"
+requires_python = ">=3.6"
+summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+groups = ["auth-saml"]
+files = [
+    {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"},
+    {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"},
+    {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"},
+    {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"},
+    {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"},
+    {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"},
+    {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"},
+    {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"},
+    {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"},
+    {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"},
+    {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"},
+    {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"},
+    {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"},
+    {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"},
+    {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"},
+    {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"},
+    {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"},
+    {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"},
+    {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"},
+    {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"},
+    {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"},
+    {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"},
+    {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"},
+    {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"},
+    {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"},
+    {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"},
+    {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"},
+    {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"},
+    {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"},
+    {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"},
+    {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"},
+    {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"},
+    {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"},
+    {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"},
+    {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"},
+]
+
 [[package]]
 name = "markdown-it-py"
 version = "3.0.0"
@@ -974,12 +1091,37 @@ files = [
     {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
 ]
 
+[[package]]
+name = "pyasn1"
+version = "0.6.1"
+requires_python = ">=3.8"
+summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
+groups = ["auth-ldap"]
+files = [
+    {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
+    {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
+]
+
+[[package]]
+name = "pyasn1-modules"
+version = "0.4.1"
+requires_python = ">=3.8"
+summary = "A collection of ASN.1-based protocols modules"
+groups = ["auth-ldap"]
+dependencies = [
+    "pyasn1<0.7.0,>=0.4.6",
+]
+files = [
+    {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
+    {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
+]
+
 [[package]]
 name = "pycparser"
 version = "2.22"
 requires_python = ">=3.8"
 summary = "C parser in Python"
-groups = ["default"]
+groups = ["default", "auth-oauth"]
 files = [
     {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
     {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
@@ -1076,6 +1218,20 @@ files = [
     {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
 ]
 
+[[package]]
+name = "python-ldap"
+version = "3.4.4"
+requires_python = ">=3.6"
+summary = "Python modules for implementing LDAP clients"
+groups = ["auth-ldap"]
+dependencies = [
+    "pyasn1-modules>=0.1.5",
+    "pyasn1>=0.3.7",
+]
+files = [
+    {file = "python-ldap-3.4.4.tar.gz", hash = "sha256:7edb0accec4e037797705f3a05cbf36a9fde50d08c8f67f2aef99a2628fab828"},
+]
+
 [[package]]
 name = "python-lsp-jsonrpc"
 version = "1.1.2"
@@ -1155,6 +1311,21 @@ files = [
     {file = "python_webpack_boilerplate-1.0.3.tar.gz", hash = "sha256:857580c936b0d465f5df8a420f7c120dbed7d1485c2b2cebd406056e3e029c90"},
 ]
 
+[[package]]
+name = "python3-saml"
+version = "1.16.0"
+summary = "Saml Python Toolkit. Add SAML support to your Python software using this library"
+groups = ["auth-saml"]
+dependencies = [
+    "isodate>=0.6.1",
+    "lxml!=4.7.0,>=4.6.5",
+    "xmlsec>=1.3.9",
+]
+files = [
+    {file = "python3-saml-1.16.0.tar.gz", hash = "sha256:97c9669aecabc283c6e5fb4eb264f446b6e006f5267d01c9734f9d8bffdac133"},
+    {file = "python3_saml-1.16.0-py3-none-any.whl", hash = "sha256:20b97d11b04f01ee22e98f4a38242e2fea2e28fbc7fbc9bdd57cab5ac7fc2d0d"},
+]
+
 [[package]]
 name = "pytoolconfig"
 version = "1.3.1"
@@ -1293,7 +1464,7 @@ name = "six"
 version = "1.16.0"
 requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 summary = "Python 2 and 3 compatibility utilities"
-groups = ["default", "dev"]
+groups = ["default", "auth-saml", "dev"]
 files = [
     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -1531,6 +1702,28 @@ files = [
     {file = "wtforms-3.1.2.tar.gz", hash = "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9"},
 ]
 
+[[package]]
+name = "xmlsec"
+version = "1.3.14"
+requires_python = ">=3.5"
+summary = "Python bindings for the XML Security Library"
+groups = ["auth-saml"]
+dependencies = [
+    "lxml>=3.8",
+]
+files = [
+    {file = "xmlsec-1.3.14-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1eb3dcf244a52f796377112d8f238dbb522eb87facffb498425dc8582a84a6bf"},
+    {file = "xmlsec-1.3.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:330147ce59fbe56a9be5b2085d739c55a569f112576b3f1b33681f87416eaf33"},
+    {file = "xmlsec-1.3.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed4034939d8566ccdcd3b4e4f23c63fd807fb8763ae5668d59a19e11640a8242"},
+    {file = "xmlsec-1.3.14-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a98eadfcb0c3b23ccceb7a2f245811f8d784bd287640dcfe696a26b9db1e2fc0"},
+    {file = "xmlsec-1.3.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ff7b2711557c1087b72b0a1a88d82eafbf2a6d38b97309a6f7101d4a7041c3"},
+    {file = "xmlsec-1.3.14-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:774d5d1e45f07f953c1cc14fd055c1063f0725f7248b6b0e681f59fd8638934d"},
+    {file = "xmlsec-1.3.14-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd10ca3201f164482775a7ce61bf7ee9aade2e7d032046044dd0f6f52c91d79d"},
+    {file = "xmlsec-1.3.14-cp312-cp312-win32.whl", hash = "sha256:19c86bab1498e4c2e56d8e2c878f461ccb6e56b67fd7522b0c8fda46d8910781"},
+    {file = "xmlsec-1.3.14-cp312-cp312-win_amd64.whl", hash = "sha256:d0762f4232bce2c7f6c0af329db8b821b4460bbe123a2528fb5677d03db7a4b5"},
+    {file = "xmlsec-1.3.14.tar.gz", hash = "sha256:934f804f2f895bcdb86f1eaee236b661013560ee69ec108d29cdd6e5f292a2d9"},
+]
+
 [[package]]
 name = "yarl"
 version = "1.11.1"
diff --git a/pyproject.toml b/pyproject.toml
index f593f05708023c52865baee11bf620e9b8e17d3c..904eae0021085da9c024fb21f00e0c9c78e5e00e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,6 +20,18 @@ dependencies = [
     "flask-weasyprint",
     "python-webpack-boilerplate",
     "pyipp @ git+https://github.com/ctalkington/python-ipp",
+    "Flask-Multipass>=0.5.5",
+]
+
+[project.optional-dependencies]
+auth-ldap = [
+    "python-ldap~=3.4",
+]
+auth-saml = [
+    "python3-saml~=1.16",
+]
+auth-oauth = [
+    "Authlib~=1.3",
 ]
 
 [tool.pdm.dev-dependencies]
diff --git a/schilder2000/__init__.py b/schilder2000/__init__.py
index df234a283fdcb3e32a1292b07ad48fca12767d11..a4c9fa3c280fa85c31ba2ee7b03563c4a1572290 100644
--- a/schilder2000/__init__.py
+++ b/schilder2000/__init__.py
@@ -1,14 +1,16 @@
 from pathlib import Path
 
+from flask_multipass import Multipass
 from flask_sqlalchemy import SQLAlchemy
 from flask_wtf.csrf import CSRFProtect
 from webpack_boilerplate.config import setup_jinja2_ext
 
-from .helpers import Flask
+from .helpers import Flask, identity_handler, require_login
 
 
 db = SQLAlchemy()
 csrf = CSRFProtect()
+multipass = Multipass()
 
 
 def create_app():
@@ -25,6 +27,17 @@ def create_app():
 
     csrf.init_app(app)
 
+    if app.config["REQUIRE_LOGIN"]:
+        app.config["MULTIPASS_LOGIN_FORM_TEMPLATE"] = "login.html.j2"
+        app.config["MULTIPASS_LOGIN_SELECTOR_TEMPLATE"] = "login_select.html.j2"
+        app.config["MULTIPASS_SUCCESS_ENDPOINT"] = "views.index"
+        multipass.identity_handler(identity_handler)
+        multipass.init_app(app)
+
+        for k, v in app.view_functions.items():
+            if k.startswith("_flaskmultipass_saml_acs_"):
+                csrf.exempt(v)
+
     app.config.update(
         {
             "WEBPACK_LOADER": {
@@ -38,12 +51,16 @@ def create_app():
 
     from . import views
 
+    if app.config["REQUIRE_LOGIN"]:
+        views.bp.before_request(require_login)
     app.register_blueprint(views.bp)
 
     from . import instance
 
     instance.bp.static_folder = instance_path / "static"
     instance.bp.template_folder = instance_path / "templates"
+    if app.config["REQUIRE_LOGIN"]:
+        instance.bp.before_request(require_login)
     app.register_blueprint(instance.bp)
 
     return app
diff --git a/schilder2000/helpers.py b/schilder2000/helpers.py
index 1a6a5752bd1ba66d9adb2708a4848347af227971..f4aaf9e6e787614a2e295aac74801332c23f8bb8 100644
--- a/schilder2000/helpers.py
+++ b/schilder2000/helpers.py
@@ -4,11 +4,16 @@ from flask import (
     Flask as _Flask,
     Blueprint as FlaskBlueprint,
     current_app,
+    redirect,
     render_template,
+    url_for,
+    session,
 )
 
 from jinja2 import BaseLoader, ChoiceLoader, PrefixLoader, Template
 
+from flask_multipass import IdentityInfo
+
 
 class Blueprint(FlaskBlueprint):
     def real_template_name(
@@ -86,3 +91,17 @@ def get_template_attribute(
         return getattr(mod, attribute)
     else:
         return getattr(mod, attribute, default)
+
+
+def identity_handler(identity_info: IdentityInfo):
+    session["identity"] = dict(
+        identifier=identity_info.identifier,
+        provider=identity_info.provider.name,
+        secure_login=identity_info.secure_login,
+        data=identity_info.data,
+    )
+
+
+def require_login():
+    if "identity" not in session:
+        return redirect(url_for("login"))
diff --git a/schilder2000/templates/_base.html.j2 b/schilder2000/templates/_base.html.j2
index 6e6816345ce79a21870d967f99181e60fceffb1c..49cd22ace8392abe659d7391f7750c3a5d5964f0 100644
--- a/schilder2000/templates/_base.html.j2
+++ b/schilder2000/templates/_base.html.j2
@@ -61,6 +61,9 @@
 					<div id="nav-links">
 						<a {{ maybe_active_href("views.index") }}>Startseite</a>
 						<a {{ maybe_active_href("views.create") }}>Neues Schild</a>
+						{%- if "identity" in session -%}
+							<a href="{{ url_for('views.logout') }}" id="logout">Abmelden</a>
+						{%- endif -%}
 					</div>
 				</nav>
 			{%- endblock nav -%}
diff --git a/schilder2000/templates/login.html.j2 b/schilder2000/templates/login.html.j2
new file mode 100644
index 0000000000000000000000000000000000000000..452343c3589f52e256470125706d91e5738b9f60
--- /dev/null
+++ b/schilder2000/templates/login.html.j2
@@ -0,0 +1,15 @@
+{% extends "_base.html.j2" %}
+
+{% block title -%}
+	Anmeldung
+{%- endblock title %}
+
+{% block main -%}
+	<form method="post">
+		{%- for field in form -%}
+			{{ render_field(field) }}
+		{%- endfor -%}
+
+		<input type="submit" value="Anmelden" />
+	</form>
+{%- endblock main %}
diff --git a/schilder2000/templates/login_select.html.j2 b/schilder2000/templates/login_select.html.j2
new file mode 100644
index 0000000000000000000000000000000000000000..a3cc5367fcf8b067f1c34844e110d34bc4aa21f4
--- /dev/null
+++ b/schilder2000/templates/login_select.html.j2
@@ -0,0 +1,14 @@
+{% extends "_base.html.j2" %}
+
+{% block title -%}
+	Anmeldung
+{%- endblock title %}
+
+{% block main -%}
+    Available login providers:
+    <ul>
+        {% for provider in providers|sort(attribute='title') %}
+            <li><a href="{{ url_for(login_endpoint, provider=provider.name, next=next) }}">{{ provider.title }}</a></li>
+        {% endfor %}
+    </ul>
+{%- endblock %}
diff --git a/schilder2000/views.py b/schilder2000/views.py
index fa26e05842519cc40f3df22b13901b14ba46cf7f..dfa02925884af9ecdce957bdb9fc20d255505b93 100644
--- a/schilder2000/views.py
+++ b/schilder2000/views.py
@@ -1,4 +1,4 @@
-from . import db
+from . import db, multipass
 from .instance import list_templates, list_images
 from .models import Schild, SchildForm, PrintForm
 
@@ -92,3 +92,7 @@ def create():
             "schild.html.j2",
             form=form,
         )
+
+@bp.route("/logout")
+def logout():
+    return multipass.logout(url_for(".index"), clear_session=True)