Completed support for hardware key 2-factor auth.

This commit is contained in:
Ylian Saint-Hilaire 2019-02-07 22:30:33 -08:00
parent e5273bd14c
commit e9381b506a
9 changed files with 908 additions and 24 deletions

View File

@ -1440,7 +1440,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
obj.parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otpauth-setup', success: true })); // Report success
// Notify change
// Notify change TODO: Should be done on all sessions/servers for this user.
try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: obj.parent.CloneSafeUser(user) })); } catch (ex) { }
} else {
ws.send(JSON.stringify({ action: 'otpauth-setup', success: false })); // Report fail
@ -1490,7 +1490,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (actionTaken) { obj.parent.db.SetUser(user); }
// Return one time passwords for this user
if (user.otpsecret) { ws.send(JSON.stringify({ action: 'otpauth-getpasswords', passwords: user.otpkeys?user.otpkeys.keys:null })); }
if (user.otpsecret || ((user.otphkeys != null) && (user.otphkeys.length > 0))) {
ws.send(JSON.stringify({ action: 'otpauth-getpasswords', passwords: user.otpkeys ? user.otpkeys.keys : null }));
}
break;
}
case 'otp-hkey-get':
@ -1521,6 +1523,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
user.otphkeys.splice(foundAtIndex, 1);
obj.parent.db.SetUser(user);
}
// Notify change TODO: Should be done on all sessions/servers for this user.
try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: obj.parent.CloneSafeUser(user) })); } catch (ex) { }
break;
}
case 'otp-hkey-yubikey-add':
@ -1585,6 +1590,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
user.otphkeys.push({ name: command.name, publicKey: result.publicKey, keyHandle: result.keyHandle, keyIndex: keyIndex });
obj.parent.db.SetUser(user);
//console.log('KEYS', JSON.stringify(user.otphkeys));
// Notify change TODO: Should be done on all sessions/servers for this user.
try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: obj.parent.CloneSafeUser(user) })); } catch (ex) { }
}
}
break;

753
public/scripts/u2f-api.js Normal file
View File

