diff --git a/jon/__init__.py b/jon/__init__.py
index 6195cdf..c375ac4 100644
--- a/jon/__init__.py
+++ b/jon/__init__.py
@@ -22,12 +22,7 @@ def create_app():
     app.config.from_file("config.json", load=json.load, silent=True)
 
     db.init_app(app)
-
-    # This function denies every request until `auth.ACCESS_TOKEN`
-    # is passed using `?token=` to authenticate the session.
-    @app.before_request
-    def before_req_fun():
-        return auth.before_request()
+    auth.init_app(app)
 
     @app.context_processor
     def utility_processor():
diff --git a/jon/auth.py b/jon/auth.py
index 10d7fbb..65c1969 100644
--- a/jon/auth.py
+++ b/jon/auth.py
@@ -1,7 +1,10 @@
 import secrets
 import string
 
-from flask import Blueprint, request, redirect, render_template, session
+from flask import Blueprint, request, redirect, render_template
+from flask_login import current_user, login_user, logout_user, LoginManager
+from typing import Any, Dict, List, Optional
+
 
 bp = Blueprint("auth", __name__, url_prefix="/auth")
 
@@ -15,27 +18,67 @@ ALLOWED_PATHS = [
 ]
 
 
-def before_request():
-    """
-    If the correct token query parameter is passed along with any request,
-    we mark this session authenticated by setting `session["authenticated"]`.
-    Unless the session is authenticated, all requests result in a 403 FORBIDDEN.
-    """
-    if "token" in request.args:
-        if request.args["token"] == ACCESS_TOKEN:
-            session["authenticated"] = ()
-        # Reload the page without query parameters
-        return redirect(request.path)
+# A poor man's replacement for memory-backed session solution.
+# We keep exactly one User (and the corresponding UserData) in
+# memory and use that to store session data.
+class UserData:
+    location: Optional[Dict[str, Any]]
+    orders: List[Dict[str, Any]]
 
-    # Don't deny any paths in `ALLOWED_PATHS`
-    if request.path in ALLOWED_PATHS:
-        return
+    def __init__(self):
+        self.location = None
+        self.orders = []
 
-    if not "authenticated" in session:
-        return render_template("auth/denied.html"), 403
+
+class User:
+    is_authenticated: bool
+    is_active: bool
+    is_anonymous: bool
+
+    data: UserData
+
+    def __init__(self):
+        self.is_authenticated = True
+        self.is_active = True
+        self.is_anonymous = False
+
+        self.data = UserData()
+
+    def get_id(self) -> str:
+        return ""
+
+
+def init_app(app):
+    login_manager = LoginManager(app)
+    the_one_and_only_user = User()
+
+    @login_manager.user_loader
+    def load_user(user_id: str) -> User:
+        assert user_id == ""
+        return the_one_and_only_user
+
+    # This function denies every request until `auth.ACCESS_TOKEN`
+    # is passed using `?token=` to authenticate the user.
+    # We use this instead of @login_required because otherwise we'd have
+    # to add that annotation to all routes.
+    # See also: https://flask-login.readthedocs.io/en/latest/#flask_login.login_required
+    @app.before_request
+    def before_request():
+        if "token" in request.args:
+            if request.args["token"] == ACCESS_TOKEN:
+                login_user(the_one_and_only_user)
+            # Reload the page without query parameters
+            return redirect(request.path)
+
+        # Never deny any paths in `ALLOWED_PATHS`
+        if request.path in ALLOWED_PATHS:
+            return
+
+        if not current_user.is_authenticated:
+            return render_template("auth/denied.html"), 403
 
 
 @bp.get("/logout")
 def logout():
-    session.pop("authenticated", None)
+    logout_user()
     return redirect("/")
diff --git a/jon/entry.py b/jon/entry.py
index 7b6c780..733f8f0 100644
--- a/jon/entry.py
+++ b/jon/entry.py
@@ -1,5 +1,5 @@
-from flask import Blueprint, flash, redirect, render_template, request, session
-
+from flask import Blueprint, flash, redirect, render_template, request
+from flask_login import current_user
 
 from . import db
 
@@ -9,22 +9,18 @@ bp = Blueprint("entry", __name__, url_prefix="/entry")
 
 @bp.route("/", methods=["GET", "POST"])
 def index():
-    cart = session.get("cart", default=[])
     return render_template(
-        "entry/index.html",
-        cart=cart
+        "entry/index.html"
     )
 
 
 @bp.post("/add-new-items")
 def add_new_entries():
-    print(session)
-
     i_know_what_im_doing = "i-know-what-im-doing" in request.form
     if not i_know_what_im_doing:
         return "Du weißt nicht was du tust", 400
 
-    orders = session.get("cart", default=[])
+    orders = current_user.data.orders
     if not orders:
         return "Keine Aufträge", 404
     
@@ -36,7 +32,7 @@ def add_new_entries():
     db.get_db().commit()
 
     # Reset the cart
-    session["cart"] = []
+    current_user.data.orders = []
 
     return redirect(request.referrer)
 
@@ -48,9 +44,7 @@ def delete_order():
     except:
         return "Incomplete or mistyped form", 400
 
-    cart = session.get("cart", default=[])
-    del cart[order_index]
-    session["cart"] = cart
+    del current_user.data.orders[order_index]
 
     return redirect(request.referrer)
 
@@ -76,9 +70,7 @@ def new_order():
         except:
             return f"Incomplete or mistyped form", 400
         
