Skip to content
Snippets Groups Projects
Commit 68adf721 authored by Julian's avatar Julian
Browse files

rebuild 2fa setup and totp registration pages

Also removed border from the output of the qrcode_svg filter.
parent 642f3e2e
No related branches found
No related tags found
No related merge requests found
...@@ -72,10 +72,18 @@ class TOTPMethod(MFAMethod): ...@@ -72,10 +72,18 @@ class TOTPMethod(MFAMethod):
s = self.key + '='*(8 - (len(self.key) % 8)) s = self.key + '='*(8 - (len(self.key) % 8))
return base64.b32decode(s.encode()) 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 @property
def key_uri(self): def key_uri(self):
issuer = urllib.parse.quote(urllib.parse.urlsplit(request.url).netloc) issuer = urllib.parse.quote(self.issuer)
accountname = urllib.parse.quote(self.user.loginname.encode()) accountname = urllib.parse.quote(self.accountname)
params = {'secret': self.key, 'issuer': issuer} params = {'secret': self.key, 'issuer': issuer}
if 'MFA_ICON_URL' in current_app.config: if 'MFA_ICON_URL' in current_app.config:
params['image'] = current_app.config['MFA_ICON_URL'] params['image'] = current_app.config['MFA_ICON_URL']
......
{% extends 'base.html' %} {% extends 'base.html' %}
{% block body %} {% 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"> <div class="col-12 col-md-7">
<a class="btn btn-primary mb-2 ml-auto" href="{{ url_for('mfa.setup_totp') }}">Setup TOTP</a> <form class="form mb-3" action="{{ url_for('mfa.setup_totp') }}">
<a class="btn btn-primary mb-2 ml-2" href="{{ url_for('mfa.setup_webauthn') }}">Setup FIDO</a> <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> </div>
{% if totp_methods or webauthn_methods %} <hr>
<table class="table">
<thead> <div class="row">
<tr> <div class="col-12 col-md-5">
<th scope="col" colspan=2>Name</th> <h4>U2F and FIDO2 Devices</h4>
<th scope="col">Registered On</th> <p>Use an U2F or FIDO2 compatible hardware security key as a second factor.</p>
<th scope="col"></th> <p>U2F and FIDO2 devices are not supported by all browsers and can be particularly difficult to use on mobile devices.
</tr> <strong>It is strongly recommended to also setup an authenticator app</strong> to be able to login on all browsers.</p>
</thead> </div>
<tbody>
{% for method in totp_methods %} <div class="col-12 col-md-7">
<tr> <noscript>
<td style="width: 0.5em;"><i class="fas fa-mobile-alt"></i></td> <div class="alert alert-warning" role="alert">Enable javascript in your browser to use U2F and FIDO2 devices!</div>
<td>{{ method.name }}</td> </noscript>
<td>{{ method.created }}</td> <div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div>
<td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_totp', id=method.id) }}">Delete</a></td> <form id="webauthn-form" class="form mb-3">
</tr> <div class="row m-0">
{% endfor %} <label class="sr-only" for="webauthn-name">Name</label>
{% for method in webauthn_methods %} <input type="text" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="webauthn-name" placeholder="Name" required>
<tr> <button type="submit" id="webauthn-btn" class="btn btn-primary mb-2 col" disabled>
<td style="width: 0.5em;"><i class="fab fa-usb"></i></td> <span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<td>{{ method.name }}</td> <span id="webauthn-btn-text">Setup new device</span>
<td>{{ method.created }}</td> </button>
<td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_webauthn', id=method.id) }}">Delete</a></td> </div>
</tr> </form>
{% endfor %}
</tbody> <table class="table">
</table> <thead>
{% else %} <tr>
<div class="alert alert-info" role="alert"> <th scope="col">Name</th>
You have not setup any two-factor methods yet! <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> </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 %} {% endblock %}
...@@ -2,22 +2,37 @@ ...@@ -2,22 +2,37 @@
{% block body %} {% block body %}
<div style="max-width: 75vh;" class="mx-auto"> <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>
{{ method.key_uri|qrcode_svg(width='100%', height='100%') }}
</div>
<form action="{{ url_for('mfa.setup_totp') }}" method="POST" class="form"> <div class="row">
<div class="form-group"> <div class="mx-auto col-9 col-md-4 mb-3">
<label for="code">Code</label> <a href="{{ method.key_uri }}">
<input name="code" type="text" class="form-control" required="required"> {{ method.key_uri|qrcode_svg(width='100%', height='100%') }}
</div> </a>
<div class="form-group"> </div>
<label for="name">Authenticator Name</label> <div class="col-12 col-md-8">
<input name="name" type="text" class="form-control" required="required"> <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>
</div> <p>
<div class="form-group"> Issuer: {{ method.issuer }}<br>
<button type="submit" class="btn btn-primary btn-block">Verify</button> 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> </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> </form>
<!-- spacer for floating footer -->
<div class="mb-5"></div>
{% endblock %} {% endblock %}
{% 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 %}
...@@ -27,13 +27,13 @@ def setup_totp(): ...@@ -27,13 +27,13 @@ def setup_totp():
user = get_current_user() user = get_current_user()
method = TOTPMethod(user) method = TOTPMethod(user)
session['mfa_totp_key'] = method.key 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']) @bp.route('/setup/totp', methods=['POST'])
@login_required() @login_required()
def setup_totp_finish(): def setup_totp_finish():
user = get_current_user() 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'] del session['mfa_totp_key']
if method.verify(request.form['code']): if method.verify(request.form['code']):
db.session.add(method) db.session.add(method)
...@@ -64,6 +64,8 @@ def get_webauthn_server(): ...@@ -64,6 +64,8 @@ def get_webauthn_server():
@login_required() @login_required()
def setup_webauthn_begin(): def setup_webauthn_begin():
user = get_current_user() 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() server = get_webauthn_server()
registration_data, state = server.register_begin( registration_data, state = server.register_begin(
{ {
...@@ -72,7 +74,7 @@ def setup_webauthn_begin(): ...@@ -72,7 +74,7 @@ def setup_webauthn_begin():
"displayName": user.displayname, "displayName": user.displayname,
"icon": "https://example.com/image.png", "icon": "https://example.com/image.png",
}, },
[], creds,
user_verification=UserVerificationRequirement.DISCOURAGED, user_verification=UserVerificationRequirement.DISCOURAGED,
authenticator_attachment="cross-platform", authenticator_attachment="cross-platform",
) )
......
...@@ -14,7 +14,7 @@ def register_template_helper(app): ...@@ -14,7 +14,7 @@ def register_template_helper(app):
@app.template_filter() @app.template_filter()
def qrcode_svg(content, **attrs): 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() svg = img.get_image()
for key, value, in attrs.items(): for key, value, in attrs.items():
svg.set(key, value) svg.set(key, value)
......
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