Security key setup page (front-end)

This commit is contained in:
devStorm 2020-05-05 01:32:49 -07:00
parent 117b120556
commit 3ce4dfb371
No known key found for this signature in database
GPG Key ID: D52E1B66F336AC57
6 changed files with 374 additions and 0 deletions

View File

@ -11,6 +11,7 @@ from .views import (
alias_contact_manager,
mfa_setup,
mfa_cancel,
fido_setup,
domain_detail,
lifetime_licence,
directory,

View File

@ -0,0 +1,58 @@
{% extends 'default.html' %}
{% set active_page = "setting" %}
{% block title %}
Security Key Setup
{% endblock %}
{% block head %}
<script src="{{ url_for('static', filename='node_modules/qrious/dist/qrious.min.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/vendors/base64.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/vendors/webauthn.js') }}"></script>
{% endblock %}
{% block default_content %}
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
<h1 class="h2 text-center">Register Your Security Key</h1>
<p class="text-center">Follow your browser's steps to register your security key with Simple Login</p>
<form id="formRegisterKey" method="post">
{{ fido_token_form.csrf_token }}
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
</form>
<div class="text-center">
<button id="btnRegisterKey" onclick="registerKey($, toastr)" class="btn btn-lg btn-primary mt-2">Register Key</button>
</div>
<script>
async function registerKey () {
$("#btnRegisterKey").prop('disabled', true);
$("#btnRegisterKey").text('Waiting for Security Key...');
const pkCredentialCreateOptions = transformCredentialCreateOptions(
JSON.parse('{{credential_create_options|tojson|safe}}')
)
let credential
try {
credential = await navigator.credentials.create({
publicKey: pkCredentialCreateOptions
});
} catch (err) {
toastr.error("An error occurred when we trying to register your key.");
$("#btnRegisterKey").prop('disabled', false);
$("#btnRegisterKey").text('Register Key');
return console.error("Error when trying to create credential:", err);
}
const skAssertion = transformNewAssertionForServer(credential);
$('#sk_assertion').val(JSON.stringify(skAssertion));
$('#formRegisterKey').submit();
}
$("#btnRegisterKey").click(registerKey);
$('document').ready(registerKey());
</script>
</div>
{% endblock %}

View File

@ -0,0 +1,62 @@
import uuid
import json
import secrets
import webauthn
from app.config import URL as SITE_URL
from urllib.parse import urlparse
from flask import render_template, flash, redirect, url_for, session
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from wtforms import HiddenField, validators
from app.dashboard.base import dashboard_bp
from app.extensions import db
from app.log import LOG
class FidoTokenForm(FlaskForm):
sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
@dashboard_bp.route("/fido_setup", methods=["GET", "POST"])
@login_required
def fido_setup():
if current_user.fido_uuid is not None:
flash("You have already registered your security key", "warning")
return redirect(url_for("dashboard.index"))
fido_token_form = FidoTokenForm()
# Prepare infomation for key registration process
rp_id = urlparse(SITE_URL).hostname
fido_uuid = str(uuid.uuid4())
challenge = secrets.token_urlsafe(32)
credential_create_options = webauthn.WebAuthnMakeCredentialOptions(
challenge, 'Simple Login', rp_id, fido_uuid,
current_user.email, current_user.name, False, attestation='none')
# Don't think this one should be used, but it's not configurable by arguments
# https://www.w3.org/TR/webauthn/#sctn-location-extension
registration_dict = credential_create_options.registration_dict
del registration_dict['extensions']['webauthn.loc']
session['fido_uuid'] = fido_uuid
session['fido_challenge'] = challenge.rstrip('=')
if fido_token_form.validate_on_submit():
sk_assertion = fido_token_form.sk_assertion.data
LOG.d(sk_assertion)
# if totp.verify(token):
# current_user.enable_otp = True
# db.session.commit()
# flash("Security key has been activated", "success")
# return redirect(url_for("dashboard.index"))
# else:
# flash("Incorrect challenge", "warning")
return render_template(
"dashboard/fido_setup.html", fido_token_form=fido_token_form,
credential_create_options=registration_dict
)

View File

@ -134,6 +134,10 @@ class User(db.Model, ModelMixin, UserMixin):
db.Boolean, nullable=False, default=False, server_default="0"
)
# Fields for WebAuthn
fido_uuid = db.Column(db.String(), nullable=True, unique=True)
fido_pk = db.Column(db.String(), nullable=True)
# some users could have lifetime premium
lifetime = db.Column(db.Boolean, default=False, nullable=False, server_default="0")

118
static/assets/js/vendors/base64.js vendored Normal file
View File

