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) {