Merge pull request #712 from chaotixkilla/feat-implement-fido-auth
Hardware credential authentication
This commit is contained in:
commit
540f1581f5
19 changed files with 1360 additions and 72 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
const { asyncLocalStorage, defaultStore } = require('../lib/async-storage')
|
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 options = require('../lib/options')
|
||||||
|
|
||||||
const name = process.argv[2]
|
const name = process.argv[2]
|
||||||
|
|
@ -31,7 +31,7 @@ if (role !== 'user' && role !== 'superuser') {
|
||||||
}
|
}
|
||||||
|
|
||||||
asyncLocalStorage.run(defaultStore(), () => {
|
asyncLocalStorage.run(defaultStore(), () => {
|
||||||
authentication.createRegisterToken(name, role).then(token => {
|
userManagement.createRegisterToken(name, role).then(token => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.log(`A user named ${name} already exists!`)
|
console.log(`A user named ${name} already exists!`)
|
||||||
process.exit(2)
|
process.exit(2)
|
||||||
|
|
|
||||||
36
lib/hardware-credentials.js
Normal file
36
lib/hardware-credentials.js
Normal 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
|
||||||
|
}
|
||||||
180
lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js
Normal file
180
lib/new-admin/graphql/modules/authentication/FIDO2FAStrategy.js
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
17
lib/new-admin/graphql/modules/authentication/index.js
Normal file
17
lib/new-admin/graphql/modules/authentication/index.js
Normal 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]
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const otplib = require('otplib')
|
const otplib = require('otplib')
|
||||||
const argon2 = require('argon2')
|
const argon2 = require('argon2')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
const constants = require('../../../constants')
|
const constants = require('../../../constants')
|
||||||
const authTokens = require('../../../auth-tokens')
|
const authTokens = require('../../../auth-tokens')
|
||||||
|
|
@ -8,6 +9,7 @@ const T = require('../../../time')
|
||||||
const users = require('../../../users')
|
const users = require('../../../users')
|
||||||
const sessionManager = require('../../../session-manager')
|
const sessionManager = require('../../../session-manager')
|
||||||
const authErrors = require('../errors/authentication')
|
const authErrors = require('../errors/authentication')
|
||||||
|
const credentials = require('../../../hardware-credentials')
|
||||||
|
|
||||||
const REMEMBER_ME_AGE = 90 * T.day
|
const REMEMBER_ME_AGE = 90 * T.day
|
||||||
|
|
||||||
|
|
@ -140,8 +142,12 @@ const deleteSession = (sessionID, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = (username, password) => {
|
const login = (username, password) => {
|
||||||
return authenticateUser(username, password).then(user => {
|
return authenticateUser(username, password)
|
||||||
const twoFASecret = user.twofa_code
|
.then(user => {
|
||||||
|
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user.twofa_code])
|
||||||
|
})
|
||||||
|
.then(([devices, twoFASecret]) => {
|
||||||
|
if (!_.isEmpty(devices)) return 'FIDO'
|
||||||
return twoFASecret ? 'INPUT2FA' : 'SETUP2FA'
|
return twoFASecret ? 'INPUT2FA' : 'SETUP2FA'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +240,7 @@ const reset2FA = (token, userID, code, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
authenticateUser,
|
||||||
getUserData,
|
getUserData,
|
||||||
get2FASecret,
|
get2FASecret,
|
||||||
confirm2FA,
|
confirm2FA,
|
||||||
|
|
@ -1,34 +1,91 @@
|
||||||
const authentication = require('../modules/authentication')
|
const authentication = require('../modules/authentication')
|
||||||
|
const userManagement = require('../modules/userManagement')
|
||||||
const users = require('../../../users')
|
const users = require('../../../users')
|
||||||
const sessionManager = require('../../../session-manager')
|
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 = {
|
const resolver = {
|
||||||
Query: {
|
Query: {
|
||||||
users: () => users.getUsers(),
|
users: () => users.getUsers(),
|
||||||
sessions: () => sessionManager.getSessions(),
|
sessions: () => sessionManager.getSessions(),
|
||||||
userSessions: (...[, { username }]) => sessionManager.getSessionsByUsername(username),
|
userSessions: (...[, { username }]) => sessionManager.getSessionsByUsername(username),
|
||||||
userData: (...[, {}, context]) => authentication.getUserData(context),
|
userData: (...[, {}, context]) => userManagement.getUserData(context),
|
||||||
get2FASecret: (...[, { username, password }]) => authentication.get2FASecret(username, password),
|
get2FASecret: (...[, { username, password }]) => userManagement.get2FASecret(username, password),
|
||||||
confirm2FA: (...[, { code }, context]) => authentication.confirm2FA(code, context),
|
confirm2FA: (...[, { code }, context]) => userManagement.confirm2FA(code, context),
|
||||||
validateRegisterLink: (...[, { token }]) => authentication.validateRegisterLink(token),
|
validateRegisterLink: (...[, { token }]) => userManagement.validateRegisterLink(token),
|
||||||
validateResetPasswordLink: (...[, { token }]) => authentication.validateResetPasswordLink(token),
|
validateResetPasswordLink: (...[, { token }]) => userManagement.validateResetPasswordLink(token),
|
||||||
validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(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: {
|
Mutation: {
|
||||||
enableUser: (...[, { confirmationCode, id }, context]) => authentication.enableUser(confirmationCode, id, context),
|
enableUser: (...[, { confirmationCode, id }, context]) => userManagement.enableUser(confirmationCode, id, context),
|
||||||
disableUser: (...[, { confirmationCode, id }, context]) => authentication.disableUser(confirmationCode, id, context),
|
disableUser: (...[, { confirmationCode, id }, context]) => userManagement.disableUser(confirmationCode, id, context),
|
||||||
deleteSession: (...[, { sid }, context]) => authentication.deleteSession(sid, context),
|
deleteSession: (...[, { sid }, context]) => userManagement.deleteSession(sid, context),
|
||||||
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
|
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
|
||||||
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => authentication.changeUserRole(confirmationCode, id, newRole, context),
|
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => userManagement.changeUserRole(confirmationCode, id, newRole, context),
|
||||||
login: (...[, { username, password }]) => authentication.login(username, password),
|
login: (...[, { username, password }]) => userManagement.login(username, password),
|
||||||
input2FA: (...[, { username, password, rememberMe, code }, context]) => authentication.input2FA(username, password, rememberMe, code, context),
|
input2FA: (...[, { username, password, rememberMe, code }, context]) => userManagement.input2FA(username, password, rememberMe, code, context),
|
||||||
setup2FA: (...[, { username, password, rememberMe, codeConfirmation }, context]) => authentication.setup2FA(username, password, rememberMe, codeConfirmation, context),
|
setup2FA: (...[, { username, password, rememberMe, codeConfirmation }, context]) => userManagement.setup2FA(username, password, rememberMe, codeConfirmation, context),
|
||||||
createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => authentication.createResetPasswordToken(confirmationCode, userID, context),
|
createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => userManagement.createResetPasswordToken(confirmationCode, userID, context),
|
||||||
createReset2FAToken: (...[, { confirmationCode, userID }, context]) => authentication.createReset2FAToken(confirmationCode, userID, context),
|
createReset2FAToken: (...[, { confirmationCode, userID }, context]) => userManagement.createReset2FAToken(confirmationCode, userID, context),
|
||||||
createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role),
|
createRegisterToken: (...[, { username, role }]) => userManagement.createRegisterToken(username, role),
|
||||||
register: (...[, { token, username, password, role }]) => authentication.register(token, username, password, role),
|
register: (...[, { token, username, password, role }]) => userManagement.register(token, username, password, role),
|
||||||
resetPassword: (...[, { token, userID, newPassword }, context]) => authentication.resetPassword(token, userID, newPassword, context),
|
resetPassword: (...[, { token, userID, newPassword }, context]) => userManagement.resetPassword(token, userID, newPassword, context),
|
||||||
reset2FA: (...[, { token, userID, code }, context]) => authentication.reset2FA(token, userID, code, 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = `
|
const typeDef = `
|
||||||
directive @auth(
|
directive @auth(
|
||||||
requires: [Role] = [USER, SUPERUSER]
|
requires: [Role] = [USER, SUPERUSER]
|
||||||
|
|
@ -54,6 +88,7 @@ const typeDef = `
|
||||||
validateRegisterLink(token: String!): User
|
validateRegisterLink(token: String!): User
|
||||||
validateResetPasswordLink(token: String!): User
|
validateResetPasswordLink(token: String!): User
|
||||||
validateReset2FALink(token: String!): TwoFactorSecret
|
validateReset2FALink(token: String!): TwoFactorSecret
|
||||||
|
${getFIDOStrategyQueryTypes()}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
|
@ -72,6 +107,7 @@ const typeDef = `
|
||||||
register(token: String!, username: String!, password: String!, role: String!): Boolean
|
register(token: String!, username: String!, password: String!, role: String!): Boolean
|
||||||
resetPassword(token: String!, userID: ID!, newPassword: String!): Boolean
|
resetPassword(token: String!, userID: ID!, newPassword: String!): Boolean
|
||||||
reset2FA(token: String!, userID: ID!, code: String!): Boolean
|
reset2FA(token: String!, userID: ID!, code: String!): Boolean
|
||||||
|
${getFIDOStrategyMutationsTypes()}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
20
migrations/1620335170327-hardware-credentials.js
Normal file
20
migrations/1620335170327-hardware-credentials.js
Normal 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()
|
||||||
|
}
|
||||||
31
new-lamassu-admin/package-lock.json
generated
31
new-lamassu-admin/package-lock.json
generated
|
|
@ -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": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
|
|
@ -17232,14 +17213,12 @@
|
||||||
"from": "git+https://github.com/lamassu/lamassu-coins.git",
|
"from": "git+https://github.com/lamassu/lamassu-coins.git",
|
||||||
"requires": {
|
"requires": {
|
||||||
"bech32": "2.0.0",
|
"bech32": "2.0.0",
|
||||||
"big-integer": "^1.6.48",
|
|
||||||
"bignumber.js": "^9.0.0",
|
"bignumber.js": "^9.0.0",
|
||||||
"bitcoinjs-lib": "4.0.3",
|
"bitcoinjs-lib": "4.0.3",
|
||||||
"bs58check": "^2.0.2",
|
"bs58check": "^2.0.2",
|
||||||
"cashaddrjs": "~0.2.8",
|
"cashaddrjs": "~0.2.8",
|
||||||
"crypto-js": "^3.1.9-1",
|
"crypto-js": "^3.1.9-1",
|
||||||
"ethereumjs-icap": "^0.3.1",
|
"ethereumjs-icap": "^0.3.1",
|
||||||
"keccak256": "^1.0.2",
|
|
||||||
"lodash": "^4.17.10"
|
"lodash": "^4.17.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"node-dir": {
|
||||||
"version": "0.1.17",
|
"version": "0.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz",
|
||||||
|
|
@ -18512,11 +18486,6 @@
|
||||||
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
|
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
|
||||||
"dev": true
|
"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": {
|
"node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
|
|
|
||||||
215
new-lamassu-admin/src/pages/Authentication/InputFIDOState.js
Normal file
215
new-lamassu-admin/src/pages/Authentication/InputFIDOState.js
Normal 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
|
||||||
|
|
@ -6,11 +6,15 @@ import { H5 } from 'src/components/typography'
|
||||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||||
|
|
||||||
import Input2FAState from './Input2FAState'
|
import Input2FAState from './Input2FAState'
|
||||||
|
import InputFIDOState from './InputFIDOState'
|
||||||
import LoginState from './LoginState'
|
import LoginState from './LoginState'
|
||||||
import Setup2FAState from './Setup2FAState'
|
import Setup2FAState from './Setup2FAState'
|
||||||
import styles from './shared.styles'
|
import styles from './shared.styles'
|
||||||
import { STATES } from './states'
|
import { STATES } from './states'
|
||||||
|
|
||||||
|
// FIDO2FA, FIDOPasswordless or FIDOUsernameless
|
||||||
|
const AUTHENTICATION_STRATEGY = 'FIDO2FA'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
|
@ -34,11 +38,21 @@ const LoginCard = () => {
|
||||||
const renderState = () => {
|
const renderState = () => {
|
||||||
switch (state.loginState) {
|
switch (state.loginState) {
|
||||||
case STATES.LOGIN:
|
case STATES.LOGIN:
|
||||||
return <LoginState state={state} dispatch={dispatch} />
|
return (
|
||||||
|
<LoginState
|
||||||
|
state={state}
|
||||||
|
dispatch={dispatch}
|
||||||
|
strategy={AUTHENTICATION_STRATEGY}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case STATES.INPUT_2FA:
|
case STATES.INPUT_2FA:
|
||||||
return <Input2FAState state={state} dispatch={dispatch} />
|
return <Input2FAState state={state} dispatch={dispatch} />
|
||||||
case STATES.SETUP_2FA:
|
case STATES.SETUP_2FA:
|
||||||
return <Setup2FAState state={state} dispatch={dispatch} />
|
return <Setup2FAState state={state} dispatch={dispatch} />
|
||||||
|
case STATES.FIDO:
|
||||||
|
return (
|
||||||
|
<InputFIDOState state={state} strategy={AUTHENTICATION_STRATEGY} />
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import { startAssertion } from '@simplewebauthn/browser'
|
||||||
import base64 from 'base-64'
|
import base64 from 'base-64'
|
||||||
import { Field, Form, Formik } from 'formik'
|
import { Field, Form, Formik } from 'formik'
|
||||||
import gql from 'graphql-tag'
|
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 * as Yup from 'yup'
|
||||||
|
|
||||||
|
import AppContext from 'src/AppContext'
|
||||||
import { Button } from 'src/components/buttons'
|
import { Button } from 'src/components/buttons'
|
||||||
import { Checkbox, SecretInput, TextInput } from 'src/components/inputs/formik'
|
import { Checkbox, SecretInput, TextInput } from 'src/components/inputs/formik'
|
||||||
import { Label3, P } from 'src/components/typography'
|
import { Label3, P } from 'src/components/typography'
|
||||||
|
|
||||||
import styles from './shared.styles'
|
import styles from './shared.styles'
|
||||||
import { STATES } from './states'
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
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({
|
const validationSchema = Yup.object().shape({
|
||||||
client: Yup.string()
|
client: Yup.string()
|
||||||
.required('Client field is required!')
|
.required('Client field is required!')
|
||||||
|
|
@ -44,10 +68,12 @@ const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoginState = ({ state, dispatch }) => {
|
const LoginState = ({ state, dispatch, strategy }) => {
|
||||||
const classes = useStyles()
|
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 submitLogin = async (username, password, rememberMe) => {
|
||||||
const options = {
|
const options = {
|
||||||
|
|
@ -65,11 +91,8 @@ const LoginState = ({ state, dispatch }) => {
|
||||||
|
|
||||||
if (!loginResponse.login) return
|
if (!loginResponse.login) return
|
||||||
|
|
||||||
const stateVar =
|
|
||||||
loginResponse.login === 'INPUT2FA' ? STATES.INPUT_2FA : STATES.SETUP_2FA
|
|
||||||
|
|
||||||
return dispatch({
|
return dispatch({
|
||||||
type: stateVar,
|
type: loginResponse.login,
|
||||||
payload: {
|
payload: {
|
||||||
clientField: username,
|
clientField: username,
|
||||||
passwordField: password,
|
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 (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
|
|
@ -95,7 +154,14 @@ const LoginState = ({ state, dispatch }) => {
|
||||||
fullWidth
|
fullWidth
|
||||||
autoFocus
|
autoFocus
|
||||||
className={classes.input}
|
className={classes.input}
|
||||||
error={getErrorMsg(errors, touched, mutationError)}
|
error={getErrorMsg(
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
loginMutationError ||
|
||||||
|
FIDOMutationError ||
|
||||||
|
assertionQueryError ||
|
||||||
|
userDataQueryError
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
name="password"
|
name="password"
|
||||||
|
|
@ -103,7 +169,14 @@ const LoginState = ({ state, dispatch }) => {
|
||||||
component={SecretInput}
|
component={SecretInput}
|
||||||
label="Password"
|
label="Password"
|
||||||
fullWidth
|
fullWidth
|
||||||
error={getErrorMsg(errors, touched, mutationError)}
|
error={getErrorMsg(
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
loginMutationError ||
|
||||||
|
FIDOMutationError ||
|
||||||
|
assertionQueryError ||
|
||||||
|
userDataQueryError
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div className={classes.rememberMeWrapper}>
|
<div className={classes.rememberMeWrapper}>
|
||||||
<Field
|
<Field
|
||||||
|
|
@ -114,11 +187,41 @@ const LoginState = ({ state, dispatch }) => {
|
||||||
<Label3>Keep me logged in</Label3>
|
<Label3>Keep me logged in</Label3>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.footer}>
|
<div className={classes.footer}>
|
||||||
{getErrorMsg(errors, touched, mutationError) && (
|
{getErrorMsg(
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
loginMutationError ||
|
||||||
|
FIDOMutationError ||
|
||||||
|
assertionQueryError ||
|
||||||
|
userDataQueryError
|
||||||
|
) && (
|
||||||
<P className={classes.errorMessage}>
|
<P className={classes.errorMessage}>
|
||||||
{getErrorMsg(errors, touched, mutationError)}
|
{getErrorMsg(
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
loginMutationError ||
|
||||||
|
FIDOMutationError ||
|
||||||
|
assertionQueryError ||
|
||||||
|
userDataQueryError
|
||||||
|
)}
|
||||||
</P>
|
</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
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="login-form"
|
form="login-form"
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ const styles = {
|
||||||
twofaFooter: {
|
twofaFooter: {
|
||||||
marginTop: '6vh'
|
marginTop: '6vh'
|
||||||
},
|
},
|
||||||
|
fidoLoginButtonWrapper: {
|
||||||
|
marginBottom: 12
|
||||||
|
},
|
||||||
loginButton: {
|
loginButton: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
width: '100%'
|
width: '100%'
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
const STATES = {
|
const STATES = {
|
||||||
LOGIN: 'LOGIN',
|
LOGIN: 'LOGIN',
|
||||||
SETUP_2FA: 'SETUP2FA',
|
SETUP_2FA: 'SETUP2FA',
|
||||||
INPUT_2FA: 'INPUT2FA'
|
INPUT_2FA: 'INPUT2FA',
|
||||||
|
FIDO: 'FIDO'
|
||||||
}
|
}
|
||||||
|
|
||||||
export { STATES }
|
export { STATES }
|
||||||
|
|
|
||||||
|
|
@ -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 { makeStyles, Box, Chip } from '@material-ui/core'
|
||||||
|
import { startAttestation } from '@simplewebauthn/browser'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useReducer, useState, useContext } from 'react'
|
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 = {
|
const initialState = {
|
||||||
showCreateUserModal: false,
|
showCreateUserModal: false,
|
||||||
showResetPasswordModal: false,
|
showResetPasswordModal: false,
|
||||||
|
|
@ -67,6 +86,25 @@ const Users = () => {
|
||||||
|
|
||||||
const [userInfo, setUserInfo] = useState(null)
|
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 = [
|
const elements = [
|
||||||
{
|
{
|
||||||
header: 'Login',
|
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
211
package-lock.json
generated
|
|
@ -3388,6 +3388,65 @@
|
||||||
"@otplib/plugin-thirty-two": "^12.0.1"
|
"@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": {
|
"@phc/format": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||||
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
|
"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": {
|
"@sindresorhus/is": {
|
||||||
"version": "0.14.0",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||||
|
|
@ -3684,6 +3792,11 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"@types/babel__core": {
|
||||||
"version": "7.1.12",
|
"version": "7.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz",
|
||||||
|
|
@ -5257,6 +5370,14 @@
|
||||||
"safer-buffer": "^2.1.0"
|
"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": {
|
"assert": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
|
"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": {
|
"basic-auth": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
"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": {
|
"ccxt": {
|
||||||
"version": "1.51.36",
|
"version": "1.51.36",
|
||||||
"resolved": "https://registry.npmjs.org/ccxt/-/ccxt-1.51.36.tgz",
|
"resolved": "https://registry.npmjs.org/ccxt/-/ccxt-1.51.36.tgz",
|
||||||
|
|
@ -14358,6 +14500,11 @@
|
||||||
"verror": "1.10.0"
|
"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": {
|
"jsx-ast-utils": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz",
|
||||||
|
|
@ -14378,6 +14525,37 @@
|
||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"jws": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
|
||||||
"integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg=="
|
"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": {
|
"nodemon": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz",
|
"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": {
|
"nopt": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
|
||||||
|
|
@ -17516,6 +17707,26 @@
|
||||||
"bitcoin-ops": "^1.3.0"
|
"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": {
|
"q": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/q/-/q-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/q/-/q-2.0.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,14 @@
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"author": "Lamassu (https://lamassu.is)",
|
"author": "Lamassu (https://lamassu.is)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@simplewebauthn/browser": "^3.0.0",
|
||||||
|
"@simplewebauthn/server": "^3.0.0",
|
||||||
"apollo-server-express": "2.25.1",
|
"apollo-server-express": "2.25.1",
|
||||||
"argon2": "0.28.2",
|
"argon2": "0.28.2",
|
||||||
"axios": "0.21.1",
|
"axios": "0.21.1",
|
||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
"base-x": "3.0.9",
|
"base-x": "3.0.9",
|
||||||
|
"base64url": "^3.0.1",
|
||||||
"bchaddrjs": "^0.3.0",
|
"bchaddrjs": "^0.3.0",
|
||||||
"bignumber.js": "9.0.1",
|
"bignumber.js": "9.0.1",
|
||||||
"bip39": "^2.3.1",
|
"bip39": "^2.3.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue