authentication.mjs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. import _ from 'lodash-es'
  2. import { generateError, generateSuccess } from '../../helpers/graph.mjs'
  3. import jwt from 'jsonwebtoken'
  4. import ms from 'ms'
  5. import { DateTime } from 'luxon'
  6. import { v4 as uuid } from 'uuid'
  7. import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server'
  8. export default {
  9. Query: {
  10. /**
  11. * List of API Keys
  12. */
  13. async apiKeys (obj, args, context) {
  14. const keys = await WIKI.db.apiKeys.query().orderBy(['isRevoked', 'name'])
  15. return keys.map(k => ({
  16. id: k.id,
  17. name: k.name,
  18. keyShort: '...' + k.key.substring(k.key.length - 20),
  19. isRevoked: k.isRevoked,
  20. expiration: k.expiration,
  21. createdAt: k.createdAt,
  22. updatedAt: k.updatedAt
  23. }))
  24. },
  25. /**
  26. * Current API State
  27. */
  28. apiState () {
  29. return WIKI.config.api.isEnabled
  30. },
  31. /**
  32. * Fetch authentication strategies
  33. */
  34. async authStrategies () {
  35. return WIKI.data.authentication.map(stg => ({
  36. ...stg,
  37. isAvailable: stg.isAvailable === true
  38. }))
  39. },
  40. /**
  41. * Fetch active authentication strategies
  42. */
  43. async authActiveStrategies (obj, args, context) {
  44. const strategies = await WIKI.db.authentication.getStrategies({ enabledOnly: args.enabledOnly })
  45. return strategies.map(a => {
  46. const str = _.find(WIKI.data.authentication, ['key', a.module]) || {}
  47. return {
  48. ...a,
  49. config: _.transform(str.props, (r, v, k) => {
  50. r[k] = v.sensitive ? '********' : a.config[k]
  51. }, {})
  52. }
  53. })
  54. },
  55. /**
  56. * Fetch site authentication strategies
  57. */
  58. async authSiteStrategies (obj, args, context, info) {
  59. const site = await WIKI.db.sites.query().findById(args.siteId)
  60. const activeStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
  61. const siteStrategies = _.sortBy(activeStrategies.map(str => {
  62. const siteAuth = _.find(site.config.authStrategies, ['id', str.id]) || {}
  63. return {
  64. id: str.id,
  65. activeStrategy: str,
  66. order: siteAuth.order ?? 0,
  67. isVisible: siteAuth.isVisible ?? false
  68. }
  69. }), ['order'])
  70. return args.visibleOnly ? siteStrategies.filter(s => s.isVisible) : siteStrategies
  71. }
  72. },
  73. Mutation: {
  74. /**
  75. * Create New API Key
  76. */
  77. async createApiKey (obj, args, context) {
  78. try {
  79. const key = await WIKI.db.apiKeys.createNewKey(args)
  80. await WIKI.auth.reloadApiKeys()
  81. WIKI.events.outbound.emit('reloadApiKeys')
  82. return {
  83. key,
  84. operation: generateSuccess('API Key created successfully')
  85. }
  86. } catch (err) {
  87. WIKI.logger.warn(err)
  88. return generateError(err)
  89. }
  90. },
  91. /**
  92. * Perform Login
  93. */
  94. async login (obj, args, context) {
  95. try {
  96. const authResult = await WIKI.db.users.login(args, context)
  97. return {
  98. ...authResult,
  99. operation: generateSuccess('Login success')
  100. }
  101. } catch (err) {
  102. // LDAP Debug Flag
  103. if (args.strategy === 'ldap' && WIKI.config.flags.ldapdebug) {
  104. WIKI.logger.warn('LDAP LOGIN ERROR (c1): ', err)
  105. }
  106. WIKI.logger.debug(err)
  107. return generateError(err)
  108. }
  109. },
  110. /**
  111. * Perform 2FA Login
  112. */
  113. async loginTFA (obj, args, context) {
  114. try {
  115. const authResult = await WIKI.db.users.loginTFA(args, context)
  116. return {
  117. ...authResult,
  118. operation: generateSuccess('TFA success')
  119. }
  120. } catch (err) {
  121. WIKI.logger.debug(err)
  122. return generateError(err)
  123. }
  124. },
  125. /**
  126. * Setup TFA
  127. */
  128. async setupTFA (obj, args, context) {
  129. try {
  130. const userId = context.req.user?.id
  131. if (!userId) {
  132. throw new Error('ERR_USER_NOT_AUTHENTICATED')
  133. }
  134. const usr = await WIKI.db.users.query().findById(userId)
  135. if (!usr) {
  136. throw new Error('ERR_INVALID_USER')
  137. }
  138. const str = WIKI.auth.strategies[args.strategyId]
  139. if (!str) {
  140. throw new Error('ERR_INVALID_STRATEGY')
  141. }
  142. if (!usr.auth[args.strategyId]) {
  143. throw new Error('ERR_INVALID_STRATEGY')
  144. }
  145. if (usr.auth[args.strategyId].tfaIsActive) {
  146. throw new Error('ERR_TFA_ALREADY_ACTIVE')
  147. }
  148. const tfaQRImage = await usr.generateTFA(args.strategyId, args.siteId)
  149. const tfaToken = await WIKI.db.userKeys.generateToken({
  150. kind: 'tfaSetup',
  151. userId: usr.id,
  152. meta: {
  153. strategyId: args.strategyId
  154. }
  155. })
  156. return {
  157. operation: generateSuccess('TFA setup started'),
  158. continuationToken: tfaToken,
  159. tfaQRImage
  160. }
  161. } catch (err) {
  162. return generateError(err)
  163. }
  164. },
  165. /**
  166. * Deactivate 2FA
  167. */
  168. async deactivateTFA (obj, args, context) {
  169. try {
  170. const userId = context.req.user?.id
  171. if (!userId) {
  172. throw new Error('ERR_USER_NOT_AUTHENTICATED')
  173. }
  174. const usr = await WIKI.db.users.query().findById(userId)
  175. if (!usr) {
  176. throw new Error('ERR_INVALID_USER')
  177. }
  178. const str = WIKI.auth.strategies[args.strategyId]
  179. if (!str) {
  180. throw new Error('ERR_INVALID_STRATEGY')
  181. }
  182. if (!usr.auth[args.strategyId]) {
  183. throw new Error('ERR_INVALID_STRATEGY')
  184. }
  185. if (!usr.auth[args.strategyId].tfaIsActive) {
  186. throw new Error('ERR_TFA_NOT_ACTIVE')
  187. }
  188. usr.auth[args.strategyId].tfaIsActive = false
  189. usr.auth[args.strategyId].tfaSecret = null
  190. await usr.$query().patch({
  191. auth: usr.auth
  192. })
  193. return {
  194. operation: generateSuccess('TFA deactivated successfully.')
  195. }
  196. } catch (err) {
  197. return generateError(err)
  198. }
  199. },
  200. /**
  201. * Setup Passkey
  202. */
  203. async setupPasskey (obj, args, context) {
  204. try {
  205. const userId = context.req.user?.id
  206. if (!userId) {
  207. throw new Error('ERR_USER_NOT_AUTHENTICATED')
  208. }
  209. const usr = await WIKI.db.users.query().findById(userId)
  210. if (!usr) {
  211. throw new Error('ERR_INVALID_USER')
  212. }
  213. const site = WIKI.sites[args.siteId]
  214. if (!site) {
  215. throw new Error('ERR_INVALID_SITE')
  216. } else if (site.hostname === '*') {
  217. WIKI.logger.warn('Cannot use passkeys with a wildcard site hostname. Enter a valid hostname under the Administration Area > General.')
  218. throw new Error('ERR_PK_HOSTNAME_MISSING')
  219. }
  220. const options = await generateRegistrationOptions({
  221. rpName: site.config.title,
  222. rpId: site.hostname,
  223. userID: usr.id,
  224. userName: usr.email,
  225. userDisplayName: usr.name,
  226. attestationType: 'none',
  227. authenticatorSelection: {
  228. residentKey: 'required',
  229. userVerification: 'preferred'
  230. },
  231. excludeCredentials: usr.passkeys.authenticators?.map(authenticator => ({
  232. id: new Uint8Array(authenticator.credentialID),
  233. type: 'public-key',
  234. transports: authenticator.transports
  235. })) ?? []
  236. })
  237. usr.passkeys.reg = {
  238. challenge: options.challenge,
  239. rpId: site.hostname,
  240. siteId: site.id
  241. }
  242. await usr.$query().patch({
  243. passkeys: usr.passkeys
  244. })
  245. return {
  246. operation: generateSuccess('Passkey registration options generated successfully.'),
  247. registrationOptions: options
  248. }
  249. } catch (err) {
  250. return generateError(err)
  251. }
  252. },
  253. /**
  254. * Finalize Passkey Registration
  255. */
  256. async finalizePasskey (obj, args, context) {
  257. try {
  258. const userId = context.req.user?.id
  259. if (!userId) {
  260. throw new Error('ERR_USER_NOT_AUTHENTICATED')
  261. }
  262. const usr = await WIKI.db.users.query().findById(userId)
  263. if (!usr) {
  264. throw new Error('ERR_INVALID_USER')
  265. } else if (!usr.passkeys?.reg) {
  266. throw new Error('ERR_PASSKEY_NOT_SETUP')
  267. }
  268. if (!args.name || args.name.trim().length < 1 || args.name.length > 255) {
  269. throw new Error('ERR_PK_NAME_MISSING_OR_INVALID')
  270. }
  271. const verification = await verifyRegistrationResponse({
  272. response: args.registrationResponse,
  273. expectedChallenge: usr.passkeys.reg.challenge,
  274. expectedOrigin: `https://${usr.passkeys.reg.rpId}`,
  275. expectedRPID: usr.passkeys.reg.rpId,
  276. requireUserVerification: true
  277. })
  278. if (!verification.verified) {
  279. throw new Error('ERR_PK_VERIFICATION_FAILED')
  280. }
  281. if (!usr.passkeys.authenticators) {
  282. usr.passkeys.authenticators = []
  283. }
  284. usr.passkeys.authenticators.push({
  285. ...verification.registrationInfo,
  286. id: uuid(),
  287. createdAt: new Date(),
  288. name: args.name,
  289. siteId: usr.passkeys.reg.siteId,
  290. transports: args.registrationResponse.response.transports
  291. })
  292. delete usr.passkeys.reg
  293. await usr.$query().patch({
  294. passkeys: JSON.stringify(usr.passkeys, (k, v) => {
  295. if (v instanceof Uint8Array) {
  296. return Array.apply([], v)
  297. }
  298. return v
  299. })
  300. })
  301. return {
  302. operation: generateSuccess('Passkey registered successfully.')
  303. }
  304. } catch (err) {
  305. return generateError(err)
  306. }
  307. },
  308. /**
  309. * Deactivate a passkey
  310. */
  311. async deactivatePasskey (obj, args, context) {
  312. try {
  313. const userId = context.req.user?.id
  314. if (!userId) {
  315. throw new Error('ERR_USER_NOT_AUTHENTICATED')
  316. }
  317. const usr = await WIKI.db.users.query().findById(userId)
  318. if (!usr) {
  319. throw new Error('ERR_INVALID_USER')
  320. } else if (!usr.passkeys?.authenticators) {
  321. throw new Error('ERR_PASSKEY_NOT_SETUP')
  322. }
  323. usr.passkeys.authenticators = usr.passkeys.authenticators.filter(a => a.id !== args.id)
  324. await usr.$query().patch({
  325. passkeys: usr.passkeys
  326. })
  327. return {
  328. operation: generateSuccess('Passkey deactivated successfully.')
  329. }
  330. } catch (err) {
  331. return generateError(err)
  332. }
  333. },
  334. /**
  335. * Perform Password Change
  336. */
  337. async changePassword (obj, args, context) {
  338. try {
  339. if (args.continuationToken) {
  340. const authResult = await WIKI.db.users.loginChangePassword(args, context)
  341. return {
  342. ...authResult,
  343. operation: generateSuccess('Password set successfully')
  344. }
  345. } else {
  346. await WIKI.db.users.changePassword(args, context)
  347. return {
  348. operation: generateSuccess('Password changed successfully')
  349. }
  350. }
  351. } catch (err) {
  352. WIKI.logger.debug(err)
  353. return generateError(err)
  354. }
  355. },
  356. /**
  357. * Perform Forget Password
  358. */
  359. async forgotPassword (obj, args, context) {
  360. try {
  361. await WIKI.db.users.loginForgotPassword(args, context)
  362. return {
  363. operation: generateSuccess('Password reset request processed.')
  364. }
  365. } catch (err) {
  366. return generateError(err)
  367. }
  368. },
  369. /**
  370. * Register a new account
  371. */
  372. async register (obj, args, context) {
  373. try {
  374. const usr = await WIKI.db.users.createNewUser({ ...args, userInitiated: true })
  375. const authResult = await WIKI.db.users.afterLoginChecks(usr, WIKI.data.systemIds.localAuthId, context)
  376. return {
  377. ...authResult,
  378. operation: generateSuccess('Registration success')
  379. }
  380. } catch (err) {
  381. return generateError(err)
  382. }
  383. },
  384. /**
  385. * Refresh Token
  386. */
  387. async refreshToken (obj, args, context) {
  388. try {
  389. let decoded = {}
  390. if (!args.token) {
  391. throw new Error('ERR_MISSING_TOKEN')
  392. }
  393. try {
  394. decoded = jwt.verify(args.token, WIKI.config.auth.certs.public, {
  395. audience: WIKI.config.auth.audience,
  396. issuer: 'urn:wiki.js',
  397. algorithms: ['RS256'],
  398. ignoreExpiration: true
  399. })
  400. } catch (err) {
  401. throw new Error('ERR_INVALID_TOKEN')
  402. }
  403. if (DateTime.utc().minus(ms(WIKI.config.auth.tokenRenewal)) > DateTime.fromSeconds(decoded.exp)) {
  404. throw new Error('ERR_EXPIRED_TOKEN')
  405. }
  406. const newToken = await WIKI.db.users.refreshToken(decoded.id)
  407. return {
  408. jwt: newToken.token,
  409. operation: generateSuccess('Token refreshed successfully')
  410. }
  411. } catch (err) {
  412. return generateError(err)
  413. }
  414. },
  415. /**
  416. * Set API state
  417. */
  418. async setApiState (obj, args, context) {
  419. try {
  420. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  421. throw new Error('ERR_FORBIDDEN')
  422. }
  423. WIKI.config.api.isEnabled = args.enabled
  424. await WIKI.configSvc.saveToDb(['api'])
  425. return {
  426. operation: generateSuccess('API State changed successfully')
  427. }
  428. } catch (err) {
  429. return generateError(err)
  430. }
  431. },
  432. /**
  433. * Revoke an API key
  434. */
  435. async revokeApiKey (obj, args, context) {
  436. try {
  437. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  438. throw new Error('ERR_FORBIDDEN')
  439. }
  440. await WIKI.db.apiKeys.query().findById(args.id).patch({
  441. isRevoked: true
  442. })
  443. await WIKI.auth.reloadApiKeys()
  444. WIKI.events.outbound.emit('reloadApiKeys')
  445. return {
  446. operation: generateSuccess('API Key revoked successfully')
  447. }
  448. } catch (err) {
  449. return generateError(err)
  450. }
  451. },
  452. /**
  453. * Update Authentication Strategies
  454. */
  455. async updateAuthStrategies (obj, args, context) {
  456. try {
  457. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  458. throw new Error('ERR_FORBIDDEN')
  459. }
  460. const previousStrategies = await WIKI.db.authentication.getStrategies()
  461. for (const str of args.strategies) {
  462. const newStr = {
  463. displayName: str.displayName,
  464. isEnabled: str.isEnabled,
  465. config: _.reduce(str.config, (result, value, key) => {
  466. _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
  467. return result
  468. }, {}),
  469. selfRegistration: str.selfRegistration,
  470. domainWhitelist: { v: str.domainWhitelist },
  471. autoEnrollGroups: { v: str.autoEnrollGroups }
  472. }
  473. if (_.some(previousStrategies, ['key', str.key])) {
  474. await WIKI.db.authentication.query().patch({
  475. key: str.key,
  476. strategyKey: str.strategyKey,
  477. ...newStr
  478. }).where('key', str.key)
  479. } else {
  480. await WIKI.db.authentication.query().insert({
  481. key: str.key,
  482. strategyKey: str.strategyKey,
  483. ...newStr
  484. })
  485. }
  486. }
  487. for (const str of _.differenceBy(previousStrategies, args.strategies, 'key')) {
  488. const hasUsers = await WIKI.db.users.query().count('* as total').where({ providerKey: str.key }).first()
  489. if (_.toSafeInteger(hasUsers.total) > 0) {
  490. throw new Error(`Cannot delete ${str.displayName} as 1 or more users are still using it.`)
  491. } else {
  492. await WIKI.db.authentication.query().delete().where('key', str.key)
  493. }
  494. }
  495. await WIKI.auth.activateStrategies()
  496. WIKI.events.outbound.emit('reloadAuthStrategies')
  497. return {
  498. responseResult: generateSuccess('Strategies updated successfully')
  499. }
  500. } catch (err) {
  501. return generateError(err)
  502. }
  503. },
  504. /**
  505. * Generate New Authentication Public / Private Key Certificates
  506. */
  507. async regenerateCertificates (obj, args, context) {
  508. try {
  509. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  510. throw new Error('ERR_FORBIDDEN')
  511. }
  512. await WIKI.auth.regenerateCertificates()
  513. return {
  514. responseResult: generateSuccess('Certificates have been regenerated successfully.')
  515. }
  516. } catch (err) {
  517. return generateError(err)
  518. }
  519. },
  520. /**
  521. * Reset Guest User
  522. */
  523. async resetGuestUser (obj, args, context) {
  524. try {
  525. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  526. throw new Error('ERR_FORBIDDEN')
  527. }
  528. await WIKI.auth.resetGuestUser()
  529. return {
  530. responseResult: generateSuccess('Guest user has been reset successfully.')
  531. }
  532. } catch (err) {
  533. return generateError(err)
  534. }
  535. }
  536. },
  537. // ------------------------------------------------------------------
  538. // TYPE: AuthenticationActiveStrategy
  539. // ------------------------------------------------------------------
  540. AuthenticationActiveStrategy: {
  541. config (obj, args, context) {
  542. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  543. throw new Error('ERR_FORBIDDEN')
  544. }
  545. return obj.config ?? {}
  546. },
  547. allowedEmailRegex (obj, args, context) {
  548. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  549. throw new Error('ERR_FORBIDDEN')
  550. }
  551. return obj.allowedEmailRegex ?? ''
  552. },
  553. autoEnrollGroups (obj, args, context) {
  554. if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
  555. throw new Error('ERR_FORBIDDEN')
  556. }
  557. return obj.autoEnrollGroups ?? []
  558. },
  559. strategy (obj, args, context) {
  560. return _.find(WIKI.data.authentication, ['key', obj.module])
  561. }
  562. }
  563. }