From 44595dee861c9c0d60d14a0127bc69fc5af5ba3b Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 23 Mar 2019 12:14:53 -0700 Subject: [PATCH] First version that can do FIDO2/WebAuthn authentication. --- meshuser.js | 2 +- views/login.handlebars | 21 ++++++------- webserver.js | 70 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/meshuser.js b/meshuser.js index e97da40f..1a3b0a6d 100644 --- a/meshuser.js +++ b/meshuser.js @@ -1987,7 +1987,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.f2l.attestationOptions().then(function (registrationOptions) { // Convert the challenge to base64 and add user information registrationOptions.challenge = Buffer(registrationOptions.challenge).toString('base64'); - registrationOptions.user.id = Buffer(parent.crypto.randomBytes(16)).toString('base64'); + registrationOptions.user.id = Buffer(user._id, 'binary').toString('base64'); registrationOptions.user.name = user._id; registrationOptions.user.displayName = user._id.split('/')[2]; diff --git a/views/login.handlebars b/views/login.handlebars index 320d1eda..f9a6b5c8 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -385,21 +385,18 @@ // New WebAuthn hardware keys navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }).then( function (rawAssertion) { - console.log(rawAssertion); - /* var assertion = { - id: base64encode(rawAssertion.rawId), - clientDataJSON: arrayBufferToString(rawAssertion.response.clientDataJSON), - userHandle: base64encode(rawAssertion.response.userHandle), - signature: base64encode(rawAssertion.response.signature), - authenticatorData: base64encode(rawAssertion.response.authenticatorData) + id: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.rawId))), //base64encode(rawAssertion.rawId), + clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.clientDataJSON))), //arrayBufferToString(rawAssertion.response.clientDataJSON), + userHandle: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.userHandle))), //base64encode(rawAssertion.response.userHandle), + signature: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.signature))), //base64encode(rawAssertion.response.signature), + authenticatorData: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.authenticatorData))), //base64encode(rawAssertion.response.authenticatorData) }; - console.log(assertion); - */ + Q('hwtokenInput').value = JSON.stringify(assertion); + QE('tokenOkButton', true); + Q('tokenOkButton').click(); }, - function (error) { - console.log('credentials-get error', error); - } + function (error) { console.log('credentials-get error', error); } ); } else if ((hardwareKeyChallenge != null) && u2fSupported()) { // Old U2F hardware keys diff --git a/webserver.js b/webserver.js index 359acc0d..0ab2d0f5 100644 --- a/webserver.js +++ b/webserver.js @@ -340,18 +340,64 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true)); if (twoStepLoginSupported == false) { func(true); return; }; - // Check U2F hardware key + // Check hardware key if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) { - // Get all U2F keys - var u2fKeys = []; - for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 1) { u2fKeys.push(user.otphkeys[i]); } } - if (u2fKeys.length > 0) { - var authResponse = null; - try { authResponse = JSON.parse(hwtoken); } catch (ex) { } - if (authResponse != null) { - // Check authentication response - require('authdog').finishAuthentication(req.session.u2fchallenge, authResponse, u2fKeys).then(function (authenticationStatus) { func(true); }, function (error) { func(false); }); - return; + var authResponse = null; + try { authResponse = JSON.parse(hwtoken); } catch (ex) { } + if (authResponse != null) { + if ((obj.f2l != null) && (authResponse.clientDataJSON)) { + // Get all WebAuthn keys + var webAuthnKeys = []; + for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } } + if (webAuthnKeys.length > 0) { + // Decode authentication response + var clientAssertionResponse = { response: {} }; + clientAssertionResponse.id = authResponse.id; + clientAssertionResponse.rawId = new Uint8Array(Buffer.from(authResponse.id, 'base64')).buffer; + clientAssertionResponse.response.authenticatorData = new Uint8Array(Buffer.from(authResponse.authenticatorData, 'base64')).buffer; + clientAssertionResponse.response.clientDataJSON = new Uint8Array(Buffer.from(authResponse.clientDataJSON, 'base64')).buffer; + clientAssertionResponse.response.signature = new Uint8Array(Buffer.from(authResponse.signature, 'base64')).buffer; + clientAssertionResponse.response.userHandle = new Uint8Array(Buffer.from(authResponse.userHandle, 'base64')).buffer; + + // Look for the key with clientAssertionResponse.id + var webAuthnKey = null; + for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } } + + // If we found a valid key to use, let's validate the response + if (webAuthnKey != null) { + var assertionExpectations = { + challenge: req.session.u2fchallenge, + origin: "https://devbox.mesh.meshcentral.com", + factor: "either", + publicKey: webAuthnKey.publicKey, + prevCounter: webAuthnKey.counter, + userHandle: Buffer(user._id, 'binary').toString('base64') + }; + + obj.f2l.assertionResult(clientAssertionResponse, assertionExpectations).then( + function (authnResult) { + // Update the hardware key counter and accept the 2nd factor + webAuthnKey.counter = authnResult.authnrData.get('counter'); + obj.db.SetUser(user); + func(true); + }, + function (error) { + //console.log('attestationResult-error', error); + func(false); + } + ); + return; + } + } + } else { + // Get all U2F keys + var u2fKeys = []; + for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 1) { u2fKeys.push(user.otphkeys[i]); } } + if (u2fKeys.length > 0) { + // Check authentication response + require('authdog').finishAuthentication(req.session.u2fchallenge, authResponse, u2fKeys).then(function (authenticationStatus) { func(true); }, function (error) { func(false); }); + return; + } } } } @@ -400,7 +446,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.f2l.assertionOptions().then(function (authnOptions) { authnOptions.type = 'webAuthn'; authnOptions.keyIds = []; - for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[0].keyId); } + for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); } req.session.u2fchallenge = authnOptions.challenge = Buffer(authnOptions.challenge).toString('base64'); func(JSON.stringify(authnOptions)); }, function (error) {