Skip to content
Snippets Groups Projects
Commit 44faccf2 authored by Julian's avatar Julian
Browse files

made fido2 dependency optional

parent e6980f7c
No related branches found
No related tags found
No related merge requests found
......@@ -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
......
......@@ -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
......
......@@ -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() {
......
......@@ -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 %}
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():
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment