Merge pull request #712 from chaotixkilla/feat-implement-fido-auth

Hardware credential authentication
This commit is contained in:
Rafael Taranto 2021-11-24 22:55:34 +00:00 committed by GitHub
commit 540f1581f5
19 changed files with 1360 additions and 72 deletions

View file

@ -1,7 +1,7 @@
#!/usr/bin/env node
const { asyncLocalStorage, defaultStore } = require('../lib/async-storage')
const authentication = require('../lib/new-admin/graphql/modules/authentication')
const userManagement = require('../lib/new-admin/graphql/modules/userManagement')
const options = require('../lib/options')
const name = process.argv[2]
@ -31,7 +31,7 @@ if (role !== 'user' && role !== 'superuser') {
}
asyncLocalStorage.run(defaultStore(), () => {
authentication.createRegisterToken(name, role).then(token => {
userManagement.createRegisterToken(name, role).then(token => {
if (!token) {
console.log(`A user named ${name} already exists!`)
process.exit(2)

View file

@ -0,0 +1,36 @@
const uuid = require('uuid')
const db = require('./db')
function createHardwareCredential (userID, credentialData) {
const sql = `INSERT INTO hardware_credentials (id, user_id, data) VALUES ($1, $2, $3)`
return db.none(sql, [uuid.v4(), userID, credentialData])
}
function getHardwareCredentials () {
const sql = `SELECT * FROM hardware_credentials`
return db.any(sql)
}
function getHardwareCredentialsByUserId (userID) {
const sql = `SELECT * FROM hardware_credentials WHERE user_id=$1`
return db.any(sql, [userID])
}
function getUserByUserHandle (userHandle) {
const sql = `SELECT users.id, users.username, users.role FROM users INNER JOIN hardware_credentials hc ON users.id=hc.user_id WHERE data->>'userHandle'=$1::jsonb::text`
return db.oneOrNone(sql, [userHandle])
}
function updateHardwareCredential (credential) {
const sql = `UPDATE hardware_credentials SET last_used=now(), data=$1 WHERE id=$2`
return db.none(sql, [credential.data, credential.id])
}
module.exports = {
createHardwareCredential,
getHardwareCredentials,
getHardwareCredentialsByUserId,
getUserByUserHandle,
updateHardwareCredential
}

View file

@ -0,0 +1,180 @@
const simpleWebauthn = require('@simplewebauthn/server')
const base64url = require('base64url')
const _ = require('lodash/fp')
const userManagement = require('../userManagement')
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 = devMode ? `localhost:3001` : domain
const expectedOrigin = `https://${rpID}`
const generateAttestationOptions = (session, options) => {
return users.getUserById(options.userId).then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
}).then(([userDevices, user]) => {
const options = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID,
userName: user.username,
userID: user.id,
timeout: 60000,
attestationType: 'indirect',
excludeCredentials: userDevices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: false
}
})
session.webauthn = {
attestation: {
challenge: options.challenge
}
}
return options
})
}
const generateAssertionOptions = (session, options) => {
return userManagement.authenticateUser(options.username, options.password).then(user => {
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const opts = simpleWebauthn.generateAssertionOptions({
timeout: 60000,
allowCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
userVerification: 'discouraged',
rpID
})
session.webauthn = {
assertion: {
challenge: opts.challenge
}
}
return opts
})
})
}
const validateAttestation = (session, options) => {
const webauthnData = session.webauthn.attestation
const expectedChallenge = webauthnData.challenge
return Promise.all([
users.getUserById(options.userId),
simpleWebauthn.verifyAttestationResponse({
credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin,
expectedRPID: rpID
})
])
.then(([user, verification]) => {
const { verified, attestationInfo } = verification
if (!(verified || attestationInfo)) {
session.webauthn = null
return false
}
const {
counter,
credentialPublicKey,
credentialID
} = attestationInfo
return credentials.getHardwareCredentialsByUserId(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)
}
session.webauthn = null
return verified
})
})
}
const validateAssertion = (session, options) => {
return userManagement.authenticateUser(options.username, options.password).then(user => {
const expectedChallenge = session.webauthn.assertion.challenge
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const dbAuthenticator = _.find(dev => {
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
}, devices)
if (!dbAuthenticator.data) {
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
}
const convertedAuthenticator = _.merge(
dbAuthenticator.data,
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
)
let verification
try {
verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin,
expectedRPID: rpID,
authenticator: convertedAuthenticator
})
} catch (err) {
console.error(err)
return false
}
const { verified, assertionInfo } = verification
if (!verified) {
session.webauthn = null
return false
}
dbAuthenticator.data.counter = assertionInfo.newCounter
return credentials.updateHardwareCredential(dbAuthenticator)
.then(() => {
const finalUser = { id: user.id, username: user.username, role: user.role }
session.user = finalUser
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
})
})
})
}
module.exports = {
generateAttestationOptions,
generateAssertionOptions,
validateAttestation,
validateAssertion
}

View file

