diff --git a/README.md b/README.md
index 810a27b72a7ee9c5975cf87f9813aff322419d6d..18ee6ba02ae88a6c4492e3ae0be81b7264f54905 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ A web service to manage LDAP users, groups and permissions.
 - python3-flask
 - python3-flask-sqlalchemy
 - python3-qrcode
-- python3-fido2
+- python3-fido2 (version 0.5.0, optional)
 - git (cli utility, musst be in path)
 
 ## development
diff --git a/uffd/mfa/models.py b/uffd/mfa/models.py
index 176c96f134989cea3bfd3619255746e3b4dfeaa2..d27b5f810966f7e746e619fdb95a3621d9792b18 100644
--- a/uffd/mfa/models.py
+++ b/uffd/mfa/models.py
@@ -6,8 +6,6 @@ import crypt
 from flask import request, current_app
 from sqlalchemy import Column, Integer, Enum, Boolean, String, DateTime, Text
 
-from fido2.ctap2 import AttestedCredentialData
-
 from uffd.database import db
 from uffd.user.models import User
 
@@ -137,6 +135,7 @@ class WebauthnMethod(MFAMethod):
 
 	@property
 	def cred(self):
+		from fido2.ctap2 import AttestedCredentialData
 		return AttestedCredentialData(base64.b64decode(self._cred))
 
 	@cred.setter
diff --git a/uffd/mfa/templates/auth.html b/uffd/mfa/templates/auth.html
index 2bacaf22618acabec1fde62e1a1df09bc3e9d660..945c4357233cf1c261c1b85e900535874c78e78c 100644
--- a/uffd/mfa/templates/auth.html
+++ b/uffd/mfa/templates/auth.html
@@ -39,7 +39,7 @@
 </div>
 </form>
 
