diff --git a/uffd/mfa/templates/auth.html b/uffd/mfa/templates/auth.html
index 945c4357233cf1c261c1b85e900535874c78e78c..831bc87a74ed93418ee6ec6139765cf0ac656c7b 100644
--- a/uffd/mfa/templates/auth.html
+++ b/uffd/mfa/templates/auth.html
@@ -50,8 +50,16 @@ function begin_webauthn() {
 	fetch({{ url_for('mfa.auth_webauthn_begin')|tojson }}, {
 		method: 'POST',
 	}).then(function(response) {
-		if(response.ok) return response.arrayBuffer();
-		throw new Error('You have not registered any U2F/FIDO2 devices for your account');
+		if (response.ok) {
+			return response.arrayBuffer();
+		} else if (response.status == 403) {
+			window.location = {{ request.url|tojson }}; /* reload */
+			throw new Error('Session timed out');
+		} else if (response.status == 404) {
+			throw new Error('You have not registered any U2F/FIDO2 devices for your account');
+		} else {
+			throw new Error('Server error');
+		}
 	}).then(CBOR.decode).then(function(options) {
 		$('#webauthn-btn-text').text('Waiting for response from your device');
 		return navigator.credentials.get(options);
@@ -72,10 +80,13 @@ function begin_webauthn() {
 			$('#webauthn-spinner').addClass('d-none');
 			$('#webauthn-btn-text').text('Success, redirecting');
 			window.location = {{ (ref or url_for('index'))|tojson }};
+		} else if (response.status == 403) {
+			window.location = {{ request.url|tojson }}; /* reload */
+			throw new Error('Session timed out');
 		} else {
 			throw new Error('Response from authenticator rejected');
 		}
-	}, function(err) {
+	}).catch(function(err) {
 		console.log(err);
 		/* various webauthn errors */
 		if (err.name == 'NotAllowedError')
diff --git a/uffd/mfa/views.py b/uffd/mfa/views.py
index 62b0f4d027a9344cc9c81dffade811582eba3168..7255ae026ec9c3992d9fdc88b917b76cb5edd8d4 100644
--- a/uffd/mfa/views.py
+++ b/uffd/mfa/views.py
@@ -5,7 +5,7 @@ from flask import Blueprint, render_template, session, request, redirect, url_fo
 
 from uffd.database import db
 from uffd.mfa.models import MFAMethod, TOTPMethod, WebauthnMethod, RecoveryCodeMethod
-from uffd.session.views import get_current_user, login_required
+from uffd.session.views import get_current_user, login_required, pre_mfa_login_required
 from uffd.ldap import uid_to_dn
 from uffd.user.models import User
 from uffd.csrf import csrf_protect
@@ -156,6 +156,7 @@ if WEBAUTHN_SUPPORTED:
 		return cbor.dumps({"status": "OK"})
 
 	@bp.route("/auth/webauthn/begin", methods=["POST"])
+	@pre_mfa_login_required(no_redirect=True)
 	def auth_webauthn_begin():
 		user = get_current_user()
 		server = get_webauthn_server()
@@ -168,6 +169,7 @@ if WEBAUTHN_SUPPORTED:
 		return cbor.dumps(auth_data)
 
 	@bp.route("/auth/webauthn/complete", methods=["POST"])
+	@pre_mfa_login_required(no_redirect=True)
 	def auth_webauthn_complete():
 		user = get_current_user()
 		server = get_webauthn_server()
@@ -204,7 +206,7 @@ def delete_webauthn(id): #pylint: disable=redefined-builtin
 	return redirect(url_for('mfa.setup'))
 
 @bp.route('/auth', methods=['GET'])
-@login_required(skip_mfa=True)
+@pre_mfa_login_required()
 def auth():
 	user = get_current_user()
 	recovery_methods = RecoveryCodeMethod.query.filter_by(dn=user.dn).all()
@@ -218,7 +220,7 @@ def auth():
 			webauthn_methods=webauthn_methods, recovery_methods=recovery_methods)
 
 @bp.route('/auth', methods=['POST'])
-@login_required(skip_mfa=True)
+@pre_mfa_login_required()
 def auth_finish():
 	user = get_current_user()
 	recovery_methods = RecoveryCodeMethod.query.filter_by(dn=user.dn).all()
diff --git a/uffd/session/views.py b/uffd/session/views.py
index 561d190956579068833aab8028672e31ce98c227..310ceaf67fb3dabf0d3c53228bd4b9b54b44965c 100644
--- a/uffd/session/views.py
+++ b/uffd/session/views.py
@@ -2,7 +2,7 @@ import datetime
 import secrets
 import functools
 
-from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session, abort
 
 from uffd.user.models import User
 from uffd.ldap import user_conn, uid_to_dn
@@ -59,15 +59,28 @@ def is_valid_session():
 	return True
 bp.add_app_template_global(is_valid_session)
 
-def login_required(group=None, skip_mfa=False):
+def pre_mfa_login_required(no_redirect=False):
+	def wrapper(func):
+		@functools.wraps(func)
+		def decorator(*args, **kwargs):
+			if not login_valid() or datetime.datetime.now().timestamp() > session['logintime'] + 10*60:
+				session.clear()
+				if no_redirect:
+					abort(403)
+				flash('You need to login first')
+				return redirect(url_for('session.login', ref=request.url))
+			return func(*args, **kwargs)
+		return decorator
+	return wrapper
+
+def login_required(group=None):
 	def wrapper(func):
 		@functools.wraps(func)
 		def decorator(*args, **kwargs):
 			if not login_valid():
 				flash('You need to login first')
 				return redirect(url_for('session.login', ref=request.url))
-			if not skip_mfa and not session.get('user_mfa'):
-				print('redirecting login_required', skip_mfa, session.get('user_mfa'))
+			if not session.get('user_mfa'):
 				return redirect(url_for('mfa.auth', ref=request.url))
 			if not get_current_user().is_in_group(group):
 				flash('Access denied')