@ -0,0 +1,179 @@
const simpleWebauthn = require('@simplewebauthn/server')
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 = devMode ? `localhost:3001` : domain
const expectedOrigin = `https://${rpID}`
const generateAttestationOptions = (session, options) => {
return users.getUserById(options.userId).then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
}).then(([userDevices, user]) => {
const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID,
userName: user.username,
userID: user.id,
timeout: 60000,
attestationType: 'indirect',
excludeCredentials: userDevices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: false
}
})
session.webauthn = {
attestation: {
challenge: opts.challenge
}
}
return opts
})
}
const generateAssertionOptions = (session, options) => {
return users.getUserByUsername(options.username).then(user => {
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const opts = simpleWebauthn.generateAssertionOptions({
timeout: 60000,
allowCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
userVerification: 'discouraged',
rpID
})
session.webauthn = {
assertion: {
challenge: opts.challenge
}
}
return opts
})
})
}
const validateAttestation = (session, options) => {
const webauthnData = session.webauthn.attestation
const expectedChallenge = webauthnData.challenge
return Promise.all([
users.getUserById(options.userId),
simpleWebauthn.verifyAttestationResponse({
credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin,
expectedRPID: rpID
})
])
.then(([user, verification]) => {
const { verified, attestationInfo } = verification
if (!(verified || attestationInfo)) {
session.webauthn = null
return false
}
const {
counter,
credentialPublicKey,
credentialID
} = attestationInfo
return credentials.getHardwareCredentialsByUserId(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)
}
session.webauthn = null
return verified
})
})
}
const validateAssertion = (session, options) => {
return users.getUserByUsername(options.username).then(user => {
const expectedChallenge = session.webauthn.assertion.challenge
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const dbAuthenticator = _.find(dev => {
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
}, devices)
if (!dbAuthenticator.data) {
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
}
const convertedAuthenticator = _.merge(
dbAuthenticator.data,
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
)
let verification
try {
verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin,
expectedRPID: rpID,
authenticator: convertedAuthenticator
})
} catch (err) {
console.error(err)
return false
}
const { verified, assertionInfo } = verification
if (!verified) {
context.req.session.webauthn = null
return false
}
dbAuthenticator.data.counter = assertionInfo.newCounter
return credentials.updateHardwareCredential(dbAuthenticator)
.then(() => {
const finalUser = { id: user.id, username: user.username, role: user.role }
session.user = finalUser
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
})
})
})
}
module.exports = {
generateAttestationOptions,
generateAssertionOptions,
validateAttestation,
validateAssertion
}

View file

@ -0,0 +1,186 @@
const simpleWebauthn = require('@simplewebauthn/server')
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 = devMode ? `localhost:3001` : domain
const expectedOrigin = `https://${rpID}`
const generateAttestationOptions = (session, options) => {
return credentials.getHardwareCredentials().then(devices => {
const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID,
userName: `Usernameless user created at ${new Date().toISOString()}`,
userID: options.userId,
timeout: 60000,
attestationType: 'direct',
excludeCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
authenticatorSelection: {
authenticatorAttachment: 'cross-platform',
userVerification: 'discouraged',
requireResidentKey: false
}
})
session.webauthn = {
attestation: {
challenge: opts.challenge
}
}
return opts
})
}
const generateAssertionOptions = session => {
return credentials.getHardwareCredentials().then(devices => {
const options = simpleWebauthn.generateAssertionOptions({
timeout: 60000,
allowCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
userVerification: 'discouraged',
rpID
})
session.webauthn = {
assertion: {
challenge: options.challenge
}
}
return options
})
}
const validateAttestation = (session, options) => {
const webauthnData = session.webauthn.attestation
const expectedChallenge = webauthnData.challenge
return Promise.all([
users.getUserById(options.userId),
simpleWebauthn.verifyAttestationResponse({
credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin,
expectedRPID: rpID
})
])
.then(([user, verification]) => {
const { verified, attestationInfo } = verification
if (!(verified || attestationInfo)) {
session.webauthn = null
return verified
}
const {
fmt,
counter,
aaguid,
credentialPublicKey,
credentialID,
credentialType,
userVerified,
attestationObject
} = attestationInfo
return credentials.getHardwareCredentialsByUserId(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)
}
session.webauthn = null
return verified
})
})
}
const validateAssertion = (session, options) => {
const expectedChallenge = session.webauthn.assertion.challenge
return credentials.getHardwareCredentials().then(devices => {
const dbAuthenticator = _.find(dev => {
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
}, devices)
if (!dbAuthenticator.data) {
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
}
const convertedAuthenticator = _.merge(
dbAuthenticator.data,
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
)
let verification
try {
verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin,
expectedRPID: rpID,
authenticator: convertedAuthenticator
})
} catch (err) {
console.error(err)
return false
}
const { verified, assertionInfo } = verification
if (!verified) {
session.webauthn = null
return false
}
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 }
session.user = finalUser
session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
})
})
}
module.exports = {
generateAttestationOptions,
generateAssertionOptions,
validateAttestation,
validateAssertion
}

View file

@ -0,0 +1,17 @@
const FIDO2FA = require('./FIDO2FAStrategy')
const FIDOPasswordless = require('./FIDOPasswordlessStrategy')
const FIDOUsernameless = require('./FIDOUsernamelessStrategy')
const STRATEGIES = {
FIDO2FA,
FIDOPasswordless,
FIDOUsernameless
}
// FIDO2FA, FIDOPasswordless or FIDOUsernameless
const CHOSEN_STRATEGY = 'FIDO2FA'
module.exports = {
CHOSEN_STRATEGY,
strategy: STRATEGIES[CHOSEN_STRATEGY]
}