-{% if webauthn_methods %}
+{% if webauthn_supported and webauthn_methods %}
 <script src="{{ url_for('static', filename="cbor.js") }}"></script>
 <script>
 function begin_webauthn() {
diff --git a/uffd/mfa/templates/setup.html b/uffd/mfa/templates/setup.html
index 681aa25780057e167eac4875081953dc8a6e12ce..93b634b2fa02e5867666e8c3d61a8e12693def63 100644
--- a/uffd/mfa/templates/setup.html
+++ b/uffd/mfa/templates/setup.html
@@ -69,7 +69,7 @@ You need to setup at least one authentication method to enable two-factor authen
 
 <div class="row mt-3">
 	<div class="col-12 col-md-5">
-		<h4>Authenticator Apps</h4>
+		<h4>Authenticator Apps (TOTP)</h4>
 		<p>Use an authenticator application on your mobile device as a second factor.</p>
 		<p>The authenticator app generates a 6-digit one-time code each time you login.
 		Compatible apps are freely available for most phones.</p>
@@ -121,6 +121,9 @@ You need to setup at least one authentication method to enable two-factor authen
 	</div>
 
 	<div class="col-12 col-md-7">
+		{% if not webauthn_supported %}
+		<div class="alert alert-warning" role="alert">U2F/FIDO2 support not enabled</div>
+		{% endif %}
 		<noscript>
 			<div class="alert alert-warning" role="alert">Enable javascript in your browser to use U2F and FIDO2 devices!</div>
 		</noscript>
@@ -162,6 +165,7 @@ You need to setup at least one authentication method to enable two-factor authen
 	</div>
 </div>
 
+{% if webauthn_supported %}
 <script src="{{ url_for('static', filename="cbor.js") }}"></script>
 <script>
 
@@ -238,5 +242,6 @@ if (typeof(PublicKeyCredential) != "undefined") {
 }
 
 </script>
+{% endif %}
 
 {% endblock %}
diff --git a/uffd/mfa/views.py b/uffd/mfa/views.py
index fbaad53b1808f09bdb5525ef593c50285168853b..447623aac660ad617ee4d34177878874b00f3493 100644
--- a/uffd/mfa/views.py
+++ b/uffd/mfa/views.py
@@ -1,10 +1,6 @@
 from flask import Blueprint, render_template, session, request, redirect, url_for, flash, current_app, abort
 import urllib.parse
-
-from fido2.client import ClientData
-from fido2.server import Fido2Server, RelyingParty
-from fido2.ctap2 import AttestationObject, AuthenticatorData
-from fido2 import cbor
+from warnings import warn
 
 from uffd.database import db
 from uffd.mfa.models import MFAMethod, TOTPMethod, WebauthnMethod, RecoveryCodeMethod
@@ -102,45 +98,99 @@ def delete_totp(id):
 	db.session.commit()
 	return redirect(url_for('mfa.setup'))
 
-def get_webauthn_server():
-	return Fido2Server(RelyingParty(current_app.config.get('MFA_RP_ID', urllib.parse.urlsplit(request.url).hostname), current_app.config['MFA_RP_NAME']))
-
-@bp.route('/setup/webauthn/begin', methods=['POST'])
-@login_required()
-@csrf_protect(blueprint=bp)
-def setup_webauthn_begin():
-	user = get_current_user()
-	if not RecoveryCodeMethod.query.filter_by(dn=user.dn).all():
-		abort(403)
-	methods = WebauthnMethod.query.filter_by(dn=user.dn).all()
-	creds = [method.cred for method in methods]
-	server = get_webauthn_server()
-	registration_data, state = server.register_begin(
-		{
-			"id": user.dn.encode(),
-			"name": user.loginname,
-			"displayName": user.displayname,
-		},
-		creds,
-		user_verification='discouraged',
-	)
-	session["webauthn-state"] = state
-	return cbor.dumps(registration_data)
-
-@bp.route('/setup/webauthn/complete', methods=['POST'])
-@login_required()
-@csrf_protect(blueprint=bp)
-def setup_webauthn_complete():
-	user = get_current_user()
-	server = get_webauthn_server()
-	data = cbor.loads(request.get_data())[0]
-	client_data = ClientData(data["clientDataJSON"])
-	att_obj = AttestationObject(data["attestationObject"])
-	auth_data = server.register_complete(session["webauthn-state"], client_data, att_obj)
-	method = WebauthnMethod(user, auth_data.credential_data, name=data['name'])
-	db.session.add(method)
-	db.session.commit()
-	return cbor.dumps({"status": "OK"})
+# WebAuthn support is optional because fido2 has a pretty unstable
+# interface (v0.5.0 on buster and current version are completely
+# incompatible) and might be difficult to install with the correct version
+try:
+	from fido2.client import ClientData
+	from fido2.server import Fido2Server, RelyingParty
+	from fido2.ctap2 import AttestationObject, AuthenticatorData
+	from fido2 import cbor
+	webauthn_supported = True
+except ImportError as e:
+	warn('2FA WebAuthn support disabled because import of the fido2 module failed (%s)'%e)
+	webauthn_supported = False
+
+bp.add_app_template_global(webauthn_supported, name='webauthn_supported')
+
+if webauthn_supported:
+	def get_webauthn_server():
+		return Fido2Server(RelyingParty(current_app.config.get('MFA_RP_ID', urllib.parse.urlsplit(request.url).hostname), current_app.config['MFA_RP_NAME']))
+
+	@bp.route('/setup/webauthn/begin', methods=['POST'])
+	@login_required()
+	@csrf_protect(blueprint=bp)
+	def setup_webauthn_begin():
+		user = get_current_user()
+		if not RecoveryCodeMethod.query.filter_by(dn=user.dn).all():
+			abort(403)
+		methods = WebauthnMethod.query.filter_by(dn=user.dn).all()
+		creds = [method.cred for method in methods]
+		server = get_webauthn_server()
+		registration_data, state = server.register_begin(
+			{
+				"id": user.dn.encode(),
+				"name": user.loginname,
+				"displayName": user.displayname,
+			},
+			creds,
+			user_verification='discouraged',
+		)
+		session["webauthn-state"] = state
+		return cbor.dumps(registration_data)
+
+	@bp.route('/setup/webauthn/complete', methods=['POST'])
+	@login_required()
+	@csrf_protect(blueprint=bp)
+	def setup_webauthn_complete():
+		user = get_current_user()
+		server = get_webauthn_server()
+		data = cbor.loads(request.get_data())[0]
+		client_data = ClientData(data["clientDataJSON"])
+		att_obj = AttestationObject(data["attestationObject"])
+		auth_data = server.register_complete(session["webauthn-state"], client_data, att_obj)
+		method = WebauthnMethod(user, auth_data.credential_data, name=data['name'])
+		db.session.add(method)
+		db.session.commit()
+		return cbor.dumps({"status": "OK"})
+
+	@bp.route("/auth/webauthn/begin", methods=["POST"])
+	def auth_webauthn_begin():
+		user = get_current_user()
+		server = get_webauthn_server()
+		methods = WebauthnMethod.query.filter_by(dn=user.dn).all()
+		creds = [method.cred for method in methods]
+		if not creds:
+			abort(404)
+		auth_data, state = server.authenticate_begin(creds, user_verification='discouraged')
+		session["webauthn-state"] = state
+		return cbor.dumps(auth_data)
+
+	@bp.route("/auth/webauthn/complete", methods=["POST"])
+	def auth_webauthn_complete():
+		user = get_current_user()
+		server = get_webauthn_server()
+		methods = WebauthnMethod.query.filter_by(dn=user.dn).all()
+		creds = [method.cred for method in methods]
+		if not creds:
+			abort(404)
+		data = cbor.loads(request.get_data())[0]
+		credential_id = data["credentialId"]
+		client_data = ClientData(data["clientDataJSON"])
+		auth_data = AuthenticatorData(data["authenticatorData"])
+		signature = data["signature"]
+		# authenticate_complete() (as of python-fido2 v0.5.0, the version in Debian Buster)
+		# does not check signCount, although the spec recommends it
+		server.authenticate_complete(
+			session.pop("webauthn-state"),
+			creds,
+			credential_id,
+			client_data,
+			auth_data,
+			signature,
+		)
+		session['user_mfa'] = True
+		return cbor.dumps({"status": "OK"})
 
 @bp.route('/setup/webauthn/<int:id>/delete')
 @login_required()
@@ -152,44 +202,6 @@ def delete_webauthn(id):
 	db.session.commit()
 	return redirect(url_for('mfa.setup'))
 
-@bp.route("/auth/webauthn/begin", methods=["POST"])
-def auth_webauthn_begin():
-	user = get_current_user()
-	server = get_webauthn_server()
-	methods = WebauthnMethod.query.filter_by(dn=user.dn).all()
-	creds = [method.cred for method in methods]
-	if not creds:
-		abort(404)
-	auth_data, state = server.authenticate_begin(creds, user_verification='discouraged')
-	session["webauthn-state"] = state
-	return cbor.dumps(auth_data)
-
-@bp.route("/auth/webauthn/complete", methods=["POST"])
-def auth_webauthn_complete():
-	user = get_current_user()
-	server = get_webauthn_server()
-	methods = WebauthnMethod.query.filter_by(dn=user.dn).all()
-	creds = [method.cred for method in methods]
-	if not creds:
-		abort(404)
-	data = cbor.loads(request.get_data())[0]
-	credential_id = data["credentialId"]
-	client_data = ClientData(data["clientDataJSON"])
-	auth_data = AuthenticatorData(data["authenticatorData"])
-	signature = data["signature"]
-	# authenticate_complete() (as of python-fido2 v0.5.0, the version in Debian Buster)
-	# does not check signCount, although the spec recommends it
-	server.authenticate_complete(
-		session.pop("webauthn-state"),
-		creds,
-		credential_id,
-		client_data,
-		auth_data,
-		signature,
-	)
-	session['user_mfa'] = True
-	return cbor.dumps({"status": "OK"})
-
 @bp.route('/auth', methods=['GET'])
 @login_required(skip_mfa=True)
 def auth():