From 5ebda12bde6c89fc75ff62fd49672fc92b6b48ea Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Wed, 20 Mar 2019 23:39:38 +0200 Subject: [PATCH] use roles for totp/u2f setup --- lib/api/2fa/totp.js | 388 ++++++++++++++++++------------------ lib/api/2fa/u2f.js | 471 +++++++++++++++++++++++--------------------- 2 files changed, 442 insertions(+), 417 deletions(-) diff --git a/lib/api/2fa/totp.js b/lib/api/2fa/totp.js index 8648e08d..b18893a7 100644 --- a/lib/api/2fa/totp.js +++ b/lib/api/2fa/totp.js @@ -2,10 +2,19 @@ const Joi = require('joi'); const ObjectID = require('mongodb').ObjectID; +const tools = require('../../tools'); +const roles = require('../../roles'); +const util = require('util'); module.exports = (db, server, userHandler) => { // Create TOTP seed and request a QR code + const setupTotp = util.promisify(userHandler.setupTotp.bind(userHandler)); + const enableTotp = util.promisify(userHandler.enableTotp.bind(userHandler)); + const disableTotp = util.promisify(userHandler.disableTotp.bind(userHandler)); + const checkTotp = util.promisify(userHandler.checkTotp.bind(userHandler)); + const disable2fa = util.promisify(userHandler.disable2fa.bind(userHandler)); + /** * @api {post} /users/:user/2fa/totp/setup Generate TOTP seed * @apiName SetupTotp2FA @@ -53,62 +62,63 @@ module.exports = (db, server, userHandler) => { * "code": "UserNotFound" * } */ - server.post('/users/:user/2fa/totp/setup', (req, res, next) => { - res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - label: Joi.string() - .empty('') - .trim() - .max(255), - issuer: Joi.string() - .trim() - .max(255) - .required(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + server.post( + '/users/:user/2fa/totp/setup', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + label: Joi.string() + .empty('') + .trim() + .max(255), + issuer: Joi.string() + .trim() + .max(255) + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - userHandler.setupTotp(user, result.value, (err, result) => { - if (err) { + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } + + let user = new ObjectID(result.value.user); + let totp = await setupTotp(user, result.value); + res.json({ success: true, - seed: result.secret, - qrcode: result.dataUrl + seed: totp.secret, + qrcode: totp.dataUrl }); return next(); - }); - }); + }) + ); /** * @api {post} /users/:user/2fa/totp/enable Enable TOTP seed @@ -151,50 +161,51 @@ module.exports = (db, server, userHandler) => { * "code": "UserNotFound" * } */ - server.post('/users/:user/2fa/totp/enable', (req, res, next) => { - res.charSet('utf-8'); + server.post( + '/users/:user/2fa/totp/enable', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - token: Joi.string() - .length(6) - .required(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + token: Joi.string() + .length(6) + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - userHandler.enableTotp(user, result.value, (err, result) => { - if (err) { + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } - if (!result) { + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } + + let user = new ObjectID(result.value.user); + let totp = await enableTotp(user, result.value); + + if (!totp) { res.json({ error: 'Invalid authentication token', code: 'InvalidToken' @@ -207,8 +218,8 @@ module.exports = (db, server, userHandler) => { }); return next(); - }); - }); + }) + ); /** * @api {delete} /users/:user/2fa/totp Disable TOTP auth @@ -245,55 +256,56 @@ module.exports = (db, server, userHandler) => { * "code": "UserNotFound" * } */ - server.del('/users/:user/2fa/totp', (req, res, next) => { - res.charSet('utf-8'); + server.del( + '/users/:user/2fa/totp', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - req.query.user = req.params.user; - - const result = Joi.validate(req.query, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + req.query.user = req.params.user; - userHandler.disableTotp(user, result.value, (err, success) => { - if (err) { + const result = Joi.validate(req.query, schema, { + abortEarly: false, + convert: true + }); + + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } + + let user = new ObjectID(result.value.user); + let success = await disableTotp(user, result.value); + res.json({ success }); return next(); - }); - }); + }) + ); /** * @api {post} /users/:user/2fa/totp/check Validate TOTP Token @@ -336,50 +348,51 @@ module.exports = (db, server, userHandler) => { * "code": "InvalidToken" * } */ - server.post('/users/:user/2fa/totp/check', (req, res, next) => { - res.charSet('utf-8'); + server.post( + '/users/:user/2fa/totp/check', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - token: Joi.string() - .length(6) - .required(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + token: Joi.string() + .length(6) + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - userHandler.checkTotp(user, result.value, (err, result) => { - if (err) { + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } - if (!result) { + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).readOwn('users')); + } else { + req.validate(roles.can(req.role).readAny('users')); + } + + let user = new ObjectID(result.value.user); + let totp = await checkTotp(user, result.value); + + if (!totp) { res.json({ error: 'Failed to validate TOTP', code: 'InvalidToken' @@ -392,8 +405,8 @@ module.exports = (db, server, userHandler) => { }); return next(); - }); - }); + }) + ); /** * @api {delete} /users/:user/2fa Disable 2FA @@ -430,53 +443,54 @@ module.exports = (db, server, userHandler) => { * "code": "UserNotFound" * } */ - server.del('/users/:user/2fa', (req, res, next) => { - res.charSet('utf-8'); + server.del( + '/users/:user/2fa', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - req.query.user = req.params.user; - - const result = Joi.validate(req.query, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + req.query.user = req.params.user; - userHandler.disable2fa(user, result.value, (err, success) => { - if (err) { + const result = Joi.validate(req.query, schema, { + abortEarly: false, + convert: true + }); + + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } + + let user = new ObjectID(result.value.user); + let success = await disable2fa(user, result.value); + res.json({ success }); return next(); - }); - }); + }) + ); }; diff --git a/lib/api/2fa/u2f.js b/lib/api/2fa/u2f.js index f2e9857c..8974f2c3 100644 --- a/lib/api/2fa/u2f.js +++ b/lib/api/2fa/u2f.js @@ -2,6 +2,9 @@ const Joi = require('joi'); const ObjectID = require('mongodb').ObjectID; +const tools = require('../../tools'); +const roles = require('../../roles'); +const util = require('util'); const U2F_ERROR_CODES = { OK: 0, @@ -21,129 +24,137 @@ const U2F_ERROR_MESSAGES = new Map([ ]); module.exports = (db, server, userHandler) => { + const setupU2f = util.promisify(userHandler.setupU2f.bind(userHandler)); + const enableU2f = util.promisify(userHandler.enableU2f.bind(userHandler)); + const disableU2f = util.promisify(userHandler.disableU2f.bind(userHandler)); + const startU2f = util.promisify(userHandler.startU2f.bind(userHandler)); + const checkU2f = util.promisify(userHandler.checkU2f.bind(userHandler)); + // Create U2F keys - server.post('/users/:user/2fa/u2f/setup', (req, res, next) => { - res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - appId: Joi.string() - .empty('') - .uri(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + server.post( + '/users/:user/2fa/u2f/setup', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + appId: Joi.string() + .empty('') + .uri(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - userHandler.setupU2f(user, result.value, (err, u2fRegRequest) => { - if (err) { + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } + + let user = new ObjectID(result.value.user); + let u2fRegRequest = await setupU2f(user, result.value); + res.json({ success: true, u2fRegRequest }); return next(); - }); - }); + }) + ); // Send response from U2F key - server.post('/users/:user/2fa/u2f/enable', (req, res, next) => { - res.charSet('utf-8'); + server.post( + '/users/:user/2fa/u2f/enable', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - errorCode: Joi.number().max(100), - clientData: Joi.string() - .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') - .max(10240), - registrationData: Joi.string() - .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') - .max(10240), - version: Joi.string().allow('U2F_V2'), - challenge: Joi.string() - .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') - .max(1024), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' - }); - return next(); - } - - if (result.value.errorCode) { - let error; - - switch (result.value.errorCode) { - case U2F_ERROR_CODES.DEVICE_INELIGIBLE: - error = 'U2F token is already registered'; - break; - default: - error = U2F_ERROR_MESSAGES.get(result.value.errorCode) || 'Unknown error code' + result.value.errorCode; - } - - res.json({ - error + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + errorCode: Joi.number().max(100), + clientData: Joi.string() + .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') + .max(10240), + registrationData: Joi.string() + .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') + .max(10240), + version: Joi.string().allow('U2F_V2'), + challenge: Joi.string() + .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') + .max(1024), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - let user = new ObjectID(result.value.user); - - userHandler.enableU2f(user, result.value, (err, result) => { - if (err) { + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } - if (!result) { + if (result.value.errorCode) { + let error; + + switch (result.value.errorCode) { + case U2F_ERROR_CODES.DEVICE_INELIGIBLE: + error = 'U2F token is already registered'; + break; + default: + error = U2F_ERROR_MESSAGES.get(result.value.errorCode) || 'Unknown error code' + result.value.errorCode; + } + + res.json({ + error + }); + + return next(); + } + + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } + + let user = new ObjectID(result.value.user); + let u2f = await enableU2f(user, result.value); + + if (!u2f) { res.json({ error: 'Failed to enable U2F', code: 'U2fEnableFailed' @@ -156,53 +167,53 @@ module.exports = (db, server, userHandler) => { }); return next(); - }); - }); + }) + ); // Disable U2F auth for an user - server.del('/users/:user/2fa/u2f', (req, res, next) => { - res.charSet('utf-8'); + server.del( + '/users/:user/2fa/u2f', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - req.query.user = req.params.user; - - const result = Joi.validate(req.query, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + req.query.user = req.params.user; - userHandler.disableU2f(user, result.value, (err, result) => { - if (err) { + const result = Joi.validate(req.query, schema, { + abortEarly: false, + convert: true + }); + + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } - if (!result) { + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } + + let user = new ObjectID(result.value.user); + let u2f = await disableU2f(user, result.value); + if (!u2f) { res.json({ error: 'Failed to disable U2F', code: 'U2fDisableFailed' @@ -215,53 +226,53 @@ module.exports = (db, server, userHandler) => { }); return next(); - }); - }); + }) + ); // Generate U2F Authentciation Request - server.post('/users/:user/2fa/u2f/start', (req, res, next) => { - res.charSet('utf-8'); + server.post( + '/users/:user/2fa/u2f/start', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - appId: Joi.string() - .empty('') - .uri(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + appId: Joi.string() + .empty('') + .uri(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - userHandler.startU2f(user, result.value, (err, u2fAuthRequest) => { - if (err) { + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).readOwn('users')); + } else { + req.validate(roles.can(req.role).readAny('users')); + } + + let user = new ObjectID(result.value.user); + let u2fAuthRequest = await startU2f(user, result.value); if (!result) { res.json({ error: 'Failed to generate authentication request for U2F', @@ -276,76 +287,76 @@ module.exports = (db, server, userHandler) => { }); return next(); - }); - }); + }) + ); // Send response from U2F key - server.post('/users/:user/2fa/u2f/check', (req, res, next) => { - res.charSet('utf-8'); + server.post( + '/users/:user/2fa/u2f/check', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - errorCode: Joi.number().max(100), - clientData: Joi.string() - .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') - .max(10240), - signatureData: Joi.string() - .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') - .max(10240), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' - }); - return next(); - } - - if (result.value.errorCode) { - let error; - - switch (result.value.errorCode) { - case U2F_ERROR_CODES.DEVICE_INELIGIBLE: - error = 'U2F token is not registered'; - break; - default: - error = U2F_ERROR_MESSAGES.get(result.value.errorCode) || 'Unknown error code' + result.value.errorCode; - } - - res.json({ - error + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + errorCode: Joi.number().max(100), + clientData: Joi.string() + .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') + .max(10240), + signatureData: Joi.string() + .regex(/^[0-9a-z\-_]+$/i, 'web safe base64') + .max(10240), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - let user = new ObjectID(result.value.user); - - userHandler.checkU2f(user, result.value, (err, result) => { - if (err) { + if (result.error) { res.json({ - error: err.message, - code: err.code + error: result.error.message, + code: 'InputValidationError' }); return next(); } - if (!result) { + if (result.value.errorCode) { + let error; + + switch (result.value.errorCode) { + case U2F_ERROR_CODES.DEVICE_INELIGIBLE: + error = 'U2F token is not registered'; + break; + default: + error = U2F_ERROR_MESSAGES.get(result.value.errorCode) || 'Unknown error code' + result.value.errorCode; + } + + res.json({ + error + }); + + return next(); + } + + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } + + let user = new ObjectID(result.value.user); + let u2f = await checkU2f(user, result.value); + if (!u2f) { res.json({ error: 'Failed to validate U2F request', code: 'U2fFail' @@ -358,6 +369,6 @@ module.exports = (db, server, userHandler) => { }); return next(); - }); - }); + }) + ); };