View file

@ -1,5 +1,6 @@
const otplib = require('otplib')
const argon2 = require('argon2')
const _ = require('lodash/fp')
const constants = require('../../../constants')
const authTokens = require('../../../auth-tokens')
@ -8,6 +9,7 @@ const T = require('../../../time')
const users = require('../../../users')
const sessionManager = require('../../../session-manager')
const authErrors = require('../errors/authentication')
const credentials = require('../../../hardware-credentials')
const REMEMBER_ME_AGE = 90 * T.day
@ -140,10 +142,14 @@ const deleteSession = (sessionID, context) => {
}
const login = (username, password) => {
return authenticateUser(username, password).then(user => {
const twoFASecret = user.twofa_code
return twoFASecret ? 'INPUT2FA' : 'SETUP2FA'
})
return authenticateUser(username, password)
.then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user.twofa_code])
})
.then(([devices, twoFASecret]) => {
if (!_.isEmpty(devices)) return 'FIDO'
return twoFASecret ? 'INPUT2FA' : 'SETUP2FA'
})
}
const input2FA = (username, password, rememberMe, code, context) => {
@ -234,6 +240,7 @@ const reset2FA = (token, userID, code, context) => {
}
module.exports = {
authenticateUser,
getUserData,
get2FASecret,
confirm2FA,

View file

@ -1,34 +1,91 @@
const authentication = require('../modules/authentication')
const userManagement = require('../modules/userManagement')
const users = require('../../../users')
const sessionManager = require('../../../session-manager')
const getAttestationQueryOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { userId: variables.userID }
case 'FIDOPasswordless':
return { userId: variables.userID }
case 'FIDOUsernameless':
return { userId: variables.userID }
default:
return {}
}
}
const getAssertionQueryOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { username: variables.username, password: variables.password }
case 'FIDOPasswordless':
return { username: variables.username }
case 'FIDOUsernameless':
return {}
default:
return {}
}
}
const getAttestationMutationOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { userId: variables.userID, attestationResponse: variables.attestationResponse }
case 'FIDOPasswordless':
return { userId: variables.userID, attestationResponse: variables.attestationResponse }
case 'FIDOUsernameless':
return { userId: variables.userID, attestationResponse: variables.attestationResponse }
default:
return {}
}
}
const getAssertionMutationOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { username: variables.username, password: variables.password, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse }
case 'FIDOPasswordless':
return { username: variables.username, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse }
case 'FIDOUsernameless':
return { assertionResponse: variables.assertionResponse }
default:
return {}
}
}
const resolver = {
Query: {
users: () => users.getUsers(),
sessions: () => sessionManager.getSessions(),
userSessions: (...[, { username }]) => sessionManager.getSessionsByUsername(username),
userData: (...[, {}, context]) => authentication.getUserData(context),
get2FASecret: (...[, { username, password }]) => authentication.get2FASecret(username, password),
confirm2FA: (...[, { code }, context]) => authentication.confirm2FA(code, context),
validateRegisterLink: (...[, { token }]) => authentication.validateRegisterLink(token),
validateResetPasswordLink: (...[, { token }]) => authentication.validateResetPasswordLink(token),
validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token)
userData: (...[, {}, context]) => userManagement.getUserData(context),
get2FASecret: (...[, { username, password }]) => userManagement.get2FASecret(username, password),
confirm2FA: (...[, { code }, context]) => userManagement.confirm2FA(code, context),
validateRegisterLink: (...[, { token }]) => userManagement.validateRegisterLink(token),
validateResetPasswordLink: (...[, { token }]) => userManagement.validateResetPasswordLink(token),
validateReset2FALink: (...[, { token }]) => userManagement.validateReset2FALink(token),
generateAttestationOptions: (...[, variables, context]) => authentication.strategy.generateAttestationOptions(context.req.session, getAttestationQueryOptions(variables)),
generateAssertionOptions: (...[, variables, context]) => authentication.strategy.generateAssertionOptions(context.req.session, getAssertionQueryOptions(variables))
},
Mutation: {
enableUser: (...[, { confirmationCode, id }, context]) => authentication.enableUser(confirmationCode, id, context),
disableUser: (...[, { confirmationCode, id }, context]) => authentication.disableUser(confirmationCode, id, context),
deleteSession: (...[, { sid }, context]) => authentication.deleteSession(sid, context),
enableUser: (...[, { confirmationCode, id }, context]) => userManagement.enableUser(confirmationCode, id, context),
disableUser: (...[, { confirmationCode, id }, context]) => userManagement.disableUser(confirmationCode, id, context),
deleteSession: (...[, { sid }, context]) => userManagement.deleteSession(sid, context),
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => authentication.changeUserRole(confirmationCode, id, newRole, context),
login: (...[, { username, password }]) => authentication.login(username, password),
input2FA: (...[, { username, password, rememberMe, code }, context]) => authentication.input2FA(username, password, rememberMe, code, context),
setup2FA: (...[, { username, password, rememberMe, codeConfirmation }, context]) => authentication.setup2FA(username, password, rememberMe, codeConfirmation, context),
createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => authentication.createResetPasswordToken(confirmationCode, userID, context),
createReset2FAToken: (...[, { confirmationCode, userID }, context]) => authentication.createReset2FAToken(confirmationCode, userID, context),
createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role),
register: (...[, { token, username, password, role }]) => authentication.register(token, username, password, role),
resetPassword: (...[, { token, userID, newPassword }, context]) => authentication.resetPassword(token, userID, newPassword, context),
reset2FA: (...[, { token, userID, code }, context]) => authentication.reset2FA(token, userID, code, context)
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => userManagement.changeUserRole(confirmationCode, id, newRole, context),
login: (...[, { username, password }]) => userManagement.login(username, password),
input2FA: (...[, { username, password, rememberMe, code }, context]) => userManagement.input2FA(username, password, rememberMe, code, context),
setup2FA: (...[, { username, password, rememberMe, codeConfirmation }, context]) => userManagement.setup2FA(username, password, rememberMe, codeConfirmation, context),
createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => userManagement.createResetPasswordToken(confirmationCode, userID, context),
createReset2FAToken: (...[, { confirmationCode, userID }, context]) => userManagement.createReset2FAToken(confirmationCode, userID, context),
createRegisterToken: (...[, { username, role }]) => userManagement.createRegisterToken(username, role),
register: (...[, { token, username, password, role }]) => userManagement.register(token, username, password, role),
resetPassword: (...[, { token, userID, newPassword }, context]) => userManagement.resetPassword(token, userID, newPassword, context),
reset2FA: (...[, { token, userID, code }, context]) => userManagement.reset2FA(token, userID, code, context),
validateAttestation: (...[, variables, context]) => authentication.strategy.validateAttestation(context.req.session, getAttestationMutationOptions(variables)),
validateAssertion: (...[, variables, context]) => authentication.strategy.validateAssertion(context.req.session, getAssertionMutationOptions(variables))
}
}

