diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 86ae7911..ae75af2a 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -1793,6 +1793,12 @@ "default": null, "description": "Maximum number of FIDO/YubikeyOTP hardware 2FA keys that can be setup in a user account." }, + "fidopininput": { + "type": "string", + "default": "preferred", + "enum": ["preferred", "required", "discouraged"], + "description": "Controls FIDO PIN prompt behavior: 'preferred' (asks only if key requires PIN), 'required' (always asks for PIN), or 'discouraged' (never asks for PIN). Default: 'preferred'." + }, "allowaccountreset": { "type": "boolean", "default": true, diff --git a/meshuser.js b/meshuser.js index 64e33a69..fcfd4a86 100644 --- a/meshuser.js +++ b/meshuser.js @@ -4017,6 +4017,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Send the registration request var registrationOptions = parent.webauthn.generateRegistrationChallenge("Anonymous Service", { id: Buffer.from(user._id, 'binary').toString('base64'), name: user._id, displayName: user._id.split('/')[2] }); //console.log('registrationOptions', registrationOptions); + registrationOptions.userVerification = (domain.passwordrequirements && domain.passwordrequirements.fidopininput) ? domain.passwordrequirements.fidopininput : 'preferred'; // Use the domain setting if it exists, otherwise use 'preferred'. obj.webAuthnReqistrationRequest = { action: 'webauthn-startregister', keyname: command.name, request: registrationOptions }; ws.send(JSON.stringify(obj.webAuthnReqistrationRequest)); break; diff --git a/views/login.handlebars b/views/login.handlebars index 2c237789..e0b1779b 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -539,7 +539,7 @@ if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) { if (typeof hardwareKeyChallenge.challenge == 'string') { hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), function (c) { return c.charCodeAt(0) }).buffer; } - publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout } + publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout, userVerification: hardwareKeyChallenge.userVerification ? hardwareKeyChallenge.userVerification : 'preferred' } for (var i = 0; i < hardwareKeyChallenge.keyIds.length; i++) { publicKeyCredentialRequestOptions.allowCredentials.push( { id: Uint8Array.from(atob(hardwareKeyChallenge.keyIds[i]), function (c) { return c.charCodeAt(0) }), type: 'public-key', transports: ['usb', 'ble', 'nfc', 'internal'] } diff --git a/views/login2.handlebars b/views/login2.handlebars index 10537565..9f9b78d4 100644 --- a/views/login2.handlebars +++ b/views/login2.handlebars @@ -635,7 +635,7 @@ if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) { if (typeof hardwareKeyChallenge.challenge == 'string') { hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), function (c) { return c.charCodeAt(0) }).buffer; } - publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout } + publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout, userVerification: hardwareKeyChallenge.userVerification ? hardwareKeyChallenge.userVerification : 'preferred' } for (var i = 0; i < hardwareKeyChallenge.keyIds.length; i++) { publicKeyCredentialRequestOptions.allowCredentials.push( { id: Uint8Array.from(atob(hardwareKeyChallenge.keyIds[i]), function (c) { return c.charCodeAt(0) }), type: 'public-key', transports: ['usb', 'ble', 'nfc', 'internal'] } diff --git a/webserver.js b/webserver.js index c6b1ff7e..aac5a4ca 100644 --- a/webserver.js +++ b/webserver.js @@ -1105,6 +1105,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (webAuthnKeys.length > 0) { // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this. var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') }; + // userVerification: 'preferred' use security pin if possible (default), 'required' always use security pin, 'discouraged' do not use security pin. + authnOptions.userVerification = (domain.passwordrequirements && domain.passwordrequirements.fidopininput) ? domain.passwordrequirements.fidopininput : 'preferred'; // Use the domain setting if it exists, otherwise use 'preferred'.{ for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); } sec.u2f = authnOptions.challenge; req.session.e = parent.encryptSessionData(sec);