|
|
|
@ -3,8 +3,13 @@ import { generateError, generateSuccess } from '../../helpers/graph.mjs'
|
|
|
|
|
import jwt from 'jsonwebtoken'
|
|
|
|
|
import ms from 'ms'
|
|
|
|
|
import { DateTime } from 'luxon'
|
|
|
|
|
import { v4 as uuid } from 'uuid'
|
|
|
|
|
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server'
|
|
|
|
|
import base64 from '@hexagon/base64'
|
|
|
|
|
import {
|
|
|
|
|
generateRegistrationOptions,
|
|
|
|
|
verifyRegistrationResponse,
|
|
|
|
|
generateAuthenticationOptions,
|
|
|
|
|
verifyAuthenticationResponse
|
|
|
|
|
} from '@simplewebauthn/server'
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
Query: {
|
|
|
|
@ -309,7 +314,7 @@ export default {
|
|
|
|
|
}
|
|
|
|
|
usr.passkeys.authenticators.push({
|
|
|
|
|
...verification.registrationInfo,
|
|
|
|
|
id: uuid(),
|
|
|
|
|
id: base64.fromArrayBuffer(verification.registrationInfo.credentialID, true),
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
name: args.name,
|
|
|
|
|
siteId: usr.passkeys.reg.siteId,
|
|
|
|
@ -364,6 +369,117 @@ export default {
|
|
|
|
|
return generateError(err)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
/**
|
|
|
|
|
* Login via passkey - Generate challenge
|
|
|
|
|
*/
|
|
|
|
|
async authenticatePasskeyGenerate (obj, args, context) {
|
|
|
|
|
try {
|
|
|
|
|
const site = WIKI.sites[args.siteId]
|
|
|
|
|
if (!site) {
|
|
|
|
|
throw new Error('ERR_INVALID_SITE')
|
|
|
|
|
} else if (site.hostname === '*') {
|
|
|
|
|
WIKI.logger.warn('Cannot use passkeys with a wildcard site hostname. Enter a valid hostname under the Administration Area > General.')
|
|
|
|
|
throw new Error('ERR_PK_HOSTNAME_MISSING')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const usr = await WIKI.db.users.query().findOne({ email: args.email })
|
|
|
|
|
if (!usr || !usr.passkeys?.authenticators) {
|
|
|
|
|
// Fake success response to prevent email leaking
|
|
|
|
|
WIKI.logger.debug(`Cannot generate passkey challenge for ${args.email}... (non-existing or missing passkeys setup)`)
|
|
|
|
|
return {
|
|
|
|
|
operation: generateSuccess('Passkey challenge generated.'),
|
|
|
|
|
authOptions: await generateAuthenticationOptions({
|
|
|
|
|
allowCredentials: [{
|
|
|
|
|
id: new Uint8Array(Array(30).map(v => _.random(0, 254))),
|
|
|
|
|
type: 'public-key',
|
|
|
|
|
transports: ['internal']
|
|
|
|
|
}],
|
|
|
|
|
userVerification: 'preferred',
|
|
|
|
|
rpId: site.hostname
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const options = await generateAuthenticationOptions({
|
|
|
|
|
allowCredentials: usr.passkeys.authenticators.map(authenticator => ({
|
|
|
|
|
id: new Uint8Array(authenticator.credentialID),
|
|
|
|
|
type: 'public-key',
|
|
|
|
|
transports: authenticator.transports
|
|
|
|
|
})),
|
|
|
|
|
userVerification: 'preferred',
|
|
|
|
|
rpId: site.hostname
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
usr.passkeys.login = {
|
|
|
|
|
challenge: options.challenge,
|
|
|
|
|
rpId: site.hostname,
|
|
|
|
|
siteId: site.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await usr.$query().patch({
|
|
|
|
|
passkeys: usr.passkeys
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
operation: generateSuccess('Passkey challenge generated.'),
|
|
|
|
|
authOptions: options
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return generateError(err)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
/**
|
|
|
|
|
* Login via passkey - Verify challenge
|
|
|
|
|
*/
|
|
|
|
|
async authenticatePasskeyVerify (obj, args, context) {
|
|
|
|
|
try {
|
|
|
|
|
if (!args.authResponse?.response?.userHandle) {
|
|
|
|
|
throw new Error('ERR_INVALID_PASSKEY_RESPONSE')
|
|
|
|
|
}
|
|
|
|
|
const usr = await WIKI.db.users.query().findById(args.authResponse.response.userHandle)
|
|
|
|
|
if (!usr) {
|
|
|
|
|
WIKI.logger.debug(`Passkey Login Failure: Cannot find user ${args.authResponse.response.userHandle}`)
|
|
|
|
|
throw new Error('ERR_LOGIN_FAILED')
|
|
|
|
|
} else if (!usr.passkeys?.login) {
|
|
|
|
|
WIKI.logger.debug(`Passkey Login Failure: Missing login auth generation step for user ${args.authResponse.response.userHandle}`)
|
|
|
|
|
throw new Error('ERR_LOGIN_FAILED')
|
|
|
|
|
} else if (!usr.passkeys.authenticators?.some(a => a.id === args.authResponse.id)) {
|
|
|
|
|
WIKI.logger.debug(`Passkey Login Failure: Authenticator provided is not registered for user ${args.authResponse.response.userHandle}`)
|
|
|
|
|
throw new Error('ERR_LOGIN_FAILED')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const verification = await verifyAuthenticationResponse({
|
|
|
|
|
response: args.authResponse,
|
|
|
|
|
expectedChallenge: usr.passkeys.login.challenge,
|
|
|
|
|
expectedOrigin: `https://${usr.passkeys.login.rpId}`,
|
|
|
|
|
expectedRPID: usr.passkeys.login.rpId,
|
|
|
|
|
requireUserVerification: true,
|
|
|
|
|
authenticator: _.find(usr.passkeys.authenticators, ['id', args.authResponse.id])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!verification.verified) {
|
|
|
|
|
WIKI.logger.debug(`Passkey Login Failure: Challenge verification failed for user ${args.authResponse.response.userHandle}`)
|
|
|
|
|
throw new Error('ERR_LOGIN_FAILED')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete usr.passkeys.login
|
|
|
|
|
|
|
|
|
|
await usr.$query().patch({
|
|
|
|
|
passkeys: usr.passkeys
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const jwtToken = await WIKI.db.users.refreshToken(usr)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
operation: generateSuccess('Passkey challenge accepted.'),
|
|
|
|
|
nextAction: 'redirect',
|
|
|
|
|
jwt: jwtToken.token,
|
|
|
|
|
redirect: '/'
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return generateError(err)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
/**
|
|
|
|
|
* Perform Password Change
|
|
|
|
|
*/
|
|
|
|
|