1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-01-11 05:17:41 +03:00

* F #962: Sunstone 2FA with WebAuthn

This strengthens the login with e.g. U2F/FIDO2 authentication keys.

Signed-off-by: Dennis Felsch <dennis.felsch@ruhr-uni-bochum.de>
(cherry picked from commit 487a6247a9)
This commit is contained in:
Dennis Felsch 2020-04-27 18:43:45 +02:00 committed by Tino Vazquez
parent 6f0ec36f6f
commit fb1294b386
No known key found for this signature in database
GPG Key ID: 2FE9C32E94AEABBE
32 changed files with 666 additions and 109 deletions

View File

@ -36,6 +36,7 @@ end
if RUBY_VERSION >= '2.4.0'
gem 'xmlrpc'
gem 'webauthn' # sunstone
end
if RUBY_VERSION < '2.1'

View File

@ -97,6 +97,40 @@
# Two Factor Authentication Issuer Label
:two_factor_auth_issuer: opennebula
################################################################################
# WebAuthn
################################################################################
# This value needs to match `window.location.origin` evaluated by the User Agent
# during registration and authentication ceremonies. Remember that WebAuthn
# requires TLS on anything else than localhost.
:webauthn_origin: http://localhost:9869
# Relying Party name for display purposes
:webauthn_rpname: 'OpenNebula Cloud'
# Optional client timeout hint, in milliseconds. Specifies how long the browser
# should wait for any interaction with the user.
:webauthn_timeout: 60000
# Optional differing Relying Party ID
# See https://www.w3.org/TR/webauthn/#relying-party-identifier
# :webauthn_rpid: example.com
# Supported cryptographic algorithms
# See https://www.iana.org/assignments/jose/jose.xhtml
# Possible is any list of
# ES256 | ES384 | ES512 | PS256 | PS384 | PS512 | RS256 | RS384 | RS512 | RS1
# :webauthn_algorithms: [ES256, PS256, RS256]
################################################################################
# Check Upgrades
################################################################################
# To check for the latest release. Comment this value if you don't want to check
# this.
:remote_version: http://downloads.opennebula.org/latest
################################################################################
# UI Settings
################################################################################

View File

@ -0,0 +1,155 @@
# -------------------------------------------------------------------------- #
# Copyright 2002-2019, OpenNebula Project, OpenNebula Systems #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
# not use this file except in compliance with the License. You may obtain #
# a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
#--------------------------------------------------------------------------- #
require 'json'
require 'webauthn'
# WebAuthn authentication
module SunstoneWebAuthn
def self.configure(conf)
@client = OpenNebula::Client.new
@challenges = Hash.new
WebAuthn.configure do |config|
if !conf.include?(:webauthn_origin) || conf[:webauthn_origin] == ''
raise StandardError.new("Configuration of ':webauthn_origin' is missing")
end
config.origin = conf[:webauthn_origin]
if !conf.include?(:webauthn_rpname) || conf[:webauthn_rpname] == ''
raise StandardError.new("Configuration of ':webauthn_rpname' is missing")
end
config.rp_name = conf[:webauthn_rpname]
conf[:webauthn_timeout] ||= 60000
config.credential_options_timeout = conf[:webauthn_timeout]
if conf.include?(:webauthn_rpid) && conf[:webauthn_rpid] != ''
config.rp_id = conf[:webauthn_rpid]
end
conf[:webauthn_algorithms] ||= ["ES256", "PS256", "RS256"]
config.algorithms = conf[:webauthn_algorithms]
end
end
def self.getOptionsForCreate(user_id, user_name)
known_credentials = getCredentialIDsForUser(user_id)
options = WebAuthn::Credential.options_for_create(
user: { id: Base64.urlsafe_encode64(user_id, padding: false), name: user_name },
authenticator_selection: { user_verification: 'discouraged' },
exclude: known_credentials
)
@challenges[user_id] = options.challenge
return renderJSON(options.as_json)
end
def self.getOptionsForGet(user_id)
known_credentials = getCredentialIDsForUser(user_id)
unless known_credentials.length == 0
options = WebAuthn::Credential.options_for_get(
user_verification: 'discouraged',
allow: known_credentials
)
@challenges[user_id] = options.challenge
return renderJSON(options.as_json)
end
end
def self.getCredentialsForUser(user_id)
user = User.new_with_id(user_id, @client)
rc = user.info
if OpenNebula.is_error?(rc)
$cloud_auth.logger.error {"user.info error: #{rc.message}"}
return nil
end
credentials = []
json_str = user['TEMPLATE/SUNSTONE/WEBAUTHN_CREDENTIALS']
if json_str.nil?
return credentials
end
begin
credentials = JSON.parse(json_str.gsub("'", '"'))['cs']
rescue Exception => e
return OpenNebula::Error.new(e.message)
end
return credentials
end
def self.getCredentialIDsForUser(user_id)
return getCredentialsForUser(user_id).map { |hash| hash['id'] }
end
def self.verifyCredentialFromRegistration(user_id, publicKeyCredential)
credential_with_attestation = WebAuthn::Credential.from_create(publicKeyCredential)
challenge = @challenges.delete(user_id.to_s)
begin
credential_with_attestation.verify(challenge)
return credential_with_attestation
rescue WebAuthn::Error => e
return OpenNebula::Error.new(e.message)
end
end
def self.authenticate(user_id, publicKeyCredential_s)
begin
publicKeyCredential = JSON.parse(publicKeyCredential_s)
received_credential = WebAuthn::Credential.from_get(publicKeyCredential)
rescue Exception => e
return false
end
credentials = getCredentialsForUser(user_id)
stored_credential = credentials.find { |hash| hash['id'] == publicKeyCredential['id'] }
challenge = @challenges.delete(user_id.to_s)
begin
received_credential.verify(challenge, public_key: stored_credential['pk'], sign_count: Integer(stored_credential['cnt']))
stored_credential['cnt'] = received_credential.sign_count
updateCredentials(user_id, credentials)
return true
rescue WebAuthn::SignCountVerificationError => e
# Cryptographic verification of the authenticator data succeeded, but the signature counter was less then or equal
# to the stored value. This can have several reasons and depending on risk tolerance an implementation can choose to fail or
# pass authentication. For more information see https://www.w3.org/TR/webauthn/#sign-counter. Here, we fail authentication.
return false
rescue WebAuthn::Error => e
return false
end
return false
end
def self.updateCredentials(user_id, credentials)
user = User.new_with_id(user_id, @client)
rc = user.info
if OpenNebula.is_error?(rc)
$cloud_auth.logger.error {"user.info error: #{rc.message}"}
return nil
end
credentialsJSON = JSON.generate({ "cs" => credentials}).gsub('"', "'")
# This does not work; breaks the template (guess the replace function is buggy)
# user.replace({'WEBAUTHN_CREDENTIALS' => credentialsJSON}, 'TEMPLATE/SUNSTONE')
user.retrieve_xmlelements('TEMPLATE/SUNSTONE/WEBAUTHN_CREDENTIALS')[0].set_content(credentialsJSON)
user.update(user.template_like_str('TEMPLATE'))
end
def self.renderJSON(hash)
return JSON.generate(hash)
end
end

