diff --git a/pdm.lock b/pdm.lock
index db2f26c0657aaa4fce64d9ec68bb2854b96a79c0..0446e970d31f12e4e6d66ad1e5565eaff4986f1d 100644
--- a/pdm.lock
+++ b/pdm.lock
@@ -5,7 +5,7 @@
 groups = ["default", "dev"]
 strategy = ["inherit_metadata"]
 lock_version = "4.5.0"
-content_hash = "sha256:63f5da6995402ed21545e227c2419cafb694852ed08e87053164e13708530363"
+content_hash = "sha256:ffa1c8b51d11a5b46e0f1e2240677b7ca08edc5983c2b8780b0ba2c0341a1862"
 
 [[metadata.targets]]
 requires_python = ">=3.12"
@@ -457,6 +457,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"
diff --git a/pyproject.toml b/pyproject.toml
index f593f05708023c52865baee11bf620e9b8e17d3c..95d9616954fc0eae38b989a80056e01e2db5f2d8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,6 +20,7 @@ dependencies = [
     "flask-weasyprint",
     "python-webpack-boilerplate",
     "pyipp @ git+https://github.com/ctalkington/python-ipp",
+    "Flask-Multipass>=0.5.5",
 ]
 
 [tool.pdm.dev-dependencies]
diff --git a/schilder2000/__init__.py b/schilder2000/__init__.py
index df234a283fdcb3e32a1292b07ad48fca12767d11..9460218b6c3ee9702184024492f41bb8426118ae 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,12 @@ def create_app():
 
     csrf.init_app(app)
 
+    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)
+
     app.config.update(
         {
             "WEBPACK_LOADER": {
@@ -38,12 +46,14 @@ def create_app():
 
     from . import views
 
+    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"
+    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..653df7f850d739f2a537a72a95b3607794393b07 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,16 @@ 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/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)