From 68adf721e0266b37d01a2ff97e0867a92dec6d7c Mon Sep 17 00:00:00 2001 From: Julian Rother <julianr@fsmpi.rwth-aachen.de> Date: Sat, 3 Oct 2020 04:36:26 +0200 Subject: [PATCH] rebuild 2fa setup and totp registration pages Also removed border from the output of the qrcode_svg filter. --- uffd/mfa/models.py | 12 +- uffd/mfa/templates/setup.html | 189 ++++++++++++++++++++----- uffd/mfa/templates/setup_totp.html | 43 ++++-- uffd/mfa/templates/setup_webauthn.html | 65 --------- uffd/mfa/views.py | 8 +- uffd/template_helper.py | 2 +- 6 files changed, 199 insertions(+), 120 deletions(-) delete mode 100644 uffd/mfa/templates/setup_webauthn.html diff --git a/uffd/mfa/models.py b/uffd/mfa/models.py index 547ae579..feffe466 100644 --- a/uffd/mfa/models.py +++ b/uffd/mfa/models.py @@ -72,10 +72,18 @@ class TOTPMethod(MFAMethod): s = self.key + '='*(8 - (len(self.key) % 8)) return base64.b32decode(s.encode()) + @property + def issuer(self): + return urllib.parse.urlsplit(request.url).hostname + + @property + def accountname(self): + return self.user.loginname + @property def key_uri(self): - issuer = urllib.parse.quote(urllib.parse.urlsplit(request.url).netloc) - accountname = urllib.parse.quote(self.user.loginname.encode()) + issuer = urllib.parse.quote(self.issuer) + accountname = urllib.parse.quote(self.accountname) params = {'secret': self.key, 'issuer': issuer} if 'MFA_ICON_URL' in current_app.config: params['image'] = current_app.config['MFA_ICON_URL'] diff --git a/uffd/mfa/templates/setup.html b/uffd/mfa/templates/setup.html index b09bd47d..5ca7003a 100644 --- a/uffd/mfa/templates/setup.html +++ b/uffd/mfa/templates/setup.html @@ -1,44 +1,163 @@ {% extends 'base.html' %} {% block body %} +{% if totp_methods or webauthn_methods %} +<p>Two-factor authentication is currently <strong>enabled</strong>. Delete all registered methods to disable it.</p> +{% else %} +<p>Two-factor authentication is currently <strong>disabled</strong>. Setup an authentication method below to enable it.</p> +{% endif %} + +<hr> + +<div class="row mt-3"> + <div class="col-12 col-md-5"> + <h4>Authenticator Apps</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> + </div> -<div class="btn-toolbar"> - <a class="btn btn-primary mb-2 ml-auto" href="{{ url_for('mfa.setup_totp') }}">Setup TOTP</a> - <a class="btn btn-primary mb-2 ml-2" href="{{ url_for('mfa.setup_webauthn') }}">Setup FIDO</a> + <div class="col-12 col-md-7"> + <form class="form mb-3" action="{{ url_for('mfa.setup_totp') }}"> + <div class="row m-0"> + <label class="sr-only" for="totp-name">Name</label> + <input type="text" name="name" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="totp-name" placeholder="Name" required> + <button type="submit" id="totp-submit" class="btn btn-primary mb-2 col">Setup new authenticator</button> + </div> + </form> + + <table class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">Registered On</th> + <th scope="col"></th> + </tr> + </thead> + <tbody> + {% for method in totp_methods %} + <tr> + <td>{{ method.name }}</td> + <td>{{ method.created.strftime('%b %d, %Y') }}</td> + <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_totp', id=method.id) }}">Delete</a></td> + </tr> + {% endfor %} + {% if not totp_methods %} + <tr class="table-secondary"> + <td colspan=3 class="text-center">No authenticator apps registered yet</td> + </tr> + {% endif %} + </tbody> + </table> + </div> </div> -{% if totp_methods or webauthn_methods %} -<table class="table"> - <thead> - <tr> - <th scope="col" colspan=2>Name</th> - <th scope="col">Registered On</th> - <th scope="col"></th> - </tr> - </thead> - <tbody> - {% for method in totp_methods %} - <tr> - <td style="width: 0.5em;"><i class="fas fa-mobile-alt"></i></td> - <td>{{ method.name }}</td> - <td>{{ method.created }}</td> - <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_totp', id=method.id) }}">Delete</a></td> - </tr> - {% endfor %} - {% for method in webauthn_methods %} - <tr> - <td style="width: 0.5em;"><i class="fab fa-usb"></i></td> - <td>{{ method.name }}</td> - <td>{{ method.created }}</td> - <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_webauthn', id=method.id) }}">Delete</a></td> - </tr> - {% endfor %} - </tbody> -</table> -{% else %} -<div class="alert alert-info" role="alert"> - You have not setup any two-factor methods yet! +<hr> + +<div class="row"> + <div class="col-12 col-md-5"> + <h4>U2F and FIDO2 Devices</h4> + <p>Use an U2F or FIDO2 compatible hardware security key as a second factor.</p> + <p>U2F and FIDO2 devices are not supported by all browsers and can be particularly difficult to use on mobile devices. + <strong>It is strongly recommended to also setup an authenticator app</strong> to be able to login on all browsers.</p> + </div> + + <div class="col-12 col-md-7"> + <noscript> + <div class="alert alert-warning" role="alert">Enable javascript in your browser to use U2F and FIDO2 devices!</div> + </noscript> + <div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div> + <form id="webauthn-form" class="form mb-3"> + <div class="row m-0"> + <label class="sr-only" for="webauthn-name">Name</label> + <input type="text" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="webauthn-name" placeholder="Name" required> + <button type="submit" id="webauthn-btn" class="btn btn-primary mb-2 col" disabled> + <span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> + <span id="webauthn-btn-text">Setup new device</span> + </button> + </div> + </form> + + <table class="table"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">Registered On</th> + <th scope="col"></th> + </tr> + </thead> + <tbody> + {% for method in webauthn_methods %} + <tr> + <td>{{ method.name }}</td> + <td>{{ method.created.strftime('%b %d, %Y') }}</td> + <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_webauthn', id=method.id) }}">Delete</a></td> + </tr> + {% endfor %} + {% if not webauthn_methods %} + <tr class="table-secondary"> + <td colspan=3 class="text-center">No devices registered yet</td> + </tr> + {% endif %} + </tbody> + </table> + </div> </div> -{% endif %} + +<!-- spacer for floating footer --> +<div class="mb-5"></div> + +<script src="{{ url_for('static', filename="cbor.js") }}"></script> +<script> + +$('#webauthn-form').on('submit', function(e) { + $('#webauthn-alert').addClass('d-none'); + $('#webauthn-spinner').removeClass('d-none'); + $('#webauthn-btn-text').text('Contacting server'); + $('#webauthn-btn').prop('disabled', true); + fetch({{ url_for('mfa.setup_webauthn_begin')|tojson }}, { + method: 'POST', + }).then(function(response) { + if(response.ok) return response.arrayBuffer(); + throw new Error('Error getting registration data!'); + }).then(CBOR.decode).then(function(options) { + $('#webauthn-btn-text').text('Waiting for response from your device'); + return navigator.credentials.create(options); + }).then(function(attestation) { + return fetch({{ url_for('mfa.setup_webauthn_complete')|tojson }}, { + method: 'POST', + headers: {'Content-Type': 'application/cbor'}, + body: CBOR.encode({ + "attestationObject": new Uint8Array(attestation.response.attestationObject), + "clientDataJSON": new Uint8Array(attestation.response.clientDataJSON), + "name": $('#webauthn-name').val() + }) + }); + }).then(function(response) { + if (response.ok) { + $('#webauthn-spinner').addClass('d-none'); + $('#webauthn-btn-text').text('Success'); + window.location = {{ url_for('mfa.setup')|tojson }}; + } else { + throw new Error('Server rejected authenticator response'); + } + }, function(reason) { + $('#webauthn-alert').text('Registration failed!'); + $('#webauthn-alert').removeClass('d-none'); + $('#webauthn-spinner').addClass('d-none'); + $('#webauthn-btn-text').text('Retry registration'); + $('#webauthn-btn').prop('disabled', false); + }); + return false; +}); + +if (typeof(PublicKeyCredential) != "undefined") { + $('#webauthn-btn').prop('disabled', false); +} else { + $('#webauthn-alert').text('U2F and FIDO2 devices are not supported by your browser!'); + $('#webauthn-alert').removeClass('d-none'); +} + +</script> {% endblock %} diff --git a/uffd/mfa/templates/setup_totp.html b/uffd/mfa/templates/setup_totp.html index 8272a8b5..db83e46b 100644 --- a/uffd/mfa/templates/setup_totp.html +++ b/uffd/mfa/templates/setup_totp.html @@ -2,22 +2,37 @@ {% block body %} -<div style="max-width: 75vh;" class="mx-auto"> - {{ method.key_uri|qrcode_svg(width='100%', height='100%') }} -</div> +<p>Install an authenticator application on your mobile device like FreeOTP or Google Authenticator and scan this QR code. On Apple devices you can use an app called "Authenticator".</p> -<form action="{{ url_for('mfa.setup_totp') }}" method="POST" class="form"> -<div class="form-group"> - <label for="code">Code</label> - <input name="code" type="text" class="form-control" required="required"> -</div> -<div class="form-group"> - <label for="name">Authenticator Name</label> - <input name="name" type="text" class="form-control" required="required"> -</div> -<div class="form-group"> - <button type="submit" class="btn btn-primary btn-block">Verify</button> +<div class="row"> + <div class="mx-auto col-9 col-md-4 mb-3"> + <a href="{{ method.key_uri }}"> + {{ method.key_uri|qrcode_svg(width='100%', height='100%') }} + </a> + </div> + <div class="col-12 col-md-8"> + <p>If you are on your mobile device and cannot scan the code, you can click on it to open it with your authenticator app. If that does not work, enter the following details manually into your authenticator app:</p> + <p> + Issuer: {{ method.issuer }}<br> + Account: {{ method.accountname }}<br> + Secret: {{ method.key }}<br> + Type: TOTP (time-based)<br> + Digits: 6<br> + Hash algorithm: SHA1<br> + Interval/period: 30 seconds + </p> + + </div> </div> + +<form action="{{ url_for('mfa.setup_totp', name=name) }}" method="POST" class="form"> + <div class="row m-0"> + <input type="text" name="code" class="form-control mb-2 mr-2 col-auto col-md" id="code" placeholder="Code" required autofocus> + <button type="submit" class="btn btn-primary mb-2 col col-md-auto">Verify and complete setup</button> + </div> </form> +<!-- spacer for floating footer --> +<div class="mb-5"></div> + {% endblock %} diff --git a/uffd/mfa/templates/setup_webauthn.html b/uffd/mfa/templates/setup_webauthn.html deleted file mode 100644 index 3a143156..00000000 --- a/uffd/mfa/templates/setup_webauthn.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends 'base.html' %} - -{% block body %} - -<div id="register-alert" class="alert alert-warning d-none" role="alert"></div> - -<form action="{{ url_for('mfa.setup_webauthn') }}" method="POST" class="form"> -<div class="form-group"> - <label for="name">Authenticator Name</label> - <input name="name" type="text" id="method-name" class="form-control" required="required"> -</div> -<div class="form-group"> - <button type="submit" id="register-btn" class="btn btn-primary btn-block"> - <span id="register-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> - <span id="register-btn-text">Register token</span> - </button> -</div> -</form> - -<script src="{{ url_for('static', filename="cbor.js") }}"></script> -<script> - -$('form').on('submit', function(e) { - $('#register-alert').addClass('d-none'); - $('#register-spinner').removeClass('d-none'); - $('#register-btn-text').text('Contacting server'); - $('#register-btn').prop('disabled', true); - fetch({{ url_for('mfa.setup_webauthn_begin')|tojson }}, { - method: 'POST', - }).then(function(response) { - if(response.ok) return response.arrayBuffer(); - throw new Error('Error getting registration data!'); - }).then(CBOR.decode).then(function(options) { - $('#register-btn-text').text('Waiting for response from your device'); - return navigator.credentials.create(options); - }).then(function(attestation) { - return fetch({{ url_for('mfa.setup_webauthn_complete')|tojson }}, { - method: 'POST', - headers: {'Content-Type': 'application/cbor'}, - body: CBOR.encode({ - "attestationObject": new Uint8Array(attestation.response.attestationObject), - "clientDataJSON": new Uint8Array(attestation.response.clientDataJSON), - "name": $('#method-name').val() - }) - }); - }).then(function(response) { - if (response.ok) { - $('#register-spinner').addClass('d-none'); - $('#register-btn-text').text('Success'); - window.location = {{ url_for('mfa.setup')|tojson }}; - } else { - throw new Error('Server rejected authenticator response'); - } - }, function(reason) { - $('#register-alert').text('Registration failed!'); - $('#register-alert').removeClass('d-none'); - $('#register-spinner').addClass('d-none'); - $('#register-btn-text').text('Retry registration'); - $('#register-btn').prop('disabled', false); - }); - return false; -}); -</script> - -{% endblock %} diff --git a/uffd/mfa/views.py b/uffd/mfa/views.py index 3baebf11..a46a6802 100644 --- a/uffd/mfa/views.py +++ b/uffd/mfa/views.py @@ -27,13 +27,13 @@ def setup_totp(): user = get_current_user() method = TOTPMethod(user) session['mfa_totp_key'] = method.key - return render_template('setup_totp.html', method=method) + return render_template('setup_totp.html', method=method, name=request.values['name']) @bp.route('/setup/totp', methods=['POST']) @login_required() def setup_totp_finish(): user = get_current_user() - method = TOTPMethod(user, name=request.form['name'], key=session['mfa_totp_key']) + method = TOTPMethod(user, name=request.values['name'], key=session['mfa_totp_key']) del session['mfa_totp_key'] if method.verify(request.form['code']): db.session.add(method) @@ -64,6 +64,8 @@ def get_webauthn_server(): @login_required() def setup_webauthn_begin(): user = get_current_user() + methods = WebauthnMethod.query.filter_by(dn=user.dn).all() + creds = [method.cred_data.credential_data for method in methods] server = get_webauthn_server() registration_data, state = server.register_begin( { @@ -72,7 +74,7 @@ def setup_webauthn_begin(): "displayName": user.displayname, "icon": "https://example.com/image.png", }, - [], + creds, user_verification=UserVerificationRequirement.DISCOURAGED, authenticator_attachment="cross-platform", ) diff --git a/uffd/template_helper.py b/uffd/template_helper.py index 0dff9a43..63bad80e 100644 --- a/uffd/template_helper.py +++ b/uffd/template_helper.py @@ -14,7 +14,7 @@ def register_template_helper(app): @app.template_filter() def qrcode_svg(content, **attrs): - img = qrcode.make(content, image_factory=qrcode.image.svg.SvgPathImage) + img = qrcode.make(content, image_factory=qrcode.image.svg.SvgPathImage, border=0) svg = img.get_image() for key, value, in attrs.items(): svg.set(key, value) -- GitLab