123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741 |
- import _ from 'lodash-es'
- import { generateError, generateSuccess } from '../../helpers/graph.mjs'
- import jwt from 'jsonwebtoken'
- import ms from 'ms'
- import { DateTime } from 'luxon'
- import { base64 } from '@hexagon/base64'
- import {
- generateRegistrationOptions,
- verifyRegistrationResponse,
- generateAuthenticationOptions,
- verifyAuthenticationResponse
- } from '@simplewebauthn/server'
- import { isoBase64URL } from '@simplewebauthn/server/helpers'
- export default {
- Query: {
- /**
- * List of API Keys
- */
- async apiKeys (obj, args, context) {
- if (!WIKI.auth.checkAccess(context.req.user, ['read:api', 'manage:api'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- const keys = await WIKI.db.apiKeys.query().orderBy(['isRevoked', 'name'])
- return keys.map(k => ({
- id: k.id,
- name: k.name,
- keyShort: '...' + k.key.substring(k.key.length - 20),
- isRevoked: k.isRevoked,
- expiration: k.expiration,
- createdAt: k.createdAt,
- updatedAt: k.updatedAt
- }))
- },
- /**
- * Current API State
- */
- apiState (obj, args, context) {
- if (!WIKI.auth.checkAccess(context.req.user, ['read:api', 'manage:api', 'read:dashboard'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- return WIKI.config.api.isEnabled
- },
- /**
- * Fetch authentication strategies
- */
- async authStrategies (obj, args, context) {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- return WIKI.data.authentication.map(stg => ({
- ...stg,
- isAvailable: stg.isAvailable === true
- }))
- },
- /**
- * Fetch active authentication strategies
- */
- async authActiveStrategies (obj, args, context) {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- const strategies = await WIKI.db.authentication.getStrategies({ enabledOnly: args.enabledOnly })
- return strategies.map(a => {
- const str = _.find(WIKI.data.authentication, ['key', a.module]) || {}
- return {
- ...a,
- config: _.transform(str.props, (r, v, k) => {
- r[k] = v.sensitive ? '********' : a.config[k]
- }, {})
- }
- })
- },
- /**
- * Fetch site authentication strategies
- */
- async authSiteStrategies (obj, args, context, info) {
- const site = await WIKI.db.sites.query().findById(args.siteId)
- const activeStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
- const siteStrategies = _.sortBy(activeStrategies.map(str => {
- const siteAuth = _.find(site.config.authStrategies, ['id', str.id]) || {}
- return {
- id: str.id,
- activeStrategy: str,
- order: siteAuth.order ?? 0,
- isVisible: siteAuth.isVisible ?? false
- }
- }), ['order'])
- return args.visibleOnly ? siteStrategies.filter(s => s.isVisible) : siteStrategies
- }
- },
- Mutation: {
- /**
- * Create New API Key
- */
- async createApiKey (obj, args, context) {
- try {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- const key = await WIKI.db.apiKeys.createNewKey(args)
- await WIKI.auth.reloadApiKeys()
- WIKI.events.outbound.emit('reloadApiKeys')
- return {
- key,
- operation: generateSuccess('API Key created successfully')
- }
- } catch (err) {
- WIKI.logger.warn(err)
- return generateError(err)
- }
- },
- /**
- * Perform Login
- */
- async login (obj, args, context) {
- try {
- const authResult = await WIKI.db.users.login(args, context)
- return {
- ...authResult,
- operation: generateSuccess('Login success')
- }
- } catch (err) {
- // LDAP Debug Flag
- if (args.strategy === 'ldap' && WIKI.config.flags.ldapdebug) {
- WIKI.logger.warn('LDAP LOGIN ERROR (c1): ', err)
- }
- WIKI.logger.debug(err)
- return generateError(err)
- }
- },
- /**
- * Perform 2FA Login
- */
- async loginTFA (obj, args, context) {
- try {
- const authResult = await WIKI.db.users.loginTFA(args, context)
- return {
- ...authResult,
- operation: generateSuccess('TFA success')
- }
- } catch (err) {
- WIKI.logger.debug(err)
- return generateError(err)
- }
- },
- /**
- * Setup TFA
- */
- async setupTFA (obj, args, context) {
- try {
- const userId = context.req.user?.id
- if (!userId) {
- throw new Error('ERR_NOT_AUTHENTICATED')
- }
- const usr = await WIKI.db.users.query().findById(userId)
- if (!usr) {
- throw new Error('ERR_INVALID_USER')
- }
- const str = WIKI.auth.strategies[args.strategyId]
- if (!str) {
- throw new Error('ERR_INVALID_STRATEGY')
- }
- if (!usr.auth[args.strategyId]) {
- throw new Error('ERR_INVALID_STRATEGY')
- }
- if (usr.auth[args.strategyId].tfaIsActive) {
- throw new Error('ERR_TFA_ALREADY_ACTIVE')
- }
- const tfaQRImage = await usr.generateTFA(args.strategyId, args.siteId)
- const tfaToken = await WIKI.db.userKeys.generateToken({
- kind: 'tfaSetup',
- userId: usr.id,
- meta: {
- strategyId: args.strategyId
- }
- })
- return {
- operation: generateSuccess('TFA setup started'),
- continuationToken: tfaToken,
- tfaQRImage
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Deactivate 2FA
- */
- async deactivateTFA (obj, args, context) {
- try {
- const userId = context.req.user?.id
- if (!userId) {
- throw new Error('ERR_NOT_AUTHENTICATED')
- }
- const usr = await WIKI.db.users.query().findById(userId)
- if (!usr) {
- throw new Error('ERR_INVALID_USER')
- }
- const str = WIKI.auth.strategies[args.strategyId]
- if (!str) {
- throw new Error('ERR_INVALID_STRATEGY')
- }
- if (!usr.auth[args.strategyId]) {
- throw new Error('ERR_INVALID_STRATEGY')
- }
- if (!usr.auth[args.strategyId].tfaIsActive) {
- throw new Error('ERR_TFA_NOT_ACTIVE')
- }
- usr.auth[args.strategyId].tfaIsActive = false
- usr.auth[args.strategyId].tfaSecret = null
- await usr.$query().patch({
- auth: usr.auth
- })
- return {
- operation: generateSuccess('TFA deactivated successfully.')
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Setup Passkey
- */
- async setupPasskey (obj, args, context) {
- try {
- const userId = context.req.user?.id
- if (!userId) {
- throw new Error('ERR_NOT_AUTHENTICATED')
- }
- const usr = await WIKI.db.users.query().findById(userId)
- if (!usr) {
- throw new Error('ERR_INVALID_USER')
- }
- 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 options = await generateRegistrationOptions({
- rpName: site.config.title,
- rpId: site.hostname,
- userID: usr.id,
- userName: usr.email,
- userDisplayName: usr.name,
- attestationType: 'none',
- authenticatorSelection: {
- residentKey: 'required',
- userVerification: 'preferred'
- },
- excludeCredentials: usr.passkeys.authenticators?.map(authenticator => ({
- id: isoBase64URL.fromBuffer(new Uint8Array(authenticator.credentialID)),
- type: 'public-key',
- transports: authenticator.transports
- })) ?? []
- })
- usr.passkeys.reg = {
- challenge: options.challenge,
- rpId: site.hostname,
- siteId: site.id
- }
- await usr.$query().patch({
- passkeys: usr.passkeys
- })
- return {
- operation: generateSuccess('Passkey registration options generated successfully.'),
- registrationOptions: options
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Finalize Passkey Registration
- */
- async finalizePasskey (obj, args, context) {
- try {
- const userId = context.req.user?.id
- if (!userId) {
- throw new Error('ERR_NOT_AUTHENTICATED')
- }
- const usr = await WIKI.db.users.query().findById(userId)
- if (!usr) {
- throw new Error('ERR_INVALID_USER')
- } else if (!usr.passkeys?.reg) {
- throw new Error('ERR_PASSKEY_NOT_SETUP')
- }
- if (!args.name || args.name.trim().length < 1 || args.name.length > 255) {
- throw new Error('ERR_PK_NAME_MISSING_OR_INVALID')
- }
- const verification = await verifyRegistrationResponse({
- response: args.registrationResponse,
- expectedChallenge: usr.passkeys.reg.challenge,
- expectedOrigin: `https://${usr.passkeys.reg.rpId}`,
- expectedRPID: usr.passkeys.reg.rpId,
- requireUserVerification: true
- })
- if (!verification.verified) {
- throw new Error('ERR_PK_VERIFICATION_FAILED')
- }
- if (!usr.passkeys.authenticators) {
- usr.passkeys.authenticators = []
- }
- usr.passkeys.authenticators.push({
- ...verification.registrationInfo,
- id: base64.fromArrayBuffer(verification.registrationInfo.credentialID, true),
- createdAt: new Date(),
- name: args.name,
- siteId: usr.passkeys.reg.siteId,
- transports: args.registrationResponse.response.transports
- })
- delete usr.passkeys.reg
- await usr.$query().patch({
- passkeys: JSON.stringify(usr.passkeys, (k, v) => {
- if (v instanceof Uint8Array) {
- return Array.apply([], v)
- }
- return v
- })
- })
- return {
- operation: generateSuccess('Passkey registered successfully.')
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Deactivate a passkey
- */
- async deactivatePasskey (obj, args, context) {
- try {
- const userId = context.req.user?.id
- if (!userId) {
- throw new Error('ERR_NOT_AUTHENTICATED')
- }
- const usr = await WIKI.db.users.query().findById(userId)
- if (!usr) {
- throw new Error('ERR_INVALID_USER')
- } else if (!usr.passkeys?.authenticators) {
- throw new Error('ERR_PASSKEY_NOT_SETUP')
- }
- usr.passkeys.authenticators = usr.passkeys.authenticators.filter(a => a.id !== args.id)
- await usr.$query().patch({
- passkeys: usr.passkeys
- })
- return {
- operation: generateSuccess('Passkey deactivated successfully.')
- }
- } catch (err) {
- 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
- */
- async changePassword (obj, args, context) {
- try {
- if (args.continuationToken) {
- const authResult = await WIKI.db.users.loginChangePassword(args, context)
- return {
- ...authResult,
- operation: generateSuccess('Password set successfully')
- }
- } else {
- await WIKI.db.users.changePassword(args, context)
- return {
- operation: generateSuccess('Password changed successfully')
- }
- }
- } catch (err) {
- WIKI.logger.debug(err)
- return generateError(err)
- }
- },
- /**
- * Perform Forget Password
- */
- async forgotPassword (obj, args, context) {
- try {
- await WIKI.db.users.loginForgotPassword(args, context)
- return {
- operation: generateSuccess('Password reset request processed.')
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Register a new account
- */
- async register (obj, args, context) {
- try {
- const usr = await WIKI.db.users.createNewUser({ ...args, userInitiated: true })
- const authResult = await WIKI.db.users.afterLoginChecks(usr, WIKI.data.systemIds.localAuthId, context)
- return {
- ...authResult,
- operation: generateSuccess('Registration success')
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Refresh Token
- */
- async refreshToken (obj, args, context) {
- try {
- let decoded = {}
- if (!args.token) {
- throw new Error('ERR_MISSING_TOKEN')
- }
- try {
- decoded = jwt.verify(args.token, WIKI.config.auth.certs.public, {
- audience: WIKI.config.auth.audience,
- issuer: 'urn:wiki.js',
- algorithms: ['RS256'],
- ignoreExpiration: true
- })
- } catch (err) {
- throw new Error('ERR_INVALID_TOKEN')
- }
- if (DateTime.utc().minus(ms(WIKI.config.auth.tokenRenewal)) > DateTime.fromSeconds(decoded.exp)) {
- throw new Error('ERR_EXPIRED_TOKEN')
- }
- const newToken = await WIKI.db.users.refreshToken(decoded.id)
- return {
- jwt: newToken.token,
- operation: generateSuccess('Token refreshed successfully')
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Set API state
- */
- async setApiState (obj, args, context) {
- try {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- WIKI.config.api.isEnabled = args.enabled
- await WIKI.configSvc.saveToDb(['api'])
- return {
- operation: generateSuccess('API State changed successfully')
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Revoke an API key
- */
- async revokeApiKey (obj, args, context) {
- try {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- await WIKI.db.apiKeys.query().findById(args.id).patch({
- isRevoked: true
- })
- await WIKI.auth.reloadApiKeys()
- WIKI.events.outbound.emit('reloadApiKeys')
- return {
- operation: generateSuccess('API Key revoked successfully')
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Update Authentication Strategies
- */
- async updateAuthStrategies (obj, args, context) {
- try {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- const previousStrategies = await WIKI.db.authentication.getStrategies()
- for (const str of args.strategies) {
- const newStr = {
- displayName: str.displayName,
- isEnabled: str.isEnabled,
- config: _.reduce(str.config, (result, value, key) => {
- _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
- return result
- }, {}),
- selfRegistration: str.selfRegistration,
- domainWhitelist: { v: str.domainWhitelist },
- autoEnrollGroups: { v: str.autoEnrollGroups }
- }
- if (_.some(previousStrategies, ['key', str.key])) {
- await WIKI.db.authentication.query().patch({
- key: str.key,
- strategyKey: str.strategyKey,
- ...newStr
- }).where('key', str.key)
- } else {
- await WIKI.db.authentication.query().insert({
- key: str.key,
- strategyKey: str.strategyKey,
- ...newStr
- })
- }
- }
- for (const str of _.differenceBy(previousStrategies, args.strategies, 'key')) {
- const hasUsers = await WIKI.db.users.query().count('* as total').where({ providerKey: str.key }).first()
- if (_.toSafeInteger(hasUsers.total) > 0) {
- throw new Error(`Cannot delete ${str.displayName} as 1 or more users are still using it.`)
- } else {
- await WIKI.db.authentication.query().delete().where('key', str.key)
- }
- }
- await WIKI.auth.activateStrategies()
- WIKI.events.outbound.emit('reloadAuthStrategies')
- return {
- responseResult: generateSuccess('Strategies updated successfully')
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Generate New Authentication Public / Private Key Certificates
- */
- async regenerateCertificates (obj, args, context) {
- try {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- await WIKI.auth.regenerateCertificates()
- return {
- responseResult: generateSuccess('Certificates have been regenerated successfully.')
- }
- } catch (err) {
- return generateError(err)
- }
- },
- /**
- * Reset Guest User
- */
- async resetGuestUser (obj, args, context) {
- try {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- await WIKI.auth.resetGuestUser()
- return {
- responseResult: generateSuccess('Guest user has been reset successfully.')
- }
- } catch (err) {
- return generateError(err)
- }
- }
- },
- // ------------------------------------------------------------------
- // TYPE: AuthenticationActiveStrategy
- // ------------------------------------------------------------------
- AuthenticationActiveStrategy: {
- config (obj, args, context) {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- return obj.config ?? {}
- },
- allowedEmailRegex (obj, args, context) {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- return obj.allowedEmailRegex ?? ''
- },
- autoEnrollGroups (obj, args, context) {
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
- throw new Error('ERR_FORBIDDEN')
- }
- return obj.autoEnrollGroups ?? []
- },
- strategy (obj, args, context) {
- return _.find(WIKI.data.authentication, ['key', obj.module])
- }
- }
- }
|