View File

@ -14,9 +14,15 @@
# limitations under the License. #
#--------------------------------------------------------------------------- #
require 'json'
require 'OpenNebulaJSON/JSONUtils'
require 'sunstone_2f_auth'
begin
require "SunstoneWebAuthn"
rescue LoadError
end
module OpenNebulaJSON
class UserJSON < OpenNebula::User
include JSONUtils
@ -46,6 +52,8 @@ module OpenNebulaJSON
when "update" then self.update(action_hash['params'])
when "enable_two_factor_auth" then self.enable_two_factor_auth(action_hash['params'])
when "disable_two_factor_auth" then self.disable_two_factor_auth(action_hash['params'])
when "enable_security_key" then self.enable_security_key(action_hash['params'])
when "disable_security_key" then self.disable_security_key(action_hash['params'])
when "set_quota" then self.set_quota(action_hash['params'])
when "addgroup" then self.addgroup(action_hash['params'])
when "delgroup" then self.delgroup(action_hash['params'])
@ -93,6 +101,46 @@ module OpenNebulaJSON
def disable_two_factor_auth(params=Hash.new)
sunstone_setting = params["current_sunstone_setting"]
sunstone_setting.delete("TWO_FACTOR_AUTH_SECRET")
if !params['delete_all'].nil? && params['delete_all'] == true
sunstone_setting.delete("WEBAUTHN_CREDENTIALS")
end
sunstone_setting = { "sunstone" => sunstone_setting }
template_raw = template_to_str_sunstone_with_explicite_empty_value(sunstone_setting)
update_params = { "template_raw" => template_raw, "append" => true }
update(update_params)
end
def enable_security_key(params=Hash.new)
if !$conf[:webauthn_avail]
return OpenNebula::Error.new("WebAuthn not available.")
end
sunstone_setting = params["current_sunstone_setting"]
webauthn_credential = SunstoneWebAuthn.verifyCredentialFromRegistration(@pe_id, params['publicKeyCredential'])
template_credentials = self['TEMPLATE/SUNSTONE/WEBAUTHN_CREDENTIALS'] || "{'cs':[]}"
credentials = parse_json(template_credentials.gsub("'", '"'), 'cs')
credentials.append({
'id' => webauthn_credential.id,
'pk' => webauthn_credential.public_key,
'cnt' => webauthn_credential.sign_count,
'name' => params['nickname']
})
sunstone_setting["WEBAUTHN_CREDENTIALS"] = JSON.generate({ "cs" => credentials}).gsub('"', "'")
sunstone_setting = { "sunstone" => sunstone_setting }
template_raw = template_to_str_sunstone_with_explicite_empty_value(sunstone_setting)
update_params = { "template_raw" => template_raw, "append" => true }
update(update_params)
end
def disable_security_key(params=Hash.new)
sunstone_setting = params["current_sunstone_setting"]
credentials = parse_json(sunstone_setting["WEBAUTHN_CREDENTIALS"].gsub("'", '"'), 'cs')
for i in 0..credentials.length() do
if credentials[i]["pk"] == params["tokenid_to_remove"]
credentials.delete_at(i)
break
end
end
sunstone_setting["WEBAUTHN_CREDENTIALS"] = JSON.generate({ "cs" => credentials}).gsub('"', "'")
sunstone_setting = { "sunstone" => sunstone_setting }
template_raw = template_to_str_sunstone_with_explicite_empty_value(sunstone_setting)
update_params = { "template_raw" => template_raw, "append" => true }