@ -0,0 +1,118 @@
var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
;(function (exports) {
'use strict'
var Arr = (typeof Uint8Array !== 'undefined')
? Uint8Array
: Array
var PLUS = '+'.charCodeAt(0)
var SLASH = '/'.charCodeAt(0)
var NUMBER = '0'.charCodeAt(0)
var LOWER = 'a'.charCodeAt(0)
var UPPER = 'A'.charCodeAt(0)
var PLUS_URL_SAFE = '-'.charCodeAt(0)
var SLASH_URL_SAFE = '_'.charCodeAt(0)
function decode (elt) {
var code = elt.charCodeAt(0)
if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
if (code < NUMBER) return -1 // no match
if (code < NUMBER + 10) return code - NUMBER + 26 + 26
if (code < UPPER + 26) return code - UPPER
if (code < LOWER + 26) return code - LOWER + 26
}
function b64ToByteArray (b64) {
var i, j, l, tmp, placeHolders, arr
if (b64.length % 4 > 0) {
throw new Error('Invalid string. Length must be a multiple of 4')
}
// the number of equal signs (place holders)
// if there are two placeholders, than the two characters before it
// represent one byte
// if there is only one, then the three characters before it represent 2 bytes
// this is just a cheap hack to not do indexOf twice
var len = b64.length
placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0
// base64 is 4/3 + up to two characters of the original data
arr = new Arr(b64.length * 3 / 4 - placeHolders)
// if there are placeholders, only get up to the last complete 4 chars
l = placeHolders > 0 ? b64.length - 4 : b64.length
var L = 0
function push (v) {
arr[L++] = v
}
for (i = 0, j = 0; i < l; i += 4, j += 3) {
tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
push((tmp & 0xFF0000) >> 16)
push((tmp & 0xFF00) >> 8)
push(tmp & 0xFF)
}
if (placeHolders === 2) {
tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
push(tmp & 0xFF)
} else if (placeHolders === 1) {
tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
push((tmp >> 8) & 0xFF)
push(tmp & 0xFF)
}
return arr
}
function uint8ToBase64 (uint8) {
var i
var extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
var output = ''
var temp, length
function encode (num) {
return lookup.charAt(num)
}
function tripletToBase64 (num) {
return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
}
// go through the array every three bytes, we'll deal with trailing stuff later
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
output += tripletToBase64(temp)
}
// pad the end with zeros, but make sure to not forget the extra bytes
switch (extraBytes) {
case 1:
temp = uint8[uint8.length - 1]
output += encode(temp >> 2)
output += encode((temp << 4) & 0x3F)
output += '=='
break
case 2:
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
output += encode(temp >> 10)
output += encode((temp >> 4) & 0x3F)
output += encode((temp << 2) & 0x3F)
output += '='
break
default:
break
}
return output
}
exports.toByteArray = b64ToByteArray
exports.fromByteArray = uint8ToBase64
}(typeof exports === 'undefined' ? (this.base64js = {}) : exports))

131
static/assets/js/vendors/webauthn.js vendored Normal file
View File

@ -0,0 +1,131 @@
// Copyright (c) 2017 Duo Security, Inc. All rights reserved.
// Under BSD 3-Clause "New" or "Revised" License
// https://github.com/duo-labs/py_webauthn/
function b64enc(buf) {
return base64js
.fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function b64RawEnc(buf) {
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
}
function hexEncode(buf) {
return Array.from(buf)
.map(function (x) {
return ("0" + x.toString(16)).substr(-2);
})
.join("");
}
const transformCredentialRequestOptions = (
credentialRequestOptionsFromServer
) => {
let { challenge, allowCredentials } = credentialRequestOptionsFromServer;
challenge = Uint8Array.from(
atob(challenge.replace(/\_/g, "/").replace(/\-/g, "+")),
(c) => c.charCodeAt(0)
);
allowCredentials = allowCredentials.map((credentialDescriptor) => {
let { id } = credentialDescriptor;
id = id.replace(/\_/g, "/").replace(/\-/g, "+");
id = Uint8Array.from(atob(id), (c) => c.charCodeAt(0));
return Object.assign({}, credentialDescriptor, { id });
});
const transformedCredentialRequestOptions = Object.assign(
{},
credentialRequestOptionsFromServer,
{ challenge, allowCredentials }
);
return transformedCredentialRequestOptions;
};
/**
* Transforms items in the credentialCreateOptions generated on the server
* into byte arrays expected by the navigator.credentials.create() call
* @param {Object} credentialCreateOptionsFromServer
*/
const transformCredentialCreateOptions = (
credentialCreateOptionsFromServer
) => {
let { challenge, user } = credentialCreateOptionsFromServer;
user.id = Uint8Array.from(
atob(
credentialCreateOptionsFromServer.user.id
.replace(/\_/g, "/")
.replace(/\-/g, "+")
),
(c) => c.charCodeAt(0)
);
challenge = Uint8Array.from(
atob(
credentialCreateOptionsFromServer.challenge
.replace(/\_/g, "/")
.replace(/\-/g, "+")
),
(c) => c.charCodeAt(0)
);
const transformedCredentialCreateOptions = Object.assign(
{},
credentialCreateOptionsFromServer,
{ challenge, user }
);
return transformedCredentialCreateOptions;
};
/**
* Transforms the binary data in the credential into base64 strings
* for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
const transformNewAssertionForServer = (newAssertion) => {
const attObj = new Uint8Array(newAssertion.response.attestationObject);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const registrationClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: b64enc(rawId),
type: newAssertion.type,
attObj: b64enc(attObj),
clientData: b64enc(clientDataJSON),
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
};
};
/**
* Encodes the binary data in the assertion into strings for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
const transformAssertionForServer = (newAssertion) => {
const authData = new Uint8Array(newAssertion.response.authenticatorData);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const sig = new Uint8Array(newAssertion.response.signature);
const assertionClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: b64enc(rawId),
type: newAssertion.type,
authData: b64RawEnc(authData),
clientData: b64RawEnc(clientDataJSON),
signature: hexEncode(sig),
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
};
};