@ -0,0 +1,753 @@
//Copyright 2014-2015 Google Inc. All rights reserved.
//Use of this source code is governed by a BSD-style
//license that can be found in the LICENSE file or at
//https://developers.google.com/open-source/licenses/bsd
/**
* @fileoverview The U2F api.
*/
'use strict';
if (!window.u2f) {
/**
* Namespace for the U2F api.
* @type {Object}
*/
var u2f = u2f || {};
/**
* FIDO U2F Javascript API Version
* @number
*/
var js_api_version;
/**
* The U2F extension id
* @const {string}
*/
// The Chrome packaged app extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the package Chrome app and does not require installing the U2F Chrome extension.
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
// The U2F Chrome extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the U2F Chrome extension to authenticate.
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
/**
* Message types for messsages to/from the extension
* @const
* @enum {string}
*/
u2f.MessageTypes = {
'U2F_REGISTER_REQUEST': 'u2f_register_request',
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
'U2F_SIGN_REQUEST': 'u2f_sign_request',
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
};
/**
* Response status codes
* @const
* @enum {number}
*/
u2f.ErrorCodes = {
'OK': 0,
'OTHER_ERROR': 1,
'BAD_REQUEST': 2,
'CONFIGURATION_UNSUPPORTED': 3,
'DEVICE_INELIGIBLE': 4,
'TIMEOUT': 5
};
/**
* A message for registration requests
* @typedef {{
* type: u2f.MessageTypes,
* appId: ?string,
* timeoutSeconds: ?number,
* requestId: ?number
* }}
*/
u2f.U2fRequest;
/**
* A message for registration responses
* @typedef {{
* type: u2f.MessageTypes,
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
* requestId: ?number
* }}
*/
u2f.U2fResponse;
/**
* An error object for responses
* @typedef {{
* errorCode: u2f.ErrorCodes,
* errorMessage: ?string
* }}
*/
u2f.Error;
/**
* Data object for a single sign request.
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
*/
u2f.Transport;
/**
* Data object for a single sign request.
* @typedef {Array<u2f.Transport>}
*/
u2f.Transports;
/**
* Data object for a single sign request.
* @typedef {{
* version: string,
* challenge: string,
* keyHandle: string,
* appId: string
* }}
*/
u2f.SignRequest;
/**
* Data object for a sign response.
* @typedef {{
* keyHandle: string,
* signatureData: string,
* clientData: string
* }}
*/
u2f.SignResponse;
/**
* Data object for a registration request.
* @typedef {{
* version: string,
* challenge: string
* }}
*/
u2f.RegisterRequest;
/**
* Data object for a registration response.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: Transports,
* appId: string
* }}
*/
u2f.RegisterResponse;
/**
* Data object for a registered key.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: ?Transports,
* appId: ?string
* }}
*/
u2f.RegisteredKey;
/**
* Data object for a get API register response.
* @typedef {{
* js_api_version: number
* }}
*/
u2f.GetJsApiVersionResponse;
//Low level MessagePort API support
/**
* Sets up a MessagePort to the U2F extension using the
* available mechanisms.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
*/
u2f.getMessagePort = function (callback) {
if (typeof chrome != 'undefined' && chrome.runtime) {
// The actual message here does not matter, but we need to get a reply
// for the callback to run. Thus, send an empty signature request
// in order to get a failure response.
var msg = {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: []
};
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function () {
if (!chrome.runtime.lastError) {
// We are on a whitelisted origin and can talk directly
// with the extension.
u2f.getChromeRuntimePort_(callback);
} else {
// chrome.runtime was available, but we couldn't message
// the extension directly, use iframe
u2f.getIframePort_(callback);
}
});
} else if (u2f.isAndroidChrome_()) {
u2f.getAuthenticatorPort_(callback);
} else if (u2f.isIosChrome_()) {
u2f.getIosPort_(callback);
} else {
// chrome.runtime was not available at all, which is normal
// when this origin doesn't have access to any extensions.
u2f.getIframePort_(callback);
}
};
/**
* Detect chrome running on android based on the browser's useragent.
* @private
*/
u2f.isAndroidChrome_ = function () {
var userAgent = navigator.userAgent;
return userAgent.indexOf('Chrome') != -1 &&
userAgent.indexOf('Android') != -1;
};
/**
* Detect chrome running on iOS based on the browser's platform.
* @private
*/
u2f.isIosChrome_ = function () {
var r = ["iPhone", "iPad", "iPod"];
for (var i in r) { if (navigator.platform == r[i]) { return true; } }
return false;
//return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
};
/**
* Connects directly to the extension via chrome.runtime.connect.
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
* @private
*/
u2f.getChromeRuntimePort_ = function (callback) {
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
{ 'includeTlsChannelId': true });
setTimeout(function () {
callback(new u2f.WrappedChromeRuntimePort_(port));
}, 0);
};
/**
* Return a 'port' abstraction to the Authenticator app.
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
* @private
*/
u2f.getAuthenticatorPort_ = function (callback) {
setTimeout(function () {
callback(new u2f.WrappedAuthenticatorPort_());
}, 0);
};
/**
* Return a 'port' abstraction to the iOS client app.
* @param {function(u2f.WrappedIosPort_)} callback
* @private
*/
u2f.getIosPort_ = function (callback) {
setTimeout(function () {
callback(new u2f.WrappedIosPort_());
}, 0);
};
/**
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
* @param {Port} port
* @constructor
* @private
*/
u2f.WrappedChromeRuntimePort_ = function (port) {
this.port_ = port;
};
/**
* Format and return a sign request compliant with the JS API version supported by the extension.
* @param {Array<u2f.SignRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatSignRequest_ =
function (appId, challenge, registeredKeys, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: challenge,
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: signRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
appId: appId,
challenge: challenge,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Format and return a register request compliant with the JS API version supported by the extension..
* @param {Array<u2f.SignRequest>} signRequests
* @param {Array<u2f.RegisterRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatRegisterRequest_ =
function (appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
for (var i = 0; i < registerRequests.length; i++) {
registerRequests[i].appId = appId;
}
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: registerRequests[0],
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
signRequests: signRequests,
registerRequests: registerRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
appId: appId,
registerRequests: registerRequests,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Posts a message on the underlying channel.
* @param {Object} message
*/
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) {
this.port_.postMessage(message);
};
/**
* Emulates the HTML 5 addEventListener interface. Works only for the
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
function (eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message' || name == 'onmessage') {
this.port_.onMessage.addListener(function (message) {
// Emulate a minimal MessageEvent object
handler({ 'data': message });
});
} else {
console.error('WrappedChromeRuntimePort only supports onMessage');
}
};
/**
* Wrap the Authenticator app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedAuthenticatorPort_ = function () {
this.requestId_ = -1;
this.requestObject_ = null;
}
/**
* Launch the Authenticator intent.
* @param {Object} message
*/
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) {
var intentUrl =
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
';end';
document.location = intentUrl;
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () {
return "WrappedAuthenticatorPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function (eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message') {
var self = this;
/* Register a callback to that executes when
* chrome injects the response. */
window.addEventListener(
'message', self.onRequestUpdate_.bind(self, handler), false);
} else {
console.error('WrappedAuthenticatorPort only supports message');
}
};
/**
* Callback invoked when a response is received from the Authenticator.
* @param function({data: Object}) callback
* @param {Object} message message Object
*/
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
function (callback, message) {
var messageObject = JSON.parse(message.data);
var intentUrl = messageObject['intentURL'];
var errorCode = messageObject['errorCode'];
var responseObject = null;
if (messageObject.hasOwnProperty('data')) {
responseObject = /** @type {Object} */ (
JSON.parse(messageObject['data']));
}
callback({ 'data': responseObject });
};
/**
* Base URL for intents to Authenticator.
* @const
* @private
*/
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
/**
* Wrap the iOS client app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedIosPort_ = function () { };
/**
* Launch the iOS client app request
* @param {Object} message
*/
u2f.WrappedIosPort_.prototype.postMessage = function (message) {
var str = JSON.stringify(message);
var url = "u2f://auth?" + encodeURI(str);
location.replace(url);
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedIosPort_.prototype.getPortType = function () {
return "WrappedIosPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedIosPort_.prototype.addEventListener = function (eventName, handler) {
var name = eventName.toLowerCase();
if (name !== 'message') {
console.error('WrappedIosPort only supports message');
}
};
/**
* Sets up an embedded trampoline iframe, sourced from the extension.
* @param {function(MessagePort)} callback
* @private
*/
u2f.getIframePort_ = function (callback) {
// Create the iframe
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
var iframe = document.createElement('iframe');
iframe.src = iframeOrigin + '/u2f-comms.html';
iframe.setAttribute('style', 'display:none');
document.body.appendChild(iframe);
var channel = new MessageChannel();
var ready = function (message) {
if (message.data == 'ready') {
channel.port1.removeEventListener('message', ready);
callback(channel.port1);
} else {
console.error('First event on iframe port was not "ready"');
}
};
channel.port1.addEventListener('message', ready);
channel.port1.start();
iframe.addEventListener('load', function () {
// Deliver the port to the iframe and initialize
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
});
};
//High-level JS API
/**
* Default extension response timeout in seconds.
* @const
*/
u2f.EXTENSION_TIMEOUT_SEC = 30;
/**
* A singleton instance for a MessagePort to the extension.
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
* @private
*/
u2f.port_ = null;
/**
* Callbacks waiting for a port
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
* @private
*/
u2f.waitingForPort_ = [];
/**
* A counter for requestIds.
* @type {number}
* @private
*/
u2f.reqCounter_ = 0;
/**
* A map from requestIds to client callbacks
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
* |function((u2f.Error|u2f.SignResponse)))>}
* @private
*/
u2f.callbackMap_ = {};
/**
* Creates or retrieves the MessagePort singleton to use.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
* @private
*/
u2f.getPortSingleton_ = function (callback) {
if (u2f.port_) {
callback(u2f.port_);
} else {
if (u2f.waitingForPort_.length == 0) {
u2f.getMessagePort(function (port) {
u2f.port_ = port;
u2f.port_.addEventListener('message',
/** @type {function(Event)} */ (u2f.responseHandler_));
// Careful, here be async callbacks. Maybe.
while (u2f.waitingForPort_.length)
u2f.waitingForPort_.shift()(u2f.port_);
});
}
u2f.waitingForPort_.push(callback);
}
};
/**
* Handles response messages from the extension.
* @param {MessageEvent.<u2f.Response>} message
* @private
*/
u2f.responseHandler_ = function (message) {
var response = message.data;
var reqId = response['requestId'];
if (!reqId || !u2f.callbackMap_[reqId]) {
console.error('Unknown or missing requestId in response.');
return;
}
var cb = u2f.callbackMap_[reqId];
delete u2f.callbackMap_[reqId];
cb(response['responseData']);
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the sign request.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sign = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual sign request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
//console.log("Extension JS API Version: ", js_api_version);
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual sign request in the supported API version.
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
}
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendSignRequest = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function (port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the register request.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.register = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual register request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
//console.log("Extension JS API Version: ", js_api_version);
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual register request in the supported API version.
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
}
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendRegisterRequest = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function (port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatRegisterRequest_(
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches a message to the extension to find out the supported
* JS API version.
* If the user is on a mobile phone and is thus using Google Authenticator instead
* of the Chrome extension, don't send the request and simply return 0.
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.getApiVersion = function (callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function (port) {
// If we are using Android Google Authenticator or iOS client app,
// do not fire an intent to ask which JS API version to use.
if (port.getPortType) {
var apiVersion;
switch (port.getPortType()) {
case 'WrappedIosPort_':
case 'WrappedAuthenticatorPort_':
apiVersion = 1.1;
break;
default:
apiVersion = 0;
break;
}
callback({ 'js_api_version': apiVersion });
return;
}
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var req = {
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
requestId: reqId
};
port.postMessage(req);
});
};
}

File diff suppressed because one or more lines are too long

View File

@ -25,6 +25,7 @@
<script type="text/javascript" src="scripts/agent-redir-rtc-0.1.0.js"></script>
<script type="text/javascript" src="scripts/agent-desktop-0.0.2.js"></script>
<script type="text/javascript" src="scripts/qrcode.min.js"></script>
<script type="text/javascript" src="scripts/u2f-api.js"></script>
<script keeplink=1 type="text/javascript" src="scripts/charts.js"></script>
<script keeplink=1 type="text/javascript" src="scripts/filesaver.1.1.20151003.js"></script>
<script keeplink=1 type="text/javascript" src="scripts/ol.js"></script>
@ -251,8 +252,8 @@
<span id="verifyEmailId" style="display:none"><a onclick="account_showVerifyEmail()" style="cursor:pointer">Verify email</a><br /></span>
<span id="otpAuth" style="display:none"><a onclick="account_addOtp()" style="cursor:pointer">Add 2-step login</a><br /></span>
<span id="otpAuthRemove" style="display:none"><a onclick="account_removeOtp()" style="cursor:pointer">Remove 2-step login</a><br /></span>
<span id="manageOtp" style="display:none"><a onclick="account_manageOtp(0)" style="cursor:pointer">Manage one time passwords</a><br /></span>
<span id="manageHardwareOtp" style="display:none"><a onclick="account_manageHardwareOtp(0)" style="cursor:pointer">Manage hardware login keys</a><br /></span>
<span id="manageOtp" style="display:none"><a onclick="account_manageOtp(0)" style="cursor:pointer">Manage one time passwords</a><br /></span>
<a onclick="account_showChangeEmail()" style="cursor:pointer">Change email address</a><br />
<a onclick="account_showChangePassword()" style="cursor:pointer">Change password</a><br />
<a onclick="account_showDeleteAccount()" style="cursor:pointer">Delete account</a><br />
@ -1172,7 +1173,7 @@
QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
QV('otpAuth', ((features & 4096) != 0) && (userinfo.otpsecret != 1));
QV('otpAuthRemove', ((features & 4096) != 0) && (userinfo.otpsecret == 1));
QV('manageOtp', ((features & 4096) != 0) && (userinfo.otpsecret == 1));
QV('manageOtp', ((features & 4096) != 0) && ((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0)));
QV('manageHardwareOtp', ((features & 0x5000) != 0)); // Requires 2-step login + YubiKey support
}
@ -1457,13 +1458,15 @@
x += "<br />";
x += "<div><input type=button value='Close' onclick=setDialogMode(0) style=float:right></input>";
//x += "<input type=button value='Add YubiKey' onclick='account_addYubiKey();'></input>";
if (window.u2f) {
x += "<input type=button value='Add Key' onclick='account_addhkey();'></input>";
if (u2fSupported()) {
x += "<input id=d2addkey type=button value='Add Key' onclick='account_addhkey();'></input>";
} else {
x += "No hardware key support on this browser.";
}
x += "</div><br />";
setDialogMode(2, "Manage Hardware Login Keys", 8, null, x, 'otpauth-hardware-manage');
if (u2fSupported() && (message.keys.length > 0)) { QE('d2addkey', false); }
break;
}
case 'otp-hkey-setup-request': {
@ -1471,7 +1474,7 @@
var x = "Press the key button now.<br /><br /><div style=width:100%;text-align:center><img src='images/hardware-keypress-120.png' /></div><input id=dp1keyname style=display:none value=" + message.name + " />";
setDialogMode(2, "Add Hardware Login Key", 2, null, x);
window.u2f.register(message.request.appId, [message.request], [], function (registrationResponse) {
if (registrationResponse.errorCode == 0) {
if (registrationResponse.registrationData) {
meshserver.send({ action: 'otp-hkey-setup-response', request: message.request, response: registrationResponse, name: Q('dp1keyname').value });
setDialogMode(2, "Add Hardware Login Key", 0, null, '<br />Checking...<br /><br /><br />', 'otpauth-hardware-manage');
} else {
@ -5297,8 +5300,8 @@
function account_manageOtp(action) {
if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-manage')) { dialogclose(0); }
if (xxdialogMode || (userinfo.otpsecret != 1) || ((features & 4096) == 0)) return;
meshserver.send({ action: 'otpauth-getpasswords', subaction: action });
if (xxdialogMode || ((features & 4096) == 0)) return;
if ((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0)) { meshserver.send({ action: 'otpauth-getpasswords', subaction: action }); }
}
function account_manageHardwareOtp() {
@ -5309,17 +5312,19 @@
function account_addhkey() {
var x = "Type in the name of the key to add.<br /><br />";
x += addHtmlValue('Key Name', '<input id=dp1keyname style=width:230px maxlength=20 autocomplete=off onchange=account_addhkeyValidate() onkeyup=account_addhkeyValidate() />');
x += addHtmlValue('Key Name', '<input id=dp1keyname style=width:230px maxlength=20 autocomplete=off placeholder="MyKey" onkeyup=account_addhkeyValidate(event) />');
setDialogMode(2, "Add Hardware Login Key", 3, account_addhkeyEx, x);
account_addhkeyValidate();
Q('dp1keyname').focus();
}
function account_addhkeyValidate() {
QE('idx_dlgOkButton', (Q('dp1keyname').value.length > 0));
function account_addhkeyValidate(e) {
if ((e != null) && (e.keyCode == 13)) { dialogclose(1); }
}
function account_addhkeyEx() {
meshserver.send({ action: 'otp-hkey-setup-request', name: Q('dp1keyname').value });
var name = Q('dp1keyname').value;
if (name == '') { name = 'MyKey'; }
meshserver.send({ action: 'otp-hkey-setup-request', name: name });
}
function account_addYubiKey() {
@ -6943,6 +6948,8 @@
function focusTextBox(x) { setTimeout(function(){ Q(x).selectionStart = Q(x).selectionEnd = 65535; Q(x).focus(); }, 0); }
function validateEmail(v) { var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return emailReg.test(v); } // New version
function isPrivateIP(a) { return (a.startsWith('10.') || a.startsWith('172.16.') || a.startsWith('192.168.')); }
function u2fSupported() { return (window.u2f && ((navigator.userAgent.indexOf('Chrome/') > 0) || (navigator.userAgent.indexOf('Firefox/') > 0) || (navigator.userAgent.indexOf('Opera/') > 0) || (navigator.userAgent.indexOf('Safari/') > 0))); }
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,7 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="format-detection" content="telephone=no" />
<script type="text/javascript" src="scripts/common-0.0.1.js"></script>
<script type="text/javascript" src="scripts/u2f-api.js"></script>
<title>MeshCentral - Login</title>
<style>
a {
@ -148,7 +149,11 @@
<table>
<tr>
<td align=right width=100>Login token:</td>
<td><input id=tokenInput type=text name=token maxlength=12 onkeypress="return (event.keyCode == 8) || (event.keyCode == 13) || (event.charCode >= 48 && event.charCode <= 57)" onkeyup=checkToken(event) onkeydown=checkToken(event) /></td>
<td>
<input id=tokenInput type=text name=token maxlength=12 onkeypress="return (event.keyCode == 8) || (event.keyCode == 13) || (event.charCode >= 48 && event.charCode <= 57)" onkeyup=checkToken(event) onkeydown=checkToken(event) />
<input id=hwtokenInput1 type=text name=hwtoken1 style="display:none" />
<input id=hwtokenInput2 type=text name=hwtoken2 style="display:none" />
</td>
</tr>
<tr>
<td colspan=2>
@ -200,6 +205,7 @@
var features = parseInt('{{{features}}}');
var passRequirements = "{{{passRequirements}}}";
if (passRequirements != "") { passRequirements = JSON.parse(decodeURIComponent(passRequirements)); }
var hardwareKeyChallenge = '{{{hkey}}}';
function startup() {
if ((features & 32) == 0) {
@ -219,6 +225,21 @@
QV("newAccountPass", (newAccountPass == 1));
QV("resetAccountDiv", (emailCheck == true));
QV("hrAccountDiv", (emailCheck == true) || (newAccountPass == 1));
if ('{{loginmode}}' == '4') {
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
if ((hardwareKeyChallenge != null) && u2fSupported()) {
var c = hardwareKeyChallenge[0];
window.u2f.sign(c.appId, c.challenge, hardwareKeyChallenge, function (authResponse) {
if (authResponse.signatureData) {
Q('hwtokenInput1').value = JSON.stringify(hardwareKeyChallenge);
Q('hwtokenInput2').value = JSON.stringify(authResponse);
QE('tokenOkButton', true);
Q('tokenOkButton').click();
}
});
}
}
}
function showPassHint() {
@ -379,6 +400,7 @@
function haltEvent(e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
function haltReturn(e) { if (e.keyCode == 13) { haltEvent(e); } }
function validateEmail(v) { var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return emailReg.test(v); } // New version
function u2fSupported() { return (window.u2f && ((navigator.userAgent.indexOf('Chrome/') > 0) || (navigator.userAgent.indexOf('Firefox/') > 0) || (navigator.userAgent.indexOf('Opera/') > 0) || (navigator.userAgent.indexOf('Safari/') > 0))); }
</script>
</body>

View File

@ -87,6 +87,7 @@
}
</style>
<script type="text/javascript" src="scripts/common-0.0.1.js"></script>
<script type="text/javascript" src="scripts/u2f-api.js"></script>
<title>MeshCentral - Login</title>
</head>
<body onload="if (typeof(startup) !== 'undefined') startup();">
@ -221,7 +222,11 @@
<table>
<tr>
<td align=right width=100>Login token:</td>
<td><input id=tokenInput type=text name=token maxlength=12 onkeypress="return (event.keyCode == 8) || (event.keyCode == 13) || (event.charCode >= 48 && event.charCode <= 57)" onkeyup=checkToken(event) onkeydown=checkToken(event) /></td>
<td>
<input id=tokenInput type=text name=token maxlength=12 onkeypress="return (event.keyCode == 8) || (event.keyCode == 13) || (event.charCode >= 48 && event.charCode <= 57)" onkeyup=checkToken(event) onkeydown=checkToken(event) />
<input id=hwtokenInput1 type=text name=hwtoken1 style="display:none" />
<input id=hwtokenInput2 type=text name=hwtoken2 style="display:none" />
</td>
</tr>
<tr>
<td colspan=2>
@ -277,6 +282,7 @@
var newAccountPass = parseInt('{{{newAccountPass}}}');
var emailCheck = ('{{{emailcheck}}}' == 'true');
var passRequirements = "{{{passRequirements}}}";
var hardwareKeyChallenge = '{{{hkey}}}';
if (passRequirements != "") { passRequirements = JSON.parse(decodeURIComponent(passRequirements)); }
var features = parseInt('{{{features}}}');
var webPageFullScreen = getstore('webPageFullScreen', true);
@ -302,6 +308,21 @@
QV("newAccountPass", (newAccountPass == 1));
QV("resetAccountDiv", (emailCheck == true));
QV("hrAccountDiv", (emailCheck == true) || (newAccountPass == 1));
if ('{{loginmode}}' == '4') {
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
if ((hardwareKeyChallenge != null) && u2fSupported()) {
var c = hardwareKeyChallenge[0];
window.u2f.sign(c.appId, c.challenge, hardwareKeyChallenge, function (authResponse) {
if (authResponse.signatureData) {
Q('hwtokenInput1').value = JSON.stringify(hardwareKeyChallenge);
Q('hwtokenInput2').value = JSON.stringify(authResponse);
QE('tokenOkButton', true);
Q('tokenOkButton').click();
}
});
}
}
}
function showPassHint() {
@ -522,6 +543,7 @@
function validateEmail(v) { var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return emailReg.test(v); } // New version
function putstore(name, val) { try { if (typeof (localStorage) === 'undefined') return; localStorage.setItem(name, val); } catch (e) { } }
function getstore(name, val) { try { if (typeof (localStorage) === 'undefined') return val; var v = localStorage.getItem(name); if ((v == null) || (v == null)) return val; return v; } catch (e) { return val; } }
function u2fSupported() { return (window.u2f && ((navigator.userAgent.indexOf('Chrome/') > 0) || (navigator.userAgent.indexOf('Firefox/') > 0) || (navigator.userAgent.indexOf('Opera/') > 0) || (navigator.userAgent.indexOf('Safari/') > 0))); }
</script>
</body>

View File

@ -336,6 +336,53 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
res.redirect(domain.url);
}
// Return true if this user has 2-step auth active
function checkUserOneTimePasswordRequired(domain, user) {
return (user.otpsecret) || (user.otphkeys && (user.otphkeys.length > 0));
}
// Check the 2-step auth token
function checkUserOneTimePassword(domain, user, token, hwtoken1, hwtoken2) {
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
if (twoStepLoginSupported == false) return true;
// Check hardware key
if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken1) == 'string') && (typeof (hwtoken2) == 'string')) {
// Check hardware token
var authRequest = null, authResponse = null;
try { authRequest = JSON.parse(hwtoken1); } catch (ex) { }
try { authResponse = JSON.parse(hwtoken2); } catch (ex) { }
if ((authRequest != null) && (authResponse != null)) {
const u2f = require('u2f');
const result = u2f.checkSignature(authRequest[0], authResponse, user.otphkeys[0].publicKey);
if (result.successful === true) return true;
}
}
// Check Google Authenticator
const otplib = require('otplib')
if (user.otpsecret && (typeof (token) == 'string') && (otplib.authenticator.check(token, user.otpsecret) == true)) return true;
// Check written down keys
if ((user.otpkeys != null) && (user.otpkeys.keys != null)) {
var tokenNumber = parseInt(token);
for (var i = 0; i < user.otpkeys.keys.length; i++) { if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) { user.otpkeys.keys[i].u = false; return true; } }
}
return false;
}
// Return a hardware key challenge
function getHardwareKeyChallenge(domain, user) {
if (user.otphkeys && (user.otphkeys.length > 0)) {
var requests = [];
const u2f = require('u2f');
for (var i in user.otphkeys) { requests.push(u2f.request('https://' + obj.parent.certificates.CommonName, user.otphkeys[i].keyHandle)); }
return JSON.stringify(requests);
}
return '';
}
function handleLoginRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) return;
@ -349,6 +396,20 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (userid) {
var user = obj.users[userid];
// Check if this user has 2-step login active
if (checkUserOneTimePasswordRequired(domain, user)) {
if (checkUserOneTimePassword(domain, user, req.body.token, req.body.hwtoken1, req.body.hwtoken2) == false) {
// 2-step auth is required, but the token is not present or not valid.
if (user.otpsecret != null) { req.session.error = '<b style=color:#8C001A>Invalid token, try again.</b>'; }
req.session.loginmode = '4';
req.session.tokenusername = xusername;
req.session.tokenpassword = xpassword;
res.redirect(domain.url);
return;
}
}
/*
// Check if this user has 2-step login active
var tokenValid = 0;
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
@ -356,7 +417,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
otplib.authenticator.options = { window: 6 }; // Set +/- 3 minute window
if (twoStepLoginSupported && user.otpsecret && ((typeof (req.body.token) != 'string') || ((tokenValid = otplib.authenticator.check(req.body.token, user.otpsecret)) !== true))) {
// Failed OTP, check user's one time passwords
if ((req.body.token != null) && (user.otpkeys != null) && (user.otpkeys.keys != null)) {
console.log(user);
if ((req.body.token != null) && ((user.otpkeys != null) && (user.otpkeys.keys != null)) || (user.otphkeys && user.otphkeys.length > 0)) {
var found = null;
var tokenNumber = parseInt(req.body.token);
for (var i = 0; i < user.otpkeys.keys.length; i++) { if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) { user.otpkeys.keys[i].u = false; found = i; } }
@ -379,6 +441,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
return;
}
}
*/
// Save login time
user.login = Math.floor(Date.now() / 1000);
@ -807,7 +870,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && (user.siteadmin == 0xFFFFFFFF)) { features += 0x0800; } // Server can self-write (Allows self-update)
if ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true)) { features += 0x1000; } // 2-step login supported
if (domain.agentnoproxy === true) { features += 0x2000; } // Indicates that agents should be installed without using a HTTP proxy
if (domain.yubikey && domain.yubikey.id && domain.yubikey.secret) { features += 0x4000; } // Indicates Yubikey support
if (domain.yubikey && domain.yubikey.id && domain.yubikey.secret) { features += 0x4000; } // Indicates Yubikey support (???)
// Create a authentication cookie
const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
@ -836,17 +899,26 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if ((parent.config != null) && (parent.config.settings != null) && (parent.config.settings.allowframing == true)) { features += 32; } // Allow site within iframe
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
// If this is a 2 factor auth request, look for a hardware key challenge.
var hardwareKeyChallenge = '';
if ((loginmode == '4') && (req.session.tokenusername)) {
var user = obj.users['user/' + domain.id + '/' + req.session.tokenusername];
if (user != null) {
hardwareKeyChallenge = getHardwareKeyChallenge(domain, user);
}
}
if (obj.args.minify && !req.query.nominify) {
// Try to server the minified version if we can.
try {
res.render(obj.path.join(obj.parent.webViewsPath, isMobileBrowser(req) ? 'login-mobile-min' : 'login-min'), { loginmode: loginmode, rootCertLink: getRootCertLink(), title: domain.title, title2: domain.title2, newAccount: domain.newaccounts, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: obj.parent.mailserver != null, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, footer: (domain.footer == null) ? '' : domain.footer });
res.render(obj.path.join(obj.parent.webViewsPath, isMobileBrowser(req) ? 'login-mobile-min' : 'login-min'), { loginmode: loginmode, rootCertLink: getRootCertLink(), title: domain.title, title2: domain.title2, newAccount: domain.newaccounts, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: obj.parent.mailserver != null, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, footer: (domain.footer == null) ? '' : domain.footer, hkey: hardwareKeyChallenge });
} catch (ex) {
// In case of an exception, serve the non-minified version.
res.render(obj.path.join(obj.parent.webViewsPath, isMobileBrowser(req) ? 'login-mobile' : 'login'), { loginmode: loginmode, rootCertLink: getRootCertLink(), title: domain.title, title2: domain.title2, newAccount: domain.newaccounts, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: obj.parent.mailserver != null, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, footer: (domain.footer == null) ? '' : domain.footer });
res.render(obj.path.join(obj.parent.webViewsPath, isMobileBrowser(req) ? 'login-mobile' : 'login'), { loginmode: loginmode, rootCertLink: getRootCertLink(), title: domain.title, title2: domain.title2, newAccount: domain.newaccounts, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: obj.parent.mailserver != null, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, footer: (domain.footer == null) ? '' : domain.footer, hkey: hardwareKeyChallenge });
}
} else {
// Serve non-minified version of web pages.
res.render(obj.path.join(obj.parent.webViewsPath, isMobileBrowser(req) ? 'login-mobile' : 'login'), { loginmode: loginmode, rootCertLink: getRootCertLink(), title: domain.title, title2: domain.title2, newAccount: domain.newaccounts, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: obj.parent.mailserver != null, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, footer: (domain.footer == null) ? '' : domain.footer });
res.render(obj.path.join(obj.parent.webViewsPath, isMobileBrowser(req) ? 'login-mobile' : 'login'), { loginmode: loginmode, rootCertLink: getRootCertLink(), title: domain.title, title2: domain.title2, newAccount: domain.newaccounts, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: obj.parent.mailserver != null, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, footer: (domain.footer == null) ? '' : domain.footer, hkey: hardwareKeyChallenge });
}
/*