View file

@ -1,3 +1,37 @@
const authentication = require('../modules/authentication')
const getFIDOStrategyQueryTypes = () => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return `generateAttestationOptions(userID: ID!): JSONObject
generateAssertionOptions(username: String!, password: String!): JSONObject`
case 'FIDOPasswordless':
return `generateAttestationOptions(userID: ID!): JSONObject
generateAssertionOptions(username: String!): JSONObject`
case 'FIDOUsernameless':
return `generateAttestationOptions(userID: ID!): JSONObject
generateAssertionOptions: JSONObject`
default:
return ``
}
}
const getFIDOStrategyMutationsTypes = () => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return `validateAttestation(userID: ID!, attestationResponse: JSONObject!): Boolean
validateAssertion(username: String!, password: String!, rememberMe: Boolean!, assertionResponse: JSONObject!): Boolean`
case 'FIDOPasswordless':
return `validateAttestation(userID: ID!, attestationResponse: JSONObject!): Boolean
validateAssertion(username: String!, rememberMe: Boolean!, assertionResponse: JSONObject!): Boolean`
case 'FIDOUsernameless':
return `validateAttestation(userID: ID!, attestationResponse: JSONObject!): Boolean
validateAssertion(assertionResponse: JSONObject!): Boolean`
default:
return ``
}
}
const typeDef = `
directive @auth(
requires: [Role] = [USER, SUPERUSER]
@ -54,6 +88,7 @@ const typeDef = `
validateRegisterLink(token: String!): User
validateResetPasswordLink(token: String!): User
validateReset2FALink(token: String!): TwoFactorSecret
${getFIDOStrategyQueryTypes()}
}
type Mutation {
@ -72,6 +107,7 @@ const typeDef = `
register(token: String!, username: String!, password: String!, role: String!): Boolean
resetPassword(token: String!, userID: ID!, newPassword: String!): Boolean
reset2FA(token: String!, userID: ID!, code: String!): Boolean
${getFIDOStrategyMutationsTypes()}
}
`

View file

@ -0,0 +1,20 @@
var db = require('./db')
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 NOT NULL,
user_id UUID REFERENCES users(id) NOT NULL,
created TIMESTAMPTZ DEFAULT now(),
last_used TIMESTAMPTZ DEFAULT now(),
data JSONB NOT NULL
)`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -17172,25 +17172,6 @@
}
}
},
"keccak": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz",
"integrity": "sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==",
"requires": {
"node-addon-api": "^2.0.0",
"node-gyp-build": "^4.2.0",
"readable-stream": "^3.6.0"
}
},
"keccak256": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/keccak256/-/keccak256-1.0.3.tgz",
"integrity": "sha512-EkF/4twuPm1V/gn75nejOUrKfDUJn87RMLzDWosXF3pXuOvesiSgX35GcmbqzdImCASEkE/WaklWGWSa+Ha5bQ==",
"requires": {
"bn.js": "^4.11.8",
"keccak": "^3.0.1"
}
},
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@ -17232,14 +17213,12 @@
"from": "git+https://github.com/lamassu/lamassu-coins.git",
"requires": {
"bech32": "2.0.0",
"big-integer": "^1.6.48",
"bignumber.js": "^9.0.0",
"bitcoinjs-lib": "4.0.3",
"bs58check": "^2.0.2",
"cashaddrjs": "~0.2.8",
"crypto-js": "^3.1.9-1",
"ethereumjs-icap": "^0.3.1",
"keccak256": "^1.0.2",
"lodash": "^4.17.10"
},
"dependencies": {
@ -18486,11 +18465,6 @@
}
}
},
"node-addon-api": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="
},
"node-dir": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz",
@ -18512,11 +18486,6 @@
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
"dev": true
},
"node-gyp-build": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz",
"integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q=="
},
"node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",

View file

@ -0,0 +1,215 @@
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import { startAssertion } from '@simplewebauthn/browser'
import { Field, Form, Formik } from 'formik'
import gql from 'graphql-tag'
import React, { useState, useContext } from 'react'
import { useHistory } from 'react-router-dom'
import * as Yup from 'yup'
import AppContext from 'src/AppContext'
import { Button } from 'src/components/buttons'
import { Checkbox, TextInput } from 'src/components/inputs/formik'
import { H2, Label2, P } from 'src/components/typography'
import styles from './shared.styles'
const useStyles = makeStyles(styles)
const GET_USER_DATA = gql`
{
userData {
id
username
role
}
}
`
const validationSchema = Yup.object().shape({
localClient: Yup.string()
.required('Client field is required!')
.email('Username field should be in an email format!'),
localRememberMe: Yup.boolean()
})
const initialValues = {
localClient: '',
localRememberMe: false
}
const InputFIDOState = ({ state, strategy }) => {
const GENERATE_ASSERTION = gql`
query generateAssertionOptions($username: String!${
strategy === 'FIDO2FA' ? `, $password: String!` : ``
}) {
generateAssertionOptions(username: $username${
strategy === 'FIDO2FA' ? `, password: $password` : ``
})
}
`
const VALIDATE_ASSERTION = gql`
mutation validateAssertion(
$username: String!
${strategy === 'FIDO2FA' ? `, $password: String!` : ``}
$rememberMe: Boolean!
$assertionResponse: JSONObject!
) {
validateAssertion(
username: $username
${strategy === 'FIDO2FA' ? `password: $password` : ``}
rememberMe: $rememberMe
assertionResponse: $assertionResponse
)
}
`
const classes = useStyles()
const history = useHistory()
const { setUserData } = useContext(AppContext)
const [localClientField, setLocalClientField] = useState('')
const [localRememberMeField, setLocalRememberMeField] = useState(false)
const [invalidUsername, setInvalidUsername] = useState(false)
const [invalidToken, setInvalidToken] = useState(false)
const [validateAssertion, { error: mutationError }] = useMutation(
VALIDATE_ASSERTION,
{
onCompleted: ({ validateAssertion: success }) => {
success ? getUserData() : setInvalidToken(true)
}
}
)
const [assertionOptions, { error: assertionQueryError }] = useLazyQuery(
GENERATE_ASSERTION,
{
variables:
strategy === 'FIDO2FA'
? {
username: state.clientField,
password: state.passwordField
}
: {
username: localClientField
},
onCompleted: ({ generateAssertionOptions: options }) => {
startAssertion(options)
.then(res => {
const variables =
strategy === 'FIDO2FA'
? {
username: state.clientField,
password: state.passwordField,
rememberMe: state.rememberMeField,
assertionResponse: res
}
: {
username: localClientField,
rememberMe: localRememberMeField,
assertionResponse: res
}
validateAssertion({
variables
})
})
.catch(err => {
console.error(err)
setInvalidToken(true)
})
}
}
)
const [getUserData, { error: queryError }] = useLazyQuery(GET_USER_DATA, {
onCompleted: ({ userData }) => {
setUserData(userData)
history.push('/')
}
})
const getErrorMsg = (formikErrors, formikTouched) => {
if (!formikErrors || !formikTouched) return null
if (assertionQueryError || queryError || mutationError)
return 'Internal server error'
if (formikErrors.client && formikTouched.client) return formikErrors.client
if (invalidUsername) return 'Invalid login.'
if (invalidToken) return 'Code is invalid. Please try again.'
return null
}
return (
<>
{strategy === 'FIDOPasswordless' && (
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={values => {
setInvalidUsername(false)
setLocalClientField(values.localClient)
setLocalRememberMeField(values.localRememberMe)
assertionOptions()
}}>
{({ errors, touched }) => (
<Form id="fido-form">
<Field
name="localClient"
label="Client"
size="lg"
component={TextInput}
fullWidth
autoFocus
className={classes.input}
error={getErrorMsg(errors, touched)}
onKeyUp={() => {
if (invalidUsername) setInvalidUsername(false)
}}
/>
<div className={classes.rememberMeWrapper}>
<Field
name="localRememberMe"
className={classes.checkbox}
component={Checkbox}
/>
<Label2 className={classes.inputLabel}>
Keep me logged in
</Label2>
</div>
<div className={classes.twofaFooter}>
{getErrorMsg(errors, touched) && (
<P className={classes.errorMessage}>
{getErrorMsg(errors, touched)}
</P>
)}
<Button
type="submit"
form="fido-form"
buttonClassName={classes.loginButton}>
Use FIDO
</Button>
</div>
</Form>
)}
</Formik>
)}
{strategy === 'FIDO2FA' && (
<>
<H2 className={classes.info}>
Insert your hardware key and follow the instructions
</H2>
<Button
type="button"
form="fido-form"
onClick={() => assertionOptions()}
buttonClassName={classes.loginButton}>
Use FIDO
</Button>
</>
)}
</>
)
}
export default InputFIDOState

View file

@ -6,11 +6,15 @@ import { H5 } from 'src/components/typography'
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
import Input2FAState from './Input2FAState'
import InputFIDOState from './InputFIDOState'
import LoginState from './LoginState'
import Setup2FAState from './Setup2FAState'
import styles from './shared.styles'
import { STATES } from './states'
// FIDO2FA, FIDOPasswordless or FIDOUsernameless
const AUTHENTICATION_STRATEGY = 'FIDO2FA'
const useStyles = makeStyles(styles)
const initialState = {
@ -34,11 +38,21 @@ const LoginCard = () => {
const renderState = () => {
switch (state.loginState) {
case STATES.LOGIN:
return <LoginState state={state} dispatch={dispatch} />
return (
<LoginState
state={state}
dispatch={dispatch}
strategy={AUTHENTICATION_STRATEGY}
/>
)
case STATES.INPUT_2FA:
return <Input2FAState state={state} dispatch={dispatch} />
case STATES.SETUP_2FA:
return <Setup2FAState state={state} dispatch={dispatch} />
case STATES.FIDO:
return (
<InputFIDOState state={state} strategy={AUTHENTICATION_STRATEGY} />
)
default:
break
}

View file

@ -1,17 +1,19 @@
import { useMutation } from '@apollo/react-hooks'
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import { startAssertion } from '@simplewebauthn/browser'
import base64 from 'base-64'
import { Field, Form, Formik } from 'formik'
import gql from 'graphql-tag'
import React from 'react'
import React, { useContext } from 'react'
import { useHistory } from 'react-router-dom'
import * as Yup from 'yup'
import AppContext from 'src/AppContext'
import { Button } from 'src/components/buttons'
import { Checkbox, SecretInput, TextInput } from 'src/components/inputs/formik'
import { Label3, P } from 'src/components/typography'
import styles from './shared.styles'
import { STATES } from './states'
const useStyles = makeStyles(styles)
@ -21,6 +23,28 @@ const LOGIN = gql`
}
`
const GENERATE_ASSERTION = gql`
query generateAssertionOptions {
generateAssertionOptions
}
`
const VALIDATE_ASSERTION = gql`
mutation validateAssertion($assertionResponse: JSONObject!) {
validateAssertion(assertionResponse: $assertionResponse)
}
`
const GET_USER_DATA = gql`
{
userData {
id
username
role
}
}
`
const validationSchema = Yup.object().shape({
client: Yup.string()
.required('Client field is required!')
@ -44,10 +68,12 @@ const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
return null
}
const LoginState = ({ state, dispatch }) => {
const LoginState = ({ state, dispatch, strategy }) => {
const classes = useStyles()
const history = useHistory()
const { setUserData } = useContext(AppContext)
const [login, { error: mutationError }] = useMutation(LOGIN)
const [login, { error: loginMutationError }] = useMutation(LOGIN)
const submitLogin = async (username, password, rememberMe) => {
const options = {
@ -65,11 +91,8 @@ const LoginState = ({ state, dispatch }) => {
if (!loginResponse.login) return
const stateVar =
loginResponse.login === 'INPUT2FA' ? STATES.INPUT_2FA : STATES.SETUP_2FA
return dispatch({
type: stateVar,
type: loginResponse.login,
payload: {
clientField: username,
passwordField: password,
@ -78,6 +101,42 @@ const LoginState = ({ state, dispatch }) => {
})
}
const [validateAssertion, { error: FIDOMutationError }] = useMutation(
VALIDATE_ASSERTION,
{
onCompleted: ({ validateAssertion: success }) => success && getUserData()
}
)
const [assertionOptions, { error: assertionQueryError }] = useLazyQuery(
GENERATE_ASSERTION,
{
onCompleted: ({ generateAssertionOptions: options }) => {
startAssertion(options)
.then(res => {
validateAssertion({
variables: {
assertionResponse: res
}
})
})
.catch(err => {
console.error(err)
})
}
}
)
const [getUserData, { error: userDataQueryError }] = useLazyQuery(
GET_USER_DATA,
{
onCompleted: ({ userData }) => {
setUserData(userData)
history.push('/')
}
}
)
return (
<Formik
validationSchema={validationSchema}
@ -95,7 +154,14 @@ const LoginState = ({ state, dispatch }) => {
fullWidth
autoFocus
className={classes.input}
error={getErrorMsg(errors, touched, mutationError)}
error={getErrorMsg(
errors,
touched,
loginMutationError ||
FIDOMutationError ||
assertionQueryError ||
userDataQueryError
)}
/>
<Field
name="password"
@ -103,7 +169,14 @@ const LoginState = ({ state, dispatch }) => {
component={SecretInput}
label="Password"
fullWidth
error={getErrorMsg(errors, touched, mutationError)}
error={getErrorMsg(
errors,
touched,
loginMutationError ||
FIDOMutationError ||
assertionQueryError ||
userDataQueryError
)}
/>
<div className={classes.rememberMeWrapper}>
<Field
@ -114,11 +187,41 @@ const LoginState = ({ state, dispatch }) => {
<Label3>Keep me logged in</Label3>
</div>
<div className={classes.footer}>
{getErrorMsg(errors, touched, mutationError) && (
{getErrorMsg(
errors,
touched,
loginMutationError ||
FIDOMutationError ||
assertionQueryError ||
userDataQueryError
) && (
<P className={classes.errorMessage}>
{getErrorMsg(errors, touched, mutationError)}
{getErrorMsg(
errors,
touched,
loginMutationError ||
FIDOMutationError ||
assertionQueryError ||
userDataQueryError
)}
</P>
)}
{strategy !== 'FIDO2FA' && (
<Button
type="button"
onClick={() => {
return strategy === 'FIDOUsernameless'
? assertionOptions()
: dispatch({
type: 'FIDO',
payload: {}
})
}}
buttonClassName={classes.loginButton}
className={classes.fidoLoginButtonWrapper}>
I have a hardware key
</Button>
)}
<Button
type="submit"
form="login-form"

View file

@ -40,6 +40,9 @@ const styles = {
twofaFooter: {
marginTop: '6vh'
},
fidoLoginButtonWrapper: {
marginBottom: 12
},
loginButton: {
display: 'block',
width: '100%'

View file

@ -1,7 +1,8 @@
const STATES = {
LOGIN: 'LOGIN',
SETUP_2FA: 'SETUP2FA',
INPUT_2FA: 'INPUT2FA'
INPUT_2FA: 'INPUT2FA',
FIDO: 'FIDO'
}
export { STATES }

View file

@ -1,5 +1,6 @@
import { useQuery } from '@apollo/react-hooks'
import { useQuery, useMutation, useLazyQuery } from '@apollo/react-hooks'
import { makeStyles, Box, Chip } from '@material-ui/core'
import { startAttestation } from '@simplewebauthn/browser'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useReducer, useState, useContext } from 'react'
@ -33,6 +34,24 @@ const GET_USERS = gql`
}
`
const GENERATE_ATTESTATION = gql`
query generateAttestationOptions($userID: ID!) {
generateAttestationOptions(userID: $userID)
}
`
const VALIDATE_ATTESTATION = gql`
mutation validateAttestation(
$userID: ID!
$attestationResponse: JSONObject!
) {
validateAttestation(
userID: $userID
attestationResponse: $attestationResponse
)
}
`
const initialState = {
showCreateUserModal: false,
showResetPasswordModal: false,
@ -67,6 +86,25 @@ const Users = () => {
const [userInfo, setUserInfo] = useState(null)
const [validateAttestation] = useMutation(VALIDATE_ATTESTATION, {
onCompleted: res => {
// TODO: show a brief popup to have UX feedback?
}
})
const [generateAttestationOptions] = useLazyQuery(GENERATE_ATTESTATION, {
onCompleted: ({ generateAttestationOptions: options }) => {
startAttestation(options).then(res => {
validateAttestation({
variables: {
userID: userInfo.id,
attestationResponse: res
}
})
})
}
})
const elements = [
{
header: 'Login',
@ -140,6 +178,19 @@ const Users = () => {
})
}}
/>
<Chip
size="small"
label="Add FIDO"
className={classes.actionChip}
onClick={() => {
setUserInfo(u)
generateAttestationOptions({
variables: {
userID: u.id
}
})
}}
/>
</>
)
}

211
package-lock.json generated
View file

@ -3388,6 +3388,65 @@
"@otplib/plugin-thirty-two": "^12.0.1"
}
},
"@peculiar/asn1-android": {
"version": "2.0.32",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.0.32.tgz",
"integrity": "sha512-R1ELsTpRkkcRZL5mnOPcXcoA+KxrJyVbZKYw6V83LE164HjnQ1wxOeIVsBWFIqJpgxzgosKvuvXd42g72/7xpA==",
"requires": {
"@peculiar/asn1-schema": "^2.0.32",
"asn1js": "^2.1.1",
"tslib": "^2.2.0"
},
"dependencies": {
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
}
}
},
"@peculiar/asn1-schema": {
"version": "2.0.32",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.0.32.tgz",
"integrity": "sha512-JzGUVxOFN+RKslJrGAxcq4l6tEmmLY1XuALHINVxc8BJsB4bXOdZzTvxbN9dCPk65Vbulno0B6DmImZ7I6SO8w==",
"requires": {
"@types/asn1js": "^2.0.0",
"asn1js": "^2.1.1",
"pvtsutils": "^1.1.2",
"tslib": "^2.2.0"
},
"dependencies": {
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
}
}
},
"@peculiar/asn1-x509": {
"version": "2.0.32",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.0.32.tgz",
"integrity": "sha512-8lbtm2nSWOE2M91kC2UAZOw3FnS6D0EROlT09/jhHVXvrtiLO58zhUG/6xHCe/0Not0Y5NGi++fsRv8pGxXc4Q==",
"requires": {
"@peculiar/asn1-schema": "^2.0.32",
"asn1js": "^2.1.1",
"ipaddr.js": "^2.0.0",
"pvtsutils": "^1.1.2",
"tslib": "^2.2.0"
},
"dependencies": {
"ipaddr.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.0.tgz",
"integrity": "sha512-S54H9mIj0rbxRIyrDMEuuER86LdlgUg9FSeZ8duQb6CUG2iRrA36MYVQBSprTF/ZeAwvyQ5mDGuNvIPM0BIl3w=="
},
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
}
}
},
"@phc/format": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
@ -3447,6 +3506,55 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"@simplewebauthn/browser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-3.0.0.tgz",
"integrity": "sha512-P661gZX/QW0Rg2NRAMtW84Q3u4nhXkPef9LLU4btLJFYoXO8RBFfxcmyqwyf2QEb4B7+lFdp5EWfZV5T7FvuHw=="
},
"@simplewebauthn/server": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-3.0.0.tgz",
"integrity": "sha512-ymGX2obBrhY9R3OxrpCYaNGAovFHmMlQrGoNdVOe2R2JUBXC1Rg5JEUl1lGyaRykN1SyZqLgz86wAjDVuRITTA==",
"requires": {
"@peculiar/asn1-android": "^2.0.26",
"@peculiar/asn1-schema": "^2.0.26",
"@peculiar/asn1-x509": "^2.0.26",
"@simplewebauthn/typescript-types": "^3.0.0",
"base64url": "^3.0.1",
"cbor": "^5.1.0",
"elliptic": "^6.5.3",
"jsrsasign": "^10.2.0",
"jwk-to-pem": "^2.0.4",
"node-fetch": "^2.6.0",
"node-rsa": "^1.1.1"
},
"dependencies": {
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
}
}
},
"@simplewebauthn/typescript-types": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@simplewebauthn/typescript-types/-/typescript-types-3.0.0.tgz",
"integrity": "sha512-bsk3EQWzPOZwP9C+ETVhcFDpZywY5sTqmNuGkNm3aNpc9Xh/mqZjy8nL0Sm7xwrlhY0zWAlOaIWQ3LvN5SoFhg=="
},
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@ -3684,6 +3792,11 @@
"@types/node": "*"
}
},
"@types/asn1js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/asn1js/-/asn1js-2.0.0.tgz",
"integrity": "sha512-Jjzp5EqU0hNpADctc/UqhiFbY1y2MqIxBVa2S4dBlbnZHTLPMuggoL5q43X63LpsOIINRDirBjP56DUUKIUWIA=="
},
"@types/babel__core": {
"version": "7.1.12",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz",
@ -5257,6 +5370,14 @@
"safer-buffer": "^2.1.0"
}
},
"asn1js": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.1.1.tgz",
"integrity": "sha512-t9u0dU0rJN4ML+uxgN6VM2Z4H5jWIYm0w8LsZLzMJaQsgL3IJNbxHgmbWDvJAwspyHpDFuzUaUFh4c05UB4+6g==",
"requires": {
"pvutils": "^1.0.17"
}
},
"assert": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
@ -6012,6 +6133,11 @@
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
@ -7196,6 +7322,22 @@
}
}
},
"cbor": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz",
"integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==",
"requires": {
"bignumber.js": "^9.0.1",
"nofilter": "^1.0.4"
},
"dependencies": {
"bignumber.js": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz",
"integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA=="
}
}
},
"ccxt": {
"version": "1.51.36",
"resolved": "https://registry.npmjs.org/ccxt/-/ccxt-1.51.36.tgz",
@ -14358,6 +14500,11 @@
"verror": "1.10.0"
}
},
"jsrsasign": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.2.0.tgz",
"integrity": "sha512-khMrV/10U02DRzmXhjuLQjddUF39GHndaJZ/3YiiKkbyEl1T5M6EQF9nQUq0DFVCHusmd/jl8TWl4mWt+1L5hg=="
},
"jsx-ast-utils": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz",
@ -14378,6 +14525,37 @@
"safe-buffer": "^5.0.1"
}
},
"jwk-to-pem": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
"integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
"requires": {
"asn1.js": "^5.3.0",
"elliptic": "^6.5.4",
"safe-buffer": "^5.0.1"
},
"dependencies": {
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
}
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
@ -15688,6 +15866,14 @@
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
"integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg=="
},
"node-rsa": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz",
"integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==",
"requires": {
"asn1": "^0.2.4"
}
},
"nodemon": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz",
@ -15729,6 +15915,11 @@
}
}
},
"nofilter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz",
"integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA=="
},
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
@ -17516,6 +17707,26 @@
"bitcoin-ops": "^1.3.0"
}
},
"pvtsutils": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.1.6.tgz",
"integrity": "sha512-Tm/74+LIqWtItcZHBJztPEPqLzNKbtPAA3LoFt763PFCHxmCfrF4YXhdFEiPAxMTakR0shbVymKKyMxg1Zqt4A==",
"requires": {
"tslib": "^2.2.0"
},
"dependencies": {
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
}
}
},
"pvutils": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz",
"integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ=="
},
"q": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/q/-/q-2.0.3.tgz",

View file

@ -6,11 +6,14 @@
"license": "Unlicense",
"author": "Lamassu (https://lamassu.is)",
"dependencies": {
"@simplewebauthn/browser": "^3.0.0",
"@simplewebauthn/server": "^3.0.0",
"apollo-server-express": "2.25.1",
"argon2": "0.28.2",
"axios": "0.21.1",
"base-64": "^1.0.0",
"base-x": "3.0.9",
"base64url": "^3.0.1",
"bchaddrjs": "^0.3.0",
"bignumber.js": "9.0.1",
"bip39": "^2.3.1",