diff --git a/lib/hardware-credentials.js b/lib/hardware-credentials.js index 36dd1f63..f7b863eb 100644 --- a/lib/hardware-credentials.js +++ b/lib/hardware-credentials.js @@ -8,12 +8,12 @@ function createHardwareCredential (userID, credentialData) { } function getHardwareCredentials () { - const sql = `SELECT * from hardware_credentials` + const sql = `SELECT * FROM hardware_credentials` return db.any(sql) } function getHardwareCredentialsOfUser (userID) { - const sql = `SELECT * from hardware_credentials where user_id=$1` + const sql = `SELECT * FROM hardware_credentials WHERE user_id=$1` return db.any(sql, [userID]) } diff --git a/lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js b/lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js index cd54e67c..944829fe 100644 --- a/lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js +++ b/lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js @@ -4,12 +4,17 @@ const _ = require('lodash/fp') const userManagement = require('../userManagement') const credentials = require('../../../../hardware-credentials') +const db = require('../../../../db') +const options = require('../../../../options') const T = require('../../../../time') const users = require('../../../../users') +const domain = options.hostname +const devMode = require('minimist')(process.argv.slice(2)).dev + const REMEMBER_ME_AGE = 90 * T.day -const rpID = `localhost` +const rpID = devMode ? `localhost` : domain const expectedOrigin = `https://${rpID}:3001` const generateAttestationOptions = (userID, session) => { @@ -73,46 +78,53 @@ const validateAttestation = (userID, attestationResponse, context) => { const webauthnData = context.req.session.webauthn.attestation const expectedChallenge = webauthnData.challenge - return users.getUserById(userID).then(user => { - return simpleWebauthn.verifyAttestationResponse({ + return Promise.all([ + users.getUserById(userID), + simpleWebauthn.verifyAttestationResponse({ credential: attestationResponse, expectedChallenge: `${expectedChallenge}`, expectedOrigin, expectedRPID: rpID - }).then(async verification => { + }) + ]) + .then(([user, verification]) => { const { verified, attestationInfo } = verification - if (verified && attestationInfo) { - const { - counter, - credentialPublicKey, - credentialID - } = attestationInfo - - const userDevices = await credentials.getHardwareCredentialsOfUser(user.id) - const existingDevice = userDevices.find(device => device.data.credentialID === credentialID) - - if (!existingDevice) { - const newDevice = { - counter, - credentialPublicKey, - credentialID - } - credentials.createHardwareCredential(user.id, newDevice) - } + if (!(verified || attestationInfo)) { + context.req.session.webauthn = null + return false } - context.req.session.webauthn = null - return verified + const { + counter, + credentialPublicKey, + credentialID + } = attestationInfo + + return credentials.getHardwareCredentialsOfUser(user.id) + .then(userDevices => { + const existingDevice = userDevices.find(device => device.data.credentialID === credentialID) + + if (!existingDevice) { + const newDevice = { + counter, + credentialPublicKey, + credentialID + } + credentials.createHardwareCredential(user.id, newDevice) + } + + context.req.session.webauthn = null + return verified + }) }) - }) } const validateAssertion = (username, password, rememberMe, assertionResponse, context) => { return userManagement.authenticateUser(username, password).then(user => { const expectedChallenge = context.req.session.webauthn.assertion.challenge - return credentials.getHardwareCredentialsOfUser(user.id).then(async devices => { + return credentials.getHardwareCredentialsOfUser(user.id).then(devices => { const dbAuthenticator = _.find(dev => { return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(assertionResponse.rawId)) === 0 }, devices) @@ -142,17 +154,21 @@ const validateAssertion = (username, password, rememberMe, assertionResponse, co const { verified, assertionInfo } = verification - if (verified) { - dbAuthenticator.data.counter = assertionInfo.newCounter - await credentials.updateHardwareCredential(dbAuthenticator) - - const finalUser = { id: user.id, username: user.username, role: user.role } - context.req.session.user = finalUser - if (rememberMe) context.req.session.cookie.maxAge = REMEMBER_ME_AGE + if (!verified) { + context.req.session.webauthn = null + return false } - context.req.session.webauthn = null - return verified + dbAuthenticator.data.counter = assertionInfo.newCounter + return credentials.updateHardwareCredential(dbAuthenticator) + .then(() => { + const finalUser = { id: user.id, username: user.username, role: user.role } + context.req.session.user = finalUser + if (rememberMe) context.req.session.cookie.maxAge = REMEMBER_ME_AGE + + context.req.session.webauthn = null + return verified + }) }) }) } diff --git a/lib/new-admin/graphql/modules/authentication/FIDOPasswordlessStrategy.js b/lib/new-admin/graphql/modules/authentication/FIDOPasswordlessStrategy.js index afe59d43..92fbc20b 100644 --- a/lib/new-admin/graphql/modules/authentication/FIDOPasswordlessStrategy.js +++ b/lib/new-admin/graphql/modules/authentication/FIDOPasswordlessStrategy.js @@ -3,12 +3,16 @@ const base64url = require('base64url') const _ = require('lodash/fp') const credentials = require('../../../../hardware-credentials') +const options = require('../../../../options') const T = require('../../../../time') const users = require('../../../../users') +const domain = options.hostname +const devMode = require('minimist')(process.argv.slice(2)).dev + const REMEMBER_ME_AGE = 90 * T.day -const rpID = `localhost` +const rpID = devMode ? `localhost` : domain const expectedOrigin = `https://${rpID}:3001` const generateAttestationOptions = (userID, session) => { @@ -72,46 +76,53 @@ const validateAttestation = (userID, attestationResponse, context) => { const webauthnData = context.req.session.webauthn.attestation const expectedChallenge = webauthnData.challenge - return users.getUserById(userID).then(user => { - return simpleWebauthn.verifyAttestationResponse({ + return Promise.all([ + users.getUserById(userID), + simpleWebauthn.verifyAttestationResponse({ credential: attestationResponse, expectedChallenge: `${expectedChallenge}`, expectedOrigin, expectedRPID: rpID - }).then(async verification => { + }) + ]) + .then(([user, verification]) => { const { verified, attestationInfo } = verification - if (verified && attestationInfo) { - const { - counter, - credentialPublicKey, - credentialID - } = attestationInfo - - const userDevices = await credentials.getHardwareCredentialsOfUser(user.id) - const existingDevice = userDevices.find(device => device.data.credentialID === credentialID) - - if (!existingDevice) { - const newDevice = { - counter, - credentialPublicKey, - credentialID - } - credentials.createHardwareCredential(user.id, newDevice) - } + if (!(verified || attestationInfo)) { + context.req.session.webauthn = null + return false } - context.req.session.webauthn = null - return verified + const { + counter, + credentialPublicKey, + credentialID + } = attestationInfo + + return credentials.getHardwareCredentialsOfUser(user.id) + .then(userDevices => { + const existingDevice = userDevices.find(device => device.data.credentialID === credentialID) + + if (!existingDevice) { + const newDevice = { + counter, + credentialPublicKey, + credentialID + } + credentials.createHardwareCredential(user.id, newDevice) + } + + context.req.session.webauthn = null + return verified + }) }) - }) } const validateAssertion = (username, rememberMe, assertionResponse, context) => { return users.getUserByUsername(username).then(user => { const expectedChallenge = context.req.session.webauthn.assertion.challenge - return credentials.getHardwareCredentialsOfUser(user.id).then(async devices => { + return credentials.getHardwareCredentialsOfUser(user.id).then(devices => { const dbAuthenticator = _.find(dev => { return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(assertionResponse.rawId)) === 0 }, devices) @@ -141,17 +152,21 @@ const validateAssertion = (username, rememberMe, assertionResponse, context) => const { verified, assertionInfo } = verification - if (verified) { - dbAuthenticator.data.counter = assertionInfo.newCounter - await credentials.updateHardwareCredential(dbAuthenticator) - - const finalUser = { id: user.id, username: user.username, role: user.role } - context.req.session.user = finalUser - if (rememberMe) context.req.session.cookie.maxAge = REMEMBER_ME_AGE + if (!verified) { + context.req.session.webauthn = null + return false } - context.req.session.webauthn = null - return verified + dbAuthenticator.data.counter = assertionInfo.newCounter + return credentials.updateHardwareCredential(dbAuthenticator) + .then(() => { + const finalUser = { id: user.id, username: user.username, role: user.role } + context.req.session.user = finalUser + if (rememberMe) context.req.session.cookie.maxAge = REMEMBER_ME_AGE + + context.req.session.webauthn = null + return verified + }) }) }) } diff --git a/lib/new-admin/graphql/modules/authentication/FIDOUsernamelessStrategy.js b/lib/new-admin/graphql/modules/authentication/FIDOUsernamelessStrategy.js index ca16b8ab..981a4e93 100644 --- a/lib/new-admin/graphql/modules/authentication/FIDOUsernamelessStrategy.js +++ b/lib/new-admin/graphql/modules/authentication/FIDOUsernamelessStrategy.js @@ -3,12 +3,16 @@ const base64url = require('base64url') const _ = require('lodash/fp') const credentials = require('../../../../hardware-credentials') +const options = require('../../../../options') const T = require('../../../../time') const users = require('../../../../users') +const domain = options.hostname +const devMode = require('minimist')(process.argv.slice(2)).dev + const REMEMBER_ME_AGE = 90 * T.day -const rpID = `localhost` +const rpID = devMode ? `localhost` : domain const expectedOrigin = `https://${rpID}:3001` const generateAttestationOptions = (userID, session) => { @@ -68,55 +72,62 @@ const validateAttestation = (userID, attestationResponse, context) => { const webauthnData = context.req.session.webauthn.attestation const expectedChallenge = webauthnData.challenge - return users.getUserById(userID).then(user => { - return simpleWebauthn.verifyAttestationResponse({ + return Promise.all([ + users.getUserById(userID), + simpleWebauthn.verifyAttestationResponse({ credential: attestationResponse, expectedChallenge: `${expectedChallenge}`, expectedOrigin, expectedRPID: rpID - }).then(async verification => { + }) + ]) + .then(([user, verification]) => { const { verified, attestationInfo } = verification - if (verified && attestationInfo) { - const { - fmt, - counter, - aaguid, - credentialPublicKey, - credentialID, - credentialType, - userVerified, - attestationObject - } = attestationInfo - - const userDevices = await credentials.getHardwareCredentialsOfUser(user.id) - const existingDevice = userDevices.find(device => device.data.credentialID === credentialID) - - if (!existingDevice) { - const newDevice = { - fmt, - counter, - aaguid, - credentialPublicKey, - credentialID, - credentialType, - userVerified, - attestationObject - } - credentials.createHardwareCredential(user.id, newDevice) - } + if (!(verified || attestationInfo)) { + context.req.session.webauthn = null + return verified } - context.req.session.webauthn = null - return verified + const { + fmt, + counter, + aaguid, + credentialPublicKey, + credentialID, + credentialType, + userVerified, + attestationObject + } = attestationInfo + + return credentials.getHardwareCredentialsOfUser(user.id) + .then(userDevices => { + const existingDevice = userDevices.find(device => device.data.credentialID === credentialID) + + if (!existingDevice) { + const newDevice = { + fmt, + counter, + aaguid, + credentialPublicKey, + credentialID, + credentialType, + userVerified, + attestationObject + } + credentials.createHardwareCredential(user.id, newDevice) + } + + context.req.session.webauthn = null + return verified + }) }) - }) } const validateAssertion = (assertionResponse, context) => { const expectedChallenge = context.req.session.webauthn.assertion.challenge - return credentials.getHardwareCredentials().then(async devices => { + return credentials.getHardwareCredentials().then(devices => { const dbAuthenticator = _.find(dev => { return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(assertionResponse.rawId)) === 0 }, devices) @@ -146,18 +157,24 @@ const validateAssertion = (assertionResponse, context) => { const { verified, assertionInfo } = verification - if (verified) { - dbAuthenticator.data.counter = assertionInfo.newCounter - await credentials.updateHardwareCredential(dbAuthenticator) - - const user = await users.getUserById(dbAuthenticator.user_id) - const finalUser = { id: user.id, username: user.username, role: user.role } - context.req.session.user = finalUser - context.req.session.cookie.maxAge = REMEMBER_ME_AGE + if (!verified) { + context.req.session.webauthn = null + return false } - context.req.session.webauthn = null - return verified + dbAuthenticator.data.counter = assertionInfo.newCounter + return Promise.all([ + credentials.updateHardwareCredential(dbAuthenticator), + users.getUserById(dbAuthenticator.user_id) + ]) + .then(([_, user]) => { + const finalUser = { id: user.id, username: user.username, role: user.role } + context.req.session.user = finalUser + context.req.session.cookie.maxAge = REMEMBER_ME_AGE + + context.req.session.webauthn = null + return verified + }) }) } diff --git a/lib/new-admin/graphql/modules/authentication/index.js b/lib/new-admin/graphql/modules/authentication/index.js index d2836d06..1b4591cc 100644 --- a/lib/new-admin/graphql/modules/authentication/index.js +++ b/lib/new-admin/graphql/modules/authentication/index.js @@ -9,7 +9,7 @@ const STRATEGIES = { } // FIDO2FA, FIDOPasswordless or FIDOUsernameless -const CHOSEN_STRATEGY = 'FIDOPasswordless' +const CHOSEN_STRATEGY = 'FIDO2FA' module.exports = { CHOSEN_STRATEGY, diff --git a/migrations/1620335170327-hardware-credentials.js b/migrations/1620335170327-hardware-credentials.js index 73764b16..a3a2b88f 100644 --- a/migrations/1620335170327-hardware-credentials.js +++ b/migrations/1620335170327-hardware-credentials.js @@ -4,11 +4,11 @@ exports.up = function (next) { var sql = [ `ALTER TABLE user_register_tokens ADD COLUMN use_fido BOOLEAN DEFAULT false`, `CREATE TABLE hardware_credentials ( - id UUID PRIMARY KEY, - user_id UUID REFERENCES users(id), + id UUID PRIMARY KEY NOT NULL, + user_id UUID REFERENCES users(id) NOT NULL, created TIMESTAMPTZ DEFAULT now(), last_used TIMESTAMPTZ DEFAULT now(), - data JSONB + data JSONB NOT NULL )` ] diff --git a/new-lamassu-admin/src/pages/Authentication/LoginCard.js b/new-lamassu-admin/src/pages/Authentication/LoginCard.js index 8229254b..e1dc5aef 100644 --- a/new-lamassu-admin/src/pages/Authentication/LoginCard.js +++ b/new-lamassu-admin/src/pages/Authentication/LoginCard.js @@ -13,7 +13,7 @@ import styles from './shared.styles' import { STATES } from './states' // FIDO2FA, FIDOPasswordless or FIDOUsernameless -const AUTHENTICATION_STRATEGY = 'FIDOPasswordless' +const AUTHENTICATION_STRATEGY = 'FIDO2FA' const useStyles = makeStyles(styles) diff --git a/new-lamassu-admin/src/pages/Authentication/LoginState.js b/new-lamassu-admin/src/pages/Authentication/LoginState.js index 394c8ece..45e7d63a 100644 --- a/new-lamassu-admin/src/pages/Authentication/LoginState.js +++ b/new-lamassu-admin/src/pages/Authentication/LoginState.js @@ -115,7 +115,6 @@ const LoginState = ({ state, dispatch, strategy }) => { GENERATE_ASSERTION, { onCompleted: ({ generateAssertionOptions: options }) => { - console.log(options) startAssertion(options) .then(res => { validateAssertion({ diff --git a/new-lamassu-admin/src/pages/UserManagement/UserManagement.js b/new-lamassu-admin/src/pages/UserManagement/UserManagement.js index f26f0f2a..09cd7e32 100644 --- a/new-lamassu-admin/src/pages/UserManagement/UserManagement.js +++ b/new-lamassu-admin/src/pages/UserManagement/UserManagement.js @@ -83,8 +83,7 @@ const Users = () => { const [validateAttestation] = useMutation(VALIDATE_ATTESTATION, { onCompleted: res => { - console.log(res) - // success ? console.log('success') : console.log('failure') + // TODO: show a brief popup to have UX feedback? } })