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
|
||||
|
||||
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)
|
||||
|
|
|
|||
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 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,
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
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": {
|
||||
"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",
|
||||
|
|
|
|||
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 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ const styles = {
|
|||
twofaFooter: {
|
||||
marginTop: '6vh'
|
||||
},
|
||||
fidoLoginButtonWrapper: {
|
||||
marginBottom: 12
|
||||
},
|
||||
loginButton: {
|
||||
display: 'block',
|
||||
width: '100%'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
const STATES = {
|
||||
LOGIN: 'LOGIN',
|
||||
SETUP_2FA: 'SETUP2FA',
|
||||
INPUT_2FA: 'INPUT2FA'
|
||||
INPUT_2FA: 'INPUT2FA',
|
||||
FIDO: 'FIDO'
|
||||
}
|
||||
|
||||
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 { 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
211
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue