First version that can do FIDO2/WebAuthn authentication.

Ylian Saint-Hilaire committed Mar 23, 2019 at 12:14 UTC 44595dee861c9c0d60d14a0127bc69fc5af5ba3b
3 files changed +68 -25
meshuser.js
+1 -1
@@ -1987,7 +1987,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1987 parent.f2l.attestationOptions().then(function (registrationOptions) {
1988 // Convert the challenge to base64 and add user information
1989 registrationOptions.challenge = Buffer(registrationOptions.challenge).toString('base64');
1990 - registrationOptions.user.id = Buffer(parent.crypto.randomBytes(16)).toString('base64');
1990 + registrationOptions.user.id = Buffer(user._id, 'binary').toString('base64');
1991 registrationOptions.user.name = user._id;
1992 registrationOptions.user.displayName = user._id.split('/')[2];
1993
views/login.handlebars
+9 -12
@@ -385,21 +385,18 @@
385 // New WebAuthn hardware keys
386 navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }).then(
387 function (rawAssertion) {
388 - console.log(rawAssertion);
389 - /*
388 var assertion = {
391 - id: base64encode(rawAssertion.rawId),
392 - clientDataJSON: arrayBufferToString(rawAssertion.response.clientDataJSON),
393 - userHandle: base64encode(rawAssertion.response.userHandle),
394 - signature: base64encode(rawAssertion.response.signature),
395 - authenticatorData: base64encode(rawAssertion.response.authenticatorData)
389 + id: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.rawId))), //base64encode(rawAssertion.rawId),
390 + clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.clientDataJSON))), //arrayBufferToString(rawAssertion.response.clientDataJSON),
391 + userHandle: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.userHandle))), //base64encode(rawAssertion.response.userHandle),
392 + signature: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.signature))), //base64encode(rawAssertion.response.signature),
393 + authenticatorData: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.authenticatorData))), //base64encode(rawAssertion.response.authenticatorData)
394 };
397 - console.log(assertion);
398 - */
395 + Q('hwtokenInput').value = JSON.stringify(assertion);
396 + QE('tokenOkButton', true);
397 + Q('tokenOkButton').click();
398 },
400 - function (error) {
401 - console.log('credentials-get error', error);
402 - }
399 + function (error) { console.log('credentials-get error', error); }
400 );
401 } else if ((hardwareKeyChallenge != null) && u2fSupported()) {
402 // Old U2F hardware keys
webserver.js
+58 -12
@@ -340,18 +340,64 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
340 const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true));
341 if (twoStepLoginSupported == false) { func(true); return; };
342
343 - // Check U2F hardware key
343 + // Check hardware key
344 if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
345 - // Get all U2F keys
346 - var u2fKeys = [];
347 - for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 1) { u2fKeys.push(user.otphkeys[i]); } }
348 - if (u2fKeys.length > 0) {
349 - var authResponse = null;
350 - try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
351 - if (authResponse != null) {
352 - // Check authentication response
353 - require('authdog').finishAuthentication(req.session.u2fchallenge, authResponse, u2fKeys).then(function (authenticationStatus) { func(true); }, function (error) { func(false); });
354 - return;
345 + var authResponse = null;
346 + try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
347 + if (authResponse != null) {
348 + if ((obj.f2l != null) && (authResponse.clientDataJSON)) {
349 + // Get all WebAuthn keys
350 + var webAuthnKeys = [];
351 + for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
352 + if (webAuthnKeys.length > 0) {
353 + // Decode authentication response
354 + var clientAssertionResponse = { response: {} };
355 + clientAssertionResponse.id = authResponse.id;
356 + clientAssertionResponse.rawId = new Uint8Array(Buffer.from(authResponse.id, 'base64')).buffer;
357 + clientAssertionResponse.response.authenticatorData = new Uint8Array(Buffer.from(authResponse.authenticatorData, 'base64')).buffer;
358 + clientAssertionResponse.response.clientDataJSON = new Uint8Array(Buffer.from(authResponse.clientDataJSON, 'base64')).buffer;
359 + clientAssertionResponse.response.signature = new Uint8Array(Buffer.from(authResponse.signature, 'base64')).buffer;
360 + clientAssertionResponse.response.userHandle = new Uint8Array(Buffer.from(authResponse.userHandle, 'base64')).buffer;
361 +
362 + // Look for the key with clientAssertionResponse.id
363 + var webAuthnKey = null;
364 + for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } }
365 +
366 + // If we found a valid key to use, let's validate the response
367 + if (webAuthnKey != null) {
368 + var assertionExpectations = {
369 + challenge: req.session.u2fchallenge,
370 + origin: "https://devbox.mesh.meshcentral.com",
371 + factor: "either",
372 + publicKey: webAuthnKey.publicKey,
373 + prevCounter: webAuthnKey.counter,
374 + userHandle: Buffer(user._id, 'binary').toString('base64')
375 + };
376 +
377 + obj.f2l.assertionResult(clientAssertionResponse, assertionExpectations).then(
378 + function (authnResult) {
379 + // Update the hardware key counter and accept the 2nd factor
380 + webAuthnKey.counter = authnResult.authnrData.get('counter');
381 + obj.db.SetUser(user);
382 + func(true);
383 + },
384 + function (error) {
385 + //console.log('attestationResult-error', error);
386 + func(false);
387 + }
388 + );
389 + return;
390 + }
391 + }
392 + } else {
393 + // Get all U2F keys
394 + var u2fKeys = [];
395 + for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 1) { u2fKeys.push(user.otphkeys[i]); } }
396 + if (u2fKeys.length > 0) {
397 + // Check authentication response
398 + require('authdog').finishAuthentication(req.session.u2fchallenge, authResponse, u2fKeys).then(function (authenticationStatus) { func(true); }, function (error) { func(false); });
399 + return;
400 + }
401 }
402 }
403 }
@@ -400,7 +446,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
446 obj.f2l.assertionOptions().then(function (authnOptions) {
447 authnOptions.type = 'webAuthn';
448 authnOptions.keyIds = [];
403 - for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[0].keyId); }
449 + for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
450 req.session.u2fchallenge = authnOptions.challenge = Buffer(authnOptions.challenge).toString('base64');
451 func(JSON.stringify(authnOptions));
452 }, function (error) {