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