View File

@ -17,11 +17,15 @@
define(function(require) {
require('../bower_components/jquery/dist/jquery.min');
var OpenNebulaAuth = require('opennebula/auth');
var WebAuthnJSON = require('../bower_components/webauthn-json/dist/index');
var showErrorAuth = false;
var uid;
var textOpenNebulaNotRunning = "OpenNebula is not running or there was a server exception. Please check the server logs.";
var textInvalidUserorPassword = "Invalid username or password";
var textNoAnswerFromServer = "No answer from server. Is it running?";
var textTwoFactorTokenInvalid = "Two factor Token Invalid";
var textTwoFactorTokenInvalid = "Invalid second factor authentication";
var idElementTwoFactor = "#two_factor_auth_token";
function auth_success(req, response) {
@ -29,6 +33,9 @@ define(function(require) {
$("#login_form").hide();
$("#login_spinner").hide();
$("#two_factor_auth").fadeIn("slow");
$("#two_factor_auth_token").focus();
$("#login_btn")[0].type = "button";
$("#two_factor_auth_login_btn")[0].type = "submit";
if(!showErrorAuth){
showErrorAuth = true;
} else {
@ -36,19 +43,57 @@ define(function(require) {
$("#error_box").fadeIn("slow");
$("#login_spinner").hide();
}
uid = response.uid;
prepareWebAuthn(uid);
} else {
showErrorAuth = false;
window.location.href = ".";
}
}
function prepareWebAuthn(uid) {
$("#webauthn_login_btn").unbind();
$.ajax({
url: "webauthn_options_for_get?uid=" + uid,
type: "GET",
dataType: "json",
success: function (response) {
if (!response) {
return
}
if (!navigator.credentials) {
$("#webauthn_login_div").hide();
console.warn('WebAuthn functionality unavailable. Ask your cloud administrator to enable TLS.');
}
$("#webauthn_login_btn").click(function () {
WebAuthnJSON.get({ "publicKey": response }).then(authenticate)
.catch((e) => {
$("#error_message").text(e.message);
$("#error_box").fadeIn("slow");
$("#login_spinner").hide();
});
});
},
error: function (response) {
if (response.status == 501) {
$("#webauthn_login_div").hide();
console.warn('WebAuthn functionality unavailable. Ask your cloud administrator to upgrade the Ruby version.');
}
}
});
}
function auth_error(req, error) {
var status = error.error.http_status;
switch (status){
case 401:
$("#error_message").text(textInvalidUserorPassword);
if (showErrorAuth) {
$("#error_message").text(textTwoFactorTokenInvalid);
} else {
$("#error_message").text(textInvalidUserorPassword);
}
break;
case 500:
$("#error_message").text(textOpenNebulaNotRunning);
@ -63,11 +108,22 @@ define(function(require) {
$("#login_spinner").hide();
}
function authenticate() {
function authenticate(publicKeyCredential) {
var username = $("#username").val();
var password = $("#password").val();
var remember = $("#check_remember").is(":checked");
var two_factor_auth_token = $("#two_factor_auth_token").val();
var two_factor_auth_token;
var error_callback;
if (publicKeyCredential == undefined) {
two_factor_auth_token = $("#two_factor_auth_token").val();
error_callback = auth_error
} else {
two_factor_auth_token = JSON.stringify(publicKeyCredential);
error_callback = (req, error) => {
auth_error(req, error);
prepareWebAuthn(uid);
}
}
$("#error_box").fadeOut("slow");
$("#login_spinner").show();
@ -80,7 +136,7 @@ define(function(require) {
remember: remember,
success: auth_success,
two_factor_auth_token: two_factor_auth_token,
error: auth_error
error: error_callback
});
}
@ -130,7 +186,7 @@ define(function(require) {
return false;
});
$("#two_factor_auth_login").click(function() {
$("#two_factor_auth_login_btn").click(function() {
if($(idElementTwoFactor) && $(idElementTwoFactor).val().length){
authenticate();
}

View File

@ -108,6 +108,14 @@ define(function(require) {
var action_obj = params.data.extra_param;
OpenNebulaAction.simple_action(params, RESOURCE, "disable_two_factor_auth", action_obj);
},
"enable_sunstone_security_key": function(params) {
var action_obj = params.data.extra_param;
OpenNebulaAction.simple_action(params, RESOURCE, "enable_security_key", action_obj);
},
"disable_sunstone_security_key": function(params) {
var action_obj = params.data.extra_param;
OpenNebulaAction.simple_action(params, RESOURCE, "disable_security_key", action_obj);
},
"accounting" : function(params) {
OpenNebulaAction.monitor(params, RESOURCE, false);
},

View File

@ -98,11 +98,7 @@ define(function(require) {
$("#provision_user_views_select option[value=\"" + config["user_config"]["default_view"] + "\"]", context).attr("selected", "selected");
if (that.element.TEMPLATE.SUNSTONE && that.element.TEMPLATE.SUNSTONE.TWO_FACTOR_AUTH_SECRET) {
$(".provision_two_factor_auth_button", context).html(Locale.tr("Disable"));
} else {
$(".provision_two_factor_auth_button", context).html(Locale.tr("Manage two factor authentication"));
}
$(".provision_two_factor_auth_button", context).html(Locale.tr("Manage two factor authentication"));
// Login token button
context.off("click", ".provision_login_token_button");
@ -116,23 +112,14 @@ define(function(require) {
context.off("click", ".provision_two_factor_auth_button");
context.on("click", ".provision_two_factor_auth_button", function(){
var sunstone_setting = that.element.TEMPLATE.SUNSTONE || {};
if (sunstone_setting.TWO_FACTOR_AUTH_SECRET) {
Sunstone.runAction(
"User.disable_sunstone_two_factor_auth",
that.element.ID,
{current_sunstone_setting: sunstone_setting}
);
} else {
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).setParams({
element: that.element,
sunstone_setting: sunstone_setting
});
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).reset();
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).show();
}
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).setParams({
element: that.element,
sunstone_setting: sunstone_setting
});
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).reset();
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).show();
});
$("#provision_change_password_form").submit(function() {
var pw = $("#provision_new_password", this).val();
var confirm_password = $("#provision_new_confirm_password", this).val();

View File

@ -239,7 +239,7 @@
<div id="provision_two_factor_auth_accordion" class="accordion-content" data-tab-content>
<br/>
<p>
{{tr "Two factor authentication can be enabled for loging into Sunestone UI."}}
{{tr "Two factor authentication can be enabled for logging into Sunestone UI."}}
</p>
<div class="row">
<div class="large-12 columns">

View File

@ -29,6 +29,7 @@ define(function(require) {
var AUTH_DRIVER_DIALOG_ID = require("./dialogs/auth-driver/dialogId");
var QUOTAS_DIALOG_ID = require("./dialogs/quotas/dialogId");
var GROUPS_DIALOG_ID = require("./dialogs/groups/dialogId");
var TWO_FACTOR_AUTH_DIALOG_ID = require('tabs/users-tab/dialogs/two-factor-auth/dialogId');
var RESOURCE = "User";
var XML_ROOT = "USER";
@ -194,6 +195,62 @@ define(function(require) {
error: Notifier.onError
},
"User.enable_sunstone_security_key" : {
type: "single",
call: OpenNebulaResource.enable_sunstone_security_key,
callback: function(request, response) {
OpenNebulaResource.show({
data : {
id: request.request.data[0]
},
success: function(request, response) {
var sunstone_template = {};
if (response[XML_ROOT].TEMPLATE.SUNSTONE) {
$.extend(sunstone_template, response[XML_ROOT].TEMPLATE.SUNSTONE);
}
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).hide();
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).setParams({
element: response[XML_ROOT],
sunstone_setting: sunstone_template
});
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).reset();
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).show();
},
error: Notifier.onError
});
Sunstone.runAction("Settings.refresh");
},
error: Notifier.onError
},
"User.disable_sunstone_security_key" : {
type: "single",
call: OpenNebulaResource.disable_sunstone_security_key,
callback: function(request, response) {
OpenNebulaResource.show({
data : {
id: request.request.data[0]
},
success: function(request, response) {
var sunstone_template = {};
if (response[XML_ROOT].TEMPLATE.SUNSTONE) {
$.extend(sunstone_template, response[XML_ROOT].TEMPLATE.SUNSTONE);
}
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).hide();
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).setParams({
element: response[XML_ROOT],
sunstone_setting: sunstone_template
});
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).reset();
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).show();
},
error: Notifier.onError
});
Sunstone.runAction("Settings.refresh");
},
error: Notifier.onError
},
"User.append_sunstone_setting_refresh" : {
type: "single",
call: function(params){

View File

@ -22,8 +22,8 @@ define(function(require) {
var Sunstone = require('sunstone');
var Notifier = require('utils/notifier');
var Locale = require('utils/locale');
var OpenNebula = require('opennebula');
var ResourceSelect = require('utils/resource-select');
var WebAuthnJSON = require('../../../../bower_components/webauthn-json/dist/index');
/* CONSTANTS */
@ -54,24 +54,111 @@ define(function(require) {
}
function _html() {
var authTokens = [];
if (this.element != undefined && this.element.TEMPLATE.SUNSTONE != undefined) {
var sunstone_setting = this.element.TEMPLATE.SUNSTONE || {};
if (sunstone_setting.TWO_FACTOR_AUTH_SECRET != undefined) {
authTokens.push({ 'ID': 0, 'TYPE': 'Authenticator app (HOTP)', 'NAME': '' });
}
if (sunstone_setting.WEBAUTHN_CREDENTIALS != undefined) {
// The handling of double quotes in JSONUtils.template_to_str() is buggy, WEBAUTHN_CREDENTIALS uses single quotes instead
var credentials = JSON.parse(sunstone_setting.WEBAUTHN_CREDENTIALS.replace(/\'/g, '"')).cs || {};
$.each(credentials, function () {
authTokens.push({ 'ID': this.pk, 'TYPE': 'Security key (FIDO2 / U2F / WebAuthn)', 'NAME': this.name });
});
}
}
return TemplateHTML({
'dialogId': this.dialogId
'dialogId': this.dialogId,
'authTokens': authTokens
});
}
function _setup(context) {
var that = this;
var secret = randomBase32();
$("#secret", context).val(secret);
$('#qr_code', context).html('<img src="'+ '/two_factor_auth_hotp_qr_code?secret=' + secret + '&csrftoken=' + csrftoken + '" width="75%" alt="' + secret + '" />');
$("#enable_btn", context).click(function(){
var secret = $("#secret", context).val();
var token = $("#token", context).val();
Sunstone.runAction(
"User.enable_sunstone_two_factor_auth",
that.element.ID,
{current_sunstone_setting: that.sunstone_setting, secret: secret, token: token}
);
var sunstone_setting = this.element != undefined ? this.element.TEMPLATE.SUNSTONE : {};
context.on("click", "i.remove-tab", function(){
var tr = $(this).closest("tr");
$(this).closest("td").html("<i class=\"fas fa-spinner fa-spin\"/>");
var tokenid = $(tr).attr("data-tokenid");
var sunstone_setting = that.element.TEMPLATE.SUNSTONE || {};
if (tokenid == 0) {
Sunstone.runAction(
"User.disable_sunstone_two_factor_auth",
that.element.ID,
{ current_sunstone_setting: sunstone_setting }
);
} else {
Sunstone.runAction(
"User.disable_sunstone_security_key",
that.element.ID,
{ current_sunstone_setting: sunstone_setting, tokenid_to_remove: tokenid }
);
}
});
// Register authenticator app button
if (sunstone_setting != undefined && sunstone_setting.TWO_FACTOR_AUTH_SECRET != undefined) {
$("#register_authenticator_app", context).prop("disabled", true);
$("#register_authenticator_app", context).html(Locale.tr("Authenticator app already registered"));
}
context.off("click", "#register_authenticator_app");
context.on("click", "#register_authenticator_app", function () {
$("#authenticator_app_div").show();
$("#security_key_div").hide();
var secret = randomBase32();
$("#secret", context).val(secret);
$('#qr_code', context).html('<img src="' + '/two_factor_auth_hotp_qr_code?secret=' + secret + '&csrftoken=' + csrftoken + '" width="75%" alt="' + secret + '" />');
$("#enable_btn", context).click(function () {
var secret = $("#secret", context).val();
var token = $("#token", context).val();
Sunstone.runAction(
"User.enable_sunstone_two_factor_auth",
that.element.ID,
{ current_sunstone_setting: that.sunstone_setting, secret: secret, token: token }
);
});
});
context.off("click", "#register_security_key");
context.on("click", "#register_security_key", function () {
$("#authenticator_app_div").hide();
$("#security_key_div").show();
context.off("click", "#add_btn");
$.ajax({
url: "webauthn_options_for_create",
type: "GET",
dataType: "json",
success: function (response) {
if (!navigator.credentials) {
$("#security_key_div").hide();
Notifier.onError(null, { error: { message: 'WebAuthn functionality unavailable. Ask your cloud administrator to enable TLS.' } });
return;
}
context.on("click", "#add_btn", function () {
WebAuthnJSON.create({ "publicKey": response }).then(publicKeyCredential => {
var nickname = $("#nickname", context).val();
Sunstone.runAction(
"User.enable_sunstone_security_key",
that.element.ID,
{ nickname: nickname, publicKeyCredential: publicKeyCredential, current_sunstone_setting: that.sunstone_setting }
);
})
.catch((e) => {
Notifier.onError(null, { error: { message: e.message } });
});
});
},
error: function (response) {
if (response.status == 501) {
$("#security_key_div").hide();
Notifier.onError(null, { error: { message: 'WebAuthn functionality unavailable. Ask your cloud administrator to upgrade the Ruby version.' } });
}
}
});
});
return false;

View File

@ -26,11 +26,45 @@
<div class="large-12 columns">
<br/>
<p>
{{tr "Two factor authentication can be enabled for loging into Sunestone UI."}}
{{tr "Two factor authentication can be enabled for logging into Sunstone UI."}}
</p>
</div>
</div>
{{#if authTokens}}
<div class="row">
<div class="large-12 columns">
<table>
<thead>
<tr>
<th>{{tr "Authenticator Type"}}</th>
<th>{{tr "Authenticator Name"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each authTokens}}
<tr data-tokenid="{{ID}}">
<td>{{tr TYPE}}</td>
<td>{{NAME}}</td>
<td>
<a href="#"><i class="fas fa-times-circle remove-tab"></i></a>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
{{/if}}
<div class="form_buttons row columns">
<button id="register_authenticator_app" type="button" class="button radius left">
{{tr "Register authenticator app"}}
</button>
<button id="register_security_key" type="button" class="button radius left">
{{tr "Register new security key"}}
</button>
</div>
<div id="authenticator_app_div" class="row fieldset" style="display: none;">
<div class="large-6 columns">
<div id="qr_code"></div>
</div>
@ -60,6 +94,22 @@
</ul>
</div>
</div>
<div id="security_key_div" class="row fieldset" style="display: none;">
<div class="row">
<div class="large-4 medium-6 columns">
<label>
{{tr "Nickname for this security key"}}
<input id="nickname" value="" type="text" class="box" />
</label>
</div>
<div class="large-4 medium-6 columns end">
<label>&nbsp;</label>
<button id="add_btn" type="button" class="button radius">
{{tr "Add"}}
</button>
</div>
</div>
</div>
<button class="close-button" data-close aria-label="{{tr "Close modal"}}" type="button">
<span aria-hidden="true">&times;</span>
</button>

View File

@ -97,27 +97,27 @@ define(function(require) {
//===
// Change two factor auth
if (that.element.TEMPLATE.SUNSTONE && that.element.TEMPLATE.SUNSTONE.TWO_FACTOR_AUTH_SECRET) {
$("#manage_two_factor_auth", context).html(Locale.tr("Disable"));
if (that.element.ID == config['user_id']) {
$("#manage_two_factor_auth", context).html(Locale.tr("Manage two factor authentication"));
} else {
if (that.element.ID == config['user_id']) {
$("#manage_two_factor_auth", context).html(Locale.tr("Manage two factor authentication"));
if (that.element.TEMPLATE.SUNSTONE && (that.element.TEMPLATE.SUNSTONE.TWO_FACTOR_AUTH_SECRET || (that.element.TEMPLATE.SUNSTONE.WEBAUTHN_CREDENTIALS != undefined && that.element.TEMPLATE.SUNSTONE.WEBAUTHN_CREDENTIALS != "{'cs':[]}"))) {
$("#manage_two_factor_auth", context).html(Locale.tr("Disable all authenticators"));
} else {
$("#manage_two_factor_auth", context).prop("disabled", true);
$("#manage_two_factor_auth", context).html(Locale.tr("No"));
}
}
context.off("click", "#manage_two_factor_auth");
context.on("click", "#manage_two_factor_auth", function() {
var sunstone_setting = that.element.TEMPLATE.SUNSTONE || {};
if (sunstone_setting.TWO_FACTOR_AUTH_SECRET) {
context.on("click", "#manage_two_factor_auth", function () {
var sunstone_setting = that.element.TEMPLATE.SUNSTONE || {};
if (that.element.ID != config['user_id'] && (sunstone_setting.TWO_FACTOR_AUTH_SECRET || (sunstone_setting.WEBAUTHN_CREDENTIALS != undefined && sunstone_setting.WEBAUTHN_CREDENTIALS != "{'cs':[]}"))) {
Sunstone.runAction(
"User.disable_sunstone_two_factor_auth",
that.element.ID,
{current_sunstone_setting: sunstone_setting}
{ current_sunstone_setting: sunstone_setting, delete_all: true }
);
} else {
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).setParams({element: that.element, sunstone_setting: sunstone_setting});
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).setParams({ element: that.element, sunstone_setting: sunstone_setting });
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).reset();
Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).show();
}

View File

@ -35,7 +35,7 @@
</tr>
{{#isTabActionEnabled tabId "User.two_factor_auth"}}
<tr>
<td class="key_td">{{tr "Two factor authtentication"}}</td>
<td class="key_td">{{tr "Two factor authentication"}}</td>
<td class="value_td" colspan="2">
<button id="manage_two_factor_auth" type="button" class="button small radius secondary" style="min-width:80%">
{{tr "Manage two factor authentication"}}

View File

@ -19,6 +19,7 @@
"sprintf": "1.0.3",
"jquery-ui": "^1.12.1",
"wickedpicker": "https://github.com/OpenNebula/sunstone-deps.git#9398b3f"
"webauthn-json": "https://registry.npmjs.org/@github/webauthn-json/-/webauthn-json-0.4.1.tgz"
},
"authors": [
"Daniel Molina <dmolina@opennebula.org>",

View File

@ -5710,7 +5710,7 @@ msgstr "Dvoufázové ověřování"
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr "Pro přihlašování do uživatelského rozhraní Sunstone lze zapnout dvoufázové ověřování."
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5803,7 +5803,7 @@ msgid "Authentication driver"
msgstr "Ovladač ověřování"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr "Dvoufázové ověřování"
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5703,7 +5703,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5796,7 +5796,7 @@ msgid "Authentication driver"
msgstr "Autentifikationsdriver"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr ""
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5726,7 +5726,7 @@ msgstr "Autenticacion en dos pasos"
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr "La autenticacion en dos pasos puede ser habilitada para Sunstone UI."
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5819,7 +5819,7 @@ msgid "Authentication driver"
msgstr "Driver de autenticación"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr "Autenticacion en dos pasos."
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5703,7 +5703,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5796,7 +5796,7 @@ msgid "Authentication driver"
msgstr "Autentimise draiver"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr ""
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5732,7 +5732,7 @@ msgstr "Identification à deux facteurs"
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr "L'identification à deux facteurs peut être activée pour se connecter à Sunstone"
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5825,7 +5825,7 @@ msgid "Authentication driver"
msgstr "Pilote dauthentification"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr "Identification à deux facteurs"
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5708,7 +5708,7 @@ msgstr "二要素認証"
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr "二要素認証はSunstoneユーザインタフェースへのログイン用に有効化できます。"
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5801,7 +5801,7 @@ msgid "Authentication driver"
msgstr "認証ドライバー"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr "二要素認証"
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5713,7 +5713,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5806,7 +5806,7 @@ msgid "Authentication driver"
msgstr "Authenticatie driver"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr ""
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5715,7 +5715,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5808,7 +5808,7 @@ msgid "Authentication driver"
msgstr "Driver de Authenticação"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr "Autênticação de dois fatores"
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5705,7 +5705,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5798,7 +5798,7 @@ msgid "Authentication driver"
msgstr "Driver de autenticação"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr ""
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5724,7 +5724,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5817,7 +5817,7 @@ msgid "Authentication driver"
msgstr "Драйвер аутентификации"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr ""
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5706,7 +5706,7 @@ msgstr "Dvojfaktorová autentifikácia"
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr "Aktivovanie dvojfaktorovej autentifikácie pre prihlásenie sa do Sunstone."
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5799,7 +5799,7 @@ msgid "Authentication driver"
msgstr "Ovládač autentifikácie"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr "Dvojfaktorová autentifikácia"
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5702,7 +5702,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5795,7 +5795,7 @@ msgid "Authentication driver"
msgstr "Authentication driver"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr ""
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5706,7 +5706,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5799,7 +5799,7 @@ msgid "Authentication driver"
msgstr "Kimlik doğrulama sürücüsü"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr ""
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5706,7 +5706,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5799,7 +5799,7 @@ msgid "Authentication driver"
msgstr "Kimlik doğrulama sürücüsü"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr ""
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -5716,7 +5716,7 @@ msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:29
#: ../app/tabs/settings-tab/panels/user-config/html.hbs:233
msgid "Two factor authentication can be enabled for loging into Sunestone UI."
msgid "Two factor authentication can be enabled for logging into Sunestone UI."
msgstr ""
#: ../app/tabs/users-tab/dialogs/two-factor-auth/html.hbs:40
@ -5809,7 +5809,7 @@ msgid "Authentication driver"
msgstr "身份验证驱动器"
#: ../app/tabs/users-tab/panels/auth/html.hbs:38
msgid "Two factor authtentication"
msgid "Two factor authentication"
msgstr ""
#: ../app/tabs/users-tab/panels/auth/html.hbs:51

View File

@ -46,9 +46,16 @@ body#login{
background-image: linear-gradient(180deg, #F6F6F6 0%, rgba(0,0,0,0.2) 51%, rgba(0,0,0,0.3) 100%);
&:after{
content: "Login";
font-weight: bold;
}
&#login_btn:after, &#two_factor_auth_login_btn:after{
content: "Login";
}
&#webauthn_login_btn:after{
content: "Use security key";
}
}
}

View File

@ -112,6 +112,13 @@ require 'CloudAuth'
require 'SunstoneServer'
require 'SunstoneViews'
begin
require "SunstoneWebAuthn"
webauthn_avail = true
rescue LoadError
webauthn_avail = false
end
##############################################################################
# Configuration
##############################################################################
@ -128,6 +135,7 @@ if $conf[:one_xmlrpc_timeout]
end
$conf[:debug_level] ||= 3
$conf[:webauthn_avail] = webauthn_avail
# Set Sunstone Session Timeout
$conf[:session_expire_time] ||= 3600
@ -220,6 +228,17 @@ rescue StandardError => e
exit -1
end
if $conf[:webauthn_avail]
begin
SunstoneWebAuthn.configure($conf)
rescue => e
logger.error {
"Error initializing WebAuthn" }
logger.error { e.message }
exit -1
end
end
#start VNC proxy
$vnc = OpenNebulaVNC.new($conf, logger)
@ -234,7 +253,6 @@ $addons = OpenNebulaAddons.new(logger)
DEFAULT_TABLE_ORDER = "desc"
DEFAULT_PAGE_LENGTH = 10
DEFAULT_TWO_FACTOR_AUTH = false
SUPPORT = {
:zendesk_url => "https://opennebula.zendesk.com/api/v2",
@ -345,21 +363,24 @@ helpers do
end
# two factor_auth
two_factor_auth =
if user[TWO_FACTOR_AUTH_SECRET_XPATH]
user[TWO_FACTOR_AUTH_SECRET_XPATH] != ""
else
DEFAULT_TWO_FACTOR_AUTH
end
if two_factor_auth
isHOTPConfigured = (user[TWO_FACTOR_AUTH_SECRET_XPATH] && user[TWO_FACTOR_AUTH_SECRET_XPATH] != "")
isWebAuthnConfigured = $conf[:webauthn_avail] && SunstoneWebAuthn.getCredentialIDsForUser(user.id).length > 0
if isHOTPConfigured || isWebAuthnConfigured
two_factor_auth_token = params[:two_factor_auth_token]
if !two_factor_auth_token || two_factor_auth_token == ""
return [202, { code: "two_factor_auth" }.to_json]
else
unless Sunstone2FAuth.authenticate(user[TWO_FACTOR_AUTH_SECRET_XPATH], two_factor_auth_token)
logger.info { "Unauthorized two factor authentication login attempt" }
return [401, ""]
end
return [202, { code: "two_factor_auth", uid: user.id }.to_json]
end
serverResponse =
isTwoFactorAuthSuccessful = false
if isHOTPConfigured && Sunstone2FAuth.authenticate(user[TWO_FACTOR_AUTH_SECRET_XPATH], two_factor_auth_token)
isTwoFactorAuthSuccessful = true
end
if isWebAuthnConfigured && SunstoneWebAuthn.authenticate(user.id, two_factor_auth_token)
isTwoFactorAuthSuccessful = true
end
if !isTwoFactorAuthSuccessful
logger.info { "Unauthorized two factor authentication login attempt" }
return [401, "Two factor authentication failed"]
end
end
@ -474,7 +495,7 @@ before do
@request_body = request.body.read
request.body.rewind
unless %w(/ /login /vnc /spice /version).include?(request.path)
unless %w(/ /login /vnc /spice /version /webauthn_options_for_get).include?(request.path)
halt [401, "csrftoken"] unless authorized? && valid_csrftoken?
end
@ -546,7 +567,7 @@ before do
end
after do
unless request.path=='/login' || request.path=='/' || request.path=='/'
unless request.path == '/login' || request.path == '/' || request.path == '/'
# secure cookies
if request.scheme == 'https'
env['rack.session.options'][:secure] = true
@ -611,6 +632,32 @@ get '/two_factor_auth_hotp_qr_code' do
[200, qr_code.as_svg]
end
get '/webauthn_options_for_create' do
content_type 'application/json'
if !$conf[:webauthn_avail]
return [501, '']
end
options = SunstoneWebAuthn.getOptionsForCreate(session[:user_id], session[:user])
[200, options]
end
get '/webauthn_options_for_get' do
content_type 'application/json'
if !$conf[:webauthn_avail]
return [501, '']
end
begin
user_id = Integer(params[:uid]).to_s
rescue ArgumentError => e
return [401, '']
end
options = SunstoneWebAuthn.getOptionsForGet(user_id)
if options.nil?
return [204, '']
end
[200, options]
end
get '/vnc' do
content_type 'text/html', :charset => 'utf-8'
if !authorized?

View File

@ -34,19 +34,38 @@
<% end %>
</form>
<div id="two_factor_auth" class="border" style="display: none;">
<div class="border columns small-6 small-centered small-offset-3 text-center" id="login">
<div class="content">
<label class="text-left">
Two Factor Token
<input value="" type="number" maxlength="15" name="two_factor_auth_token" id="two_factor_auth_token" class="box"/>
</label>
<div class="row buttons small-collapse">
<div class="columns small-offset-6 small-6 text-right">
<button id="two_factor_auth_login" type="button"></button>
<form id="two_factor_form" method="post" class="row">
<div class="border columns small-6 small-centered small-offset-3 text-center" id="login">
<div class="content">
<div class="fieldset">
<label class="text-left">
Enter the six-digit code from your authenticator app
<input value="" type="text" maxlength="15" name="two_factor_auth_token" id="two_factor_auth_token" class="box"/>
</label>
<div class="row buttons small-collapse">
<div class="columns small-offset-6 small-6 text-right">
<button id="two_factor_auth_login_btn" type="button"></button>
</div>
</div>
</div>
<div id="webauthn_login_div">
<div class="text-center">
- or -
</div>
<div class="fieldset">
<div class="small-offset-2 small-8 text-center">
When you are ready to authenticate with your security key, press the button below.
<div class="row buttons small-collapse">
<div class="columns text-center">
<button id="webauthn_login_btn" type="button"></button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="small-offset-3 small-6 error-place">
<div id="error_box" class="alert alert-box callout hidden secondary small" style="display: none">