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):
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']
......
{% 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 %}
......@@ -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 %}
{% 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():
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",
)
......
......@@ -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)
......
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