fix: add domain passthrough to webauthn credential creation functions

This commit is contained in:
Sérgio Salgado 2022-01-25 18:00:08 +00:00
parent a9e74086bf
commit f5540a4c12
9 changed files with 82 additions and 82 deletions

View file

@ -4,25 +4,20 @@ const _ = require('lodash/fp')
const userManagement = require('../userManagement') const userManagement = require('../userManagement')
const credentials = require('../../../../hardware-credentials') const credentials = require('../../../../hardware-credentials')
const options = require('../../../../options')
const T = require('../../../../time') const T = require('../../../../time')
const users = require('../../../../users') const users = require('../../../../users')
const domain = options.hostname
const devMode = require('minimist')(process.argv.slice(2)).dev const devMode = require('minimist')(process.argv.slice(2)).dev
const REMEMBER_ME_AGE = 90 * T.day const REMEMBER_ME_AGE = 90 * T.day
const rpID = domain
const expectedOrigin = `https://${rpID}${devMode ? `:3001` : ``}`
const generateAttestationOptions = (session, options) => { const generateAttestationOptions = (session, options) => {
return users.getUserById(options.userId).then(user => { return users.getUserById(options.userId).then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user]) return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
}).then(([userDevices, user]) => { }).then(([userDevices, user]) => {
const options = simpleWebauthn.generateAttestationOptions({ const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu', rpName: 'Lamassu',
rpID, rpID: options.domain,
userName: user.username, userName: user.username,
userID: user.id, userID: user.id,
timeout: 60000, timeout: 60000,
@ -40,11 +35,11 @@ const generateAttestationOptions = (session, options) => {
session.webauthn = { session.webauthn = {
attestation: { attestation: {
challenge: options.challenge challenge: opts.challenge
} }
} }
return options return opts
}) })
} }
@ -59,7 +54,7 @@ const generateAssertionOptions = (session, options) => {
transports: ['usb', 'ble', 'nfc', 'internal'] transports: ['usb', 'ble', 'nfc', 'internal']
})), })),
userVerification: 'discouraged', userVerification: 'discouraged',
rpID rpID: options.domain
}) })
session.webauthn = { session.webauthn = {
@ -82,8 +77,8 @@ const validateAttestation = (session, options) => {
simpleWebauthn.verifyAttestationResponse({ simpleWebauthn.verifyAttestationResponse({
credential: options.attestationResponse, credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`, expectedChallenge: `${expectedChallenge}`,
expectedOrigin, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: rpID expectedRPID: options.domain
}) })
]) ])
.then(([user, verification]) => { .then(([user, verification]) => {
@ -142,8 +137,8 @@ const validateAssertion = (session, options) => {
verification = simpleWebauthn.verifyAssertionResponse({ verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse, credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`, expectedChallenge: `${expectedChallenge}`,
expectedOrigin, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: rpID, expectedRPID: options.domain,
authenticator: convertedAuthenticator authenticator: convertedAuthenticator
}) })
} catch (err) { } catch (err) {

View file

@ -3,25 +3,20 @@ const base64url = require('base64url')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const credentials = require('../../../../hardware-credentials') const credentials = require('../../../../hardware-credentials')
const options = require('../../../../options')
const T = require('../../../../time') const T = require('../../../../time')
const users = require('../../../../users') const users = require('../../../../users')
const domain = options.hostname
const devMode = require('minimist')(process.argv.slice(2)).dev const devMode = require('minimist')(process.argv.slice(2)).dev
const REMEMBER_ME_AGE = 90 * T.day const REMEMBER_ME_AGE = 90 * T.day
const rpID = domain
const expectedOrigin = `https://${rpID}${devMode ? `:3001` : ``}`
const generateAttestationOptions = (session, options) => { const generateAttestationOptions = (session, options) => {
return users.getUserById(options.userId).then(user => { return users.getUserById(options.userId).then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user]) return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
}).then(([userDevices, user]) => { }).then(([userDevices, user]) => {
const opts = simpleWebauthn.generateAttestationOptions({ const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu', rpName: 'Lamassu',
rpID, rpID: options.domain,
userName: user.username, userName: user.username,
userID: user.id, userID: user.id,
timeout: 60000, timeout: 60000,
@ -58,7 +53,7 @@ const generateAssertionOptions = (session, options) => {
transports: ['usb', 'ble', 'nfc', 'internal'] transports: ['usb', 'ble', 'nfc', 'internal']
})), })),
userVerification: 'discouraged', userVerification: 'discouraged',
rpID rpID: options.domain
}) })
session.webauthn = { session.webauthn = {
@ -81,8 +76,8 @@ const validateAttestation = (session, options) => {
simpleWebauthn.verifyAttestationResponse({ simpleWebauthn.verifyAttestationResponse({
credential: options.attestationResponse, credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`, expectedChallenge: `${expectedChallenge}`,
expectedOrigin, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: rpID expectedRPID: options.domain
}) })
]) ])
.then(([user, verification]) => { .then(([user, verification]) => {
@ -141,8 +136,8 @@ const validateAssertion = (session, options) => {
verification = simpleWebauthn.verifyAssertionResponse({ verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse, credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`, expectedChallenge: `${expectedChallenge}`,
expectedOrigin, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: rpID, expectedRPID: options.domain,
authenticator: convertedAuthenticator authenticator: convertedAuthenticator
}) })
} catch (err) { } catch (err) {

View file

@ -3,23 +3,18 @@ const base64url = require('base64url')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const credentials = require('../../../../hardware-credentials') const credentials = require('../../../../hardware-credentials')
const options = require('../../../../options')
const T = require('../../../../time') const T = require('../../../../time')
const users = require('../../../../users') const users = require('../../../../users')
const domain = options.hostname
const devMode = require('minimist')(process.argv.slice(2)).dev const devMode = require('minimist')(process.argv.slice(2)).dev
const REMEMBER_ME_AGE = 90 * T.day const REMEMBER_ME_AGE = 90 * T.day
const rpID = domain
const expectedOrigin = `https://${rpID}${devMode ? `:3001` : ``}`
const generateAttestationOptions = (session, options) => { const generateAttestationOptions = (session, options) => {
return credentials.getHardwareCredentials().then(devices => { return credentials.getHardwareCredentials().then(devices => {
const opts = simpleWebauthn.generateAttestationOptions({ const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu', rpName: 'Lamassu',
rpID, rpID: options.domain,
userName: `Usernameless user created at ${new Date().toISOString()}`, userName: `Usernameless user created at ${new Date().toISOString()}`,
userID: options.userId, userID: options.userId,
timeout: 60000, timeout: 60000,
@ -46,9 +41,9 @@ const generateAttestationOptions = (session, options) => {
}) })
} }
const generateAssertionOptions = session => { const generateAssertionOptions = (session, options) => {
return credentials.getHardwareCredentials().then(devices => { return credentials.getHardwareCredentials().then(devices => {
const options = simpleWebauthn.generateAssertionOptions({ const opts = simpleWebauthn.generateAssertionOptions({
timeout: 60000, timeout: 60000,
allowCredentials: devices.map(dev => ({ allowCredentials: devices.map(dev => ({
id: dev.data.credentialID, id: dev.data.credentialID,
@ -56,15 +51,15 @@ const generateAssertionOptions = session => {
transports: ['usb', 'ble', 'nfc', 'internal'] transports: ['usb', 'ble', 'nfc', 'internal']
})), })),
userVerification: 'discouraged', userVerification: 'discouraged',
rpID rpID: options.domain
}) })
session.webauthn = { session.webauthn = {
assertion: { assertion: {
challenge: options.challenge challenge: opts.challenge
} }
} }
return options return opts
}) })
} }
@ -77,8 +72,8 @@ const validateAttestation = (session, options) => {
simpleWebauthn.verifyAttestationResponse({ simpleWebauthn.verifyAttestationResponse({
credential: options.attestationResponse, credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`, expectedChallenge: `${expectedChallenge}`,
expectedOrigin, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: rpID expectedRPID: options.domain
}) })
]) ])
.then(([user, verification]) => { .then(([user, verification]) => {
@ -146,8 +141,8 @@ const validateAssertion = (session, options) => {
verification = simpleWebauthn.verifyAssertionResponse({ verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse, credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`, expectedChallenge: `${expectedChallenge}`,
expectedOrigin, expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: rpID, expectedRPID: options.domain,
authenticator: convertedAuthenticator authenticator: convertedAuthenticator
}) })
} catch (err) { } catch (err) {

View file

@ -6,11 +6,11 @@ const sessionManager = require('../../../session-manager')
const getAttestationQueryOptions = variables => { const getAttestationQueryOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) { switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA': case 'FIDO2FA':
return { userId: variables.userID } return { userId: variables.userID, domain: variables.domain }
case 'FIDOPasswordless': case 'FIDOPasswordless':
return { userId: variables.userID } return { userId: variables.userID, domain: variables.domain }
case 'FIDOUsernameless': case 'FIDOUsernameless':
return { userId: variables.userID } return { userId: variables.userID, domain: variables.domain }
default: default:
return {} return {}
} }
@ -19,11 +19,11 @@ const getAttestationQueryOptions = variables => {
const getAssertionQueryOptions = variables => { const getAssertionQueryOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) { switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA': case 'FIDO2FA':
return { username: variables.username, password: variables.password } return { username: variables.username, password: variables.password, domain: variables.domain }
case 'FIDOPasswordless': case 'FIDOPasswordless':
return { username: variables.username } return { username: variables.username, domain: variables.domain }
case 'FIDOUsernameless': case 'FIDOUsernameless':
return {} return { domain: variables.domain }
default: default:
return {} return {}
} }
@ -32,11 +32,11 @@ const getAssertionQueryOptions = variables => {
const getAttestationMutationOptions = variables => { const getAttestationMutationOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) { switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA': case 'FIDO2FA':
return { userId: variables.userID, attestationResponse: variables.attestationResponse } return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
case 'FIDOPasswordless': case 'FIDOPasswordless':
return { userId: variables.userID, attestationResponse: variables.attestationResponse } return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
case 'FIDOUsernameless': case 'FIDOUsernameless':
return { userId: variables.userID, attestationResponse: variables.attestationResponse } return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
default: default:
return {} return {}
} }
@ -45,11 +45,11 @@ const getAttestationMutationOptions = variables => {
const getAssertionMutationOptions = variables => { const getAssertionMutationOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) { switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA': case 'FIDO2FA':
return { username: variables.username, password: variables.password, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse } return { username: variables.username, password: variables.password, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain }
case 'FIDOPasswordless': case 'FIDOPasswordless':
return { username: variables.username, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse } return { username: variables.username, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain }
case 'FIDOUsernameless': case 'FIDOUsernameless':
return { assertionResponse: variables.assertionResponse } return { assertionResponse: variables.assertionResponse, domain: variables.domain }
default: default:
return {} return {}
} }

View file

@ -3,14 +3,14 @@ const authentication = require('../modules/authentication')
const getFIDOStrategyQueryTypes = () => { const getFIDOStrategyQueryTypes = () => {
switch (authentication.CHOSEN_STRATEGY) { switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA': case 'FIDO2FA':
return `generateAttestationOptions(userID: ID!): JSONObject return `generateAttestationOptions(userID: ID!, domain: String!): JSONObject
generateAssertionOptions(username: String!, password: String!): JSONObject` generateAssertionOptions(username: String!, password: String!, domain: String!): JSONObject`
case 'FIDOPasswordless': case 'FIDOPasswordless':
return `generateAttestationOptions(userID: ID!): JSONObject return `generateAttestationOptions(userID: ID!, domain: String!): JSONObject
generateAssertionOptions(username: String!): JSONObject` generateAssertionOptions(username: String!, domain: String!): JSONObject`
case 'FIDOUsernameless': case 'FIDOUsernameless':
return `generateAttestationOptions(userID: ID!): JSONObject return `generateAttestationOptions(userID: ID!, domain: String!): JSONObject
generateAssertionOptions: JSONObject` generateAssertionOptions(domain: String!): JSONObject`
default: default:
return `` return ``
} }
@ -19,14 +19,14 @@ const getFIDOStrategyQueryTypes = () => {
const getFIDOStrategyMutationsTypes = () => { const getFIDOStrategyMutationsTypes = () => {
switch (authentication.CHOSEN_STRATEGY) { switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA': case 'FIDO2FA':
return `validateAttestation(userID: ID!, attestationResponse: JSONObject!): Boolean return `validateAttestation(userID: ID!, attestationResponse: JSONObject!, domain: String!): Boolean
validateAssertion(username: String!, password: String!, rememberMe: Boolean!, assertionResponse: JSONObject!): Boolean` validateAssertion(username: String!, password: String!, rememberMe: Boolean!, assertionResponse: JSONObject!, domain: String!): Boolean`
case 'FIDOPasswordless': case 'FIDOPasswordless':
return `validateAttestation(userID: ID!, attestationResponse: JSONObject!): Boolean return `validateAttestation(userID: ID!, attestationResponse: JSONObject!, domain: String!): Boolean
validateAssertion(username: String!, rememberMe: Boolean!, assertionResponse: JSONObject!): Boolean` validateAssertion(username: String!, rememberMe: Boolean!, assertionResponse: JSONObject!, domain: String!): Boolean`
case 'FIDOUsernameless': case 'FIDOUsernameless':
return `validateAttestation(userID: ID!, attestationResponse: JSONObject!): Boolean return `validateAttestation(userID: ID!, attestationResponse: JSONObject!, domain: String!): Boolean
validateAssertion(assertionResponse: JSONObject!): Boolean` validateAssertion(assertionResponse: JSONObject!, domain: String!): Boolean`
default: default:
return `` return ``
} }

View file

@ -21,7 +21,6 @@ router.use('*', async (req, res, next) => getOperatorId('authentication').then((
cookie: { cookie: {
httpOnly: true, httpOnly: true,
secure: true, secure: true,
domain: hostname,
sameSite: true, sameSite: true,
maxAge: 60 * 10 * 1000 // 10 minutes maxAge: 60 * 10 * 1000 // 10 minutes
} }

View file

@ -42,10 +42,10 @@ const InputFIDOState = ({ state, strategy }) => {
const GENERATE_ASSERTION = gql` const GENERATE_ASSERTION = gql`
query generateAssertionOptions($username: String!${ query generateAssertionOptions($username: String!${
strategy === 'FIDO2FA' ? `, $password: String!` : `` strategy === 'FIDO2FA' ? `, $password: String!` : ``
}) { }, $domain: String!) {
generateAssertionOptions(username: $username${ generateAssertionOptions(username: $username${
strategy === 'FIDO2FA' ? `, password: $password` : `` strategy === 'FIDO2FA' ? `, password: $password` : ``
}) }, domain: $domain)
} }
` `
@ -55,12 +55,14 @@ const InputFIDOState = ({ state, strategy }) => {
${strategy === 'FIDO2FA' ? `, $password: String!` : ``} ${strategy === 'FIDO2FA' ? `, $password: String!` : ``}
$rememberMe: Boolean! $rememberMe: Boolean!
$assertionResponse: JSONObject! $assertionResponse: JSONObject!
$domain: String!
) { ) {
validateAssertion( validateAssertion(
username: $username username: $username
${strategy === 'FIDO2FA' ? `password: $password` : ``} ${strategy === 'FIDO2FA' ? `password: $password` : ``}
rememberMe: $rememberMe rememberMe: $rememberMe
assertionResponse: $assertionResponse assertionResponse: $assertionResponse
domain: $domain
) )
} }
` `
@ -90,10 +92,12 @@ const InputFIDOState = ({ state, strategy }) => {
strategy === 'FIDO2FA' strategy === 'FIDO2FA'
? { ? {
username: state.clientField, username: state.clientField,
password: state.passwordField password: state.passwordField,
domain: window.location.hostname
} }
: { : {
username: localClientField username: localClientField,
domain: window.location.hostname
}, },
onCompleted: ({ generateAssertionOptions: options }) => { onCompleted: ({ generateAssertionOptions: options }) => {
startAssertion(options) startAssertion(options)
@ -104,12 +108,14 @@ const InputFIDOState = ({ state, strategy }) => {
username: state.clientField, username: state.clientField,
password: state.passwordField, password: state.passwordField,
rememberMe: state.rememberMeField, rememberMe: state.rememberMeField,
assertionResponse: res assertionResponse: res,
domain: window.location.hostname
} }
: { : {
username: localClientField, username: localClientField,
rememberMe: localRememberMeField, rememberMe: localRememberMeField,
assertionResponse: res assertionResponse: res,
domain: window.location.hostname
} }
validateAssertion({ validateAssertion({
variables variables

View file

@ -24,14 +24,17 @@ const LOGIN = gql`
` `
const GENERATE_ASSERTION = gql` const GENERATE_ASSERTION = gql`
query generateAssertionOptions { query generateAssertionOptions($domain: String!) {
generateAssertionOptions generateAssertionOptions(domain: $domain)
} }
` `
const VALIDATE_ASSERTION = gql` const VALIDATE_ASSERTION = gql`
mutation validateAssertion($assertionResponse: JSONObject!) { mutation validateAssertion(
validateAssertion(assertionResponse: $assertionResponse) $assertionResponse: JSONObject!
$domain: String!
) {
validateAssertion(assertionResponse: $assertionResponse, domain: $domain)
} }
` `
@ -117,7 +120,8 @@ const LoginState = ({ state, dispatch, strategy }) => {
.then(res => { .then(res => {
validateAssertion({ validateAssertion({
variables: { variables: {
assertionResponse: res assertionResponse: res,
domain: window.location.hostname
} }
}) })
}) })
@ -212,7 +216,9 @@ const LoginState = ({ state, dispatch, strategy }) => {
type="button" type="button"
onClick={() => { onClick={() => {
return strategy === 'FIDOUsernameless' return strategy === 'FIDOUsernameless'
? assertionOptions() ? assertionOptions({
variables: { domain: window.location.hostname }
})
: dispatch({ : dispatch({
type: 'FIDO', type: 'FIDO',
payload: {} payload: {}

View file

@ -41,8 +41,8 @@ const GET_USERS = gql`
` `
const GENERATE_ATTESTATION = gql` const GENERATE_ATTESTATION = gql`
query generateAttestationOptions($userID: ID!) { query generateAttestationOptions($userID: ID!, $domain: String!) {
generateAttestationOptions(userID: $userID) generateAttestationOptions(userID: $userID, domain: $domain)
} }
` `
@ -50,10 +50,12 @@ const VALIDATE_ATTESTATION = gql`
mutation validateAttestation( mutation validateAttestation(
$userID: ID! $userID: ID!
$attestationResponse: JSONObject! $attestationResponse: JSONObject!
$domain: String!
) { ) {
validateAttestation( validateAttestation(
userID: $userID userID: $userID
attestationResponse: $attestationResponse attestationResponse: $attestationResponse
domain: $domain
) )
} }
` `
@ -100,11 +102,12 @@ const Users = () => {
const [generateAttestationOptions] = useLazyQuery(GENERATE_ATTESTATION, { const [generateAttestationOptions] = useLazyQuery(GENERATE_ATTESTATION, {
onCompleted: ({ generateAttestationOptions: options }) => { onCompleted: ({ generateAttestationOptions: options }) => {
startAttestation(options).then(res => { return startAttestation(options).then(res => {
validateAttestation({ validateAttestation({
variables: { variables: {
userID: userInfo.id, userID: userInfo.id,
attestationResponse: res attestationResponse: res,
domain: window.location.hostname
} }
}) })
}) })
@ -194,7 +197,8 @@ const Users = () => {
setUserInfo(u) setUserInfo(u)
generateAttestationOptions({ generateAttestationOptions({
variables: { variables: {
userID: u.id userID: u.id,
domain: window.location.hostname
} }
}) })
}}> }}>