-        cart = session.get("cart", default=[])
-        print(cart)
-        cart.append({
+        current_user.data.orders.append({
             "barcode": barcode,
             "name": name,
             "sales_units": sales_units,
@@ -91,7 +83,6 @@ def new_order():
             "net_unit_price": net_unit_price,
             "gross_unit_price": gross_unit_price
         })
-        session["cart"] = cart
         return redirect("/entry")
 
     with db.run_query("entry/get_groups.sql") as cursor:
@@ -122,10 +113,9 @@ def api_search_items():
     except:
         return {"error": "Missing query parameter `search-term`"}, 400
 
-    location = session.get("location", None)
-
+    location = current_user.data.location
     with db.run_query("search_items.sql", {
-        "location_id": None if location is None else location["location_id"],
+        "location_id": location["location_id"] if location else None,
         "search_term": search_term
     }) as cursor:
         items = cursor.fetchall()
diff --git a/jon/inventory.py b/jon/inventory.py
index 43aafad..7fe9254 100644
--- a/jon/inventory.py
+++ b/jon/inventory.py
@@ -1,4 +1,5 @@
-from flask import Blueprint, redirect, render_template, request, session
+from flask import Blueprint, redirect, render_template, request
+from flask_login import current_user
 
 from . import db
 
@@ -8,7 +9,7 @@ bp = Blueprint("inventory", __name__, url_prefix="/inventory")
 
 @bp.get("/")
 def index():
-    location = session.get("location", None)
+    location = current_user.data.location
     items = db.run_query("get_inventory_overview.sql", {
         "location_id": None if location is None else location["location_id"]
     }).fetchall()
@@ -20,7 +21,7 @@ def index():
 
 @bp.get("/report")
 def read_report():
-    location = session.get("location", None)
+    location = current_user.data.location
     items = db.run_query("get_inventory_report.sql", {
         "location_id": None if location is None else location["location_id"]
     }).fetchall()
diff --git a/jon/location.py b/jon/location.py
index daf84bd..bcd324c 100644
--- a/jon/location.py
+++ b/jon/location.py
@@ -1,4 +1,5 @@
-from flask import Blueprint, render_template, request, session
+from flask import Blueprint, render_template, request
+from flask_login import current_user
 
 from . import db
 
@@ -11,13 +12,12 @@ def index():
     if request.method == "POST":
         location_id = request.form.get("location_id", "")
         if location_id == "":
-            session.pop("location", None)
+            current_user.data.location = None
         else:
             location = db.run_query("get_location_by_id.sql", {
-                "location_id": location_id}
-            ).fetchone()
-            session["location"] = location
-        
+                "location_id": location_id
+            }).fetchone()
+            current_user.data.location = location
 
     locations = db.run_query("get_locations.sql").fetchall()
 
diff --git a/jon/static/jon.css b/jon/static/jon.css
index 0085d72..9f92248 100644
--- a/jon/static/jon.css
+++ b/jon/static/jon.css
@@ -71,3 +71,6 @@ th {
   display: block;
   width: 8em;
 }
+details {
+  font-size: 0.8em;
+}
diff --git a/jon/templates/base.html b/jon/templates/base.html
index c4af7be..54a0654 100644
--- a/jon/templates/base.html
+++ b/jon/templates/base.html
@@ -15,10 +15,10 @@
           <li{{ " class=current-page" if request.path.startswith("/entry") else "" }}><a href="/entry">Eintragen</a></li>
           <li{{ " class=current-page" if request.path.startswith("/location") else "" }}>
             <a href="/location">
-            {% if "location" not in session %}
+            {% if not current_user.data.location %}
               Raum wählen
             {% else %}
-              Raum: {{ session.location.location_name }}
+              Raum: {{ current_user.data.location.location_name }}
             {% endif %}
             </a>
           </li>
@@ -30,6 +30,16 @@
       <details>
         <summary><code>config</code></summary>
         <pre>{% for key, value in config.items() %}{{ key }} = {{ value }}
+{% endfor %}</pre>
+      </details>
+      <details>
+        <summary><code>session</code></summary>
+        <pre>{% for key, value in session.items() %}{{ key }} = {{ value }}
+{% endfor %}</pre>
+      </details>
+      <details>
+        <summary><code>current_user.data</code></summary>
+        <pre>{% for key, value in current_user.data.__dict__.items() %}{{ key }} = {{ value }}
 {% endfor %}</pre>
       </details>
       {% endif %}
diff --git a/jon/templates/entry/index.html b/jon/templates/entry/index.html
index e08c717..126464f 100644
--- a/jon/templates/entry/index.html
+++ b/jon/templates/entry/index.html
@@ -15,7 +15,7 @@
     <th>VK-Preis (Brutto)</th>
     <th>Aktionen</th>
   </tr>
-  {% for cart_item in cart %}
+  {% for cart_item in current_user.data.orders %}
   <tr>
     <td><code>{{ cart_item.barcode }}</code></td>
     <td>{{ cart_item.name }}</td>
diff --git a/jon/templates/location/index.html b/jon/templates/location/index.html
index c395213..286fd6a 100644
--- a/jon/templates/location/index.html
+++ b/jon/templates/location/index.html
@@ -3,9 +3,9 @@
 {% block content %}
 <form method="POST">
   <select name="location_id">
-    <option value="" {{ "selected" if "location" not in session else ""}}>-</option>
+    <option value="" {{ "selected" if not current_user.data.location else ""}}>-</option>
   {% for location in locations %}
-    <option value="{{ location.location_id }}" {{ "selected" if "location" in session and session.location.location_id == location.location_id else "" }}>{{ location.location_name }}</option>
+    <option value="{{ location.location_id }}" {{ "selected" if current_user.data.location.location_id == location.location_id else "" }}>{{ location.location_name }}</option>
   {% endfor %}
   </select>