You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

475 lines
14 KiB

6 years ago
  1. /* global WIKI */
  2. const bcrypt = require('bcryptjs-then')
  3. const _ = require('lodash')
  4. const tfa = require('node-2fa')
  5. const securityHelper = require('../helpers/security')
  6. const jwt = require('jsonwebtoken')
  7. const Model = require('objection').Model
  8. const validate = require('validate.js')
  9. const bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/
  10. /**
  11. * Users model
  12. */
  13. module.exports = class User extends Model {
  14. static get tableName() { return 'users' }
  15. static get jsonSchema () {
  16. return {
  17. type: 'object',
  18. required: ['email'],
  19. properties: {
  20. id: {type: 'integer'},
  21. email: {type: 'string', format: 'email'},
  22. name: {type: 'string', minLength: 1, maxLength: 255},
  23. providerId: {type: 'string'},
  24. password: {type: 'string'},
  25. role: {type: 'string', enum: ['admin', 'guest', 'user']},
  26. tfaIsActive: {type: 'boolean', default: false},
  27. tfaSecret: {type: 'string'},
  28. jobTitle: {type: 'string'},
  29. location: {type: 'string'},
  30. pictureUrl: {type: 'string'},
  31. isSystem: {type: 'boolean'},
  32. isActive: {type: 'boolean'},
  33. isVerified: {type: 'boolean'},
  34. createdAt: {type: 'string'},
  35. updatedAt: {type: 'string'}
  36. }
  37. }
  38. }
  39. static get relationMappings() {
  40. return {
  41. groups: {
  42. relation: Model.ManyToManyRelation,
  43. modelClass: require('./groups'),
  44. join: {
  45. from: 'users.id',
  46. through: {
  47. from: 'userGroups.userId',
  48. to: 'userGroups.groupId'
  49. },
  50. to: 'groups.id'
  51. }
  52. },
  53. provider: {
  54. relation: Model.BelongsToOneRelation,
  55. modelClass: require('./authentication'),
  56. join: {
  57. from: 'users.providerKey',
  58. to: 'authentication.key'
  59. }
  60. },
  61. defaultEditor: {
  62. relation: Model.BelongsToOneRelation,
  63. modelClass: require('./editors'),
  64. join: {
  65. from: 'users.editorKey',
  66. to: 'editors.key'
  67. }
  68. },
  69. locale: {
  70. relation: Model.BelongsToOneRelation,
  71. modelClass: require('./locales'),
  72. join: {
  73. from: 'users.localeCode',
  74. to: 'locales.code'
  75. }
  76. }
  77. }
  78. }
  79. async $beforeUpdate(opt, context) {
  80. await super.$beforeUpdate(opt, context)
  81. this.updatedAt = new Date().toISOString()
  82. if (!(opt.patch && this.password === undefined)) {
  83. await this.generateHash()
  84. }
  85. }
  86. async $beforeInsert(context) {
  87. await super.$beforeInsert(context)
  88. this.createdAt = new Date().toISOString()
  89. this.updatedAt = new Date().toISOString()
  90. await this.generateHash()
  91. }
  92. // ------------------------------------------------
  93. // Instance Methods
  94. // ------------------------------------------------
  95. async generateHash() {
  96. if (this.password) {
  97. if (bcryptRegexp.test(this.password)) { return }
  98. this.password = await bcrypt.hash(this.password, 12)
  99. }
  100. }
  101. async verifyPassword(pwd) {
  102. if (await bcrypt.compare(pwd, this.password) === true) {
  103. return true
  104. } else {
  105. throw new WIKI.Error.AuthLoginFailed()
  106. }
  107. }
  108. async enableTFA() {
  109. let tfaInfo = tfa.generateSecret({
  110. name: WIKI.config.site.title
  111. })
  112. return this.$query.patch({
  113. tfaIsActive: true,
  114. tfaSecret: tfaInfo.secret
  115. })
  116. }
  117. async disableTFA() {
  118. return this.$query.patch({
  119. tfaIsActive: false,
  120. tfaSecret: ''
  121. })
  122. }
  123. async verifyTFA(code) {
  124. let result = tfa.verifyToken(this.tfaSecret, code)
  125. return (result && _.has(result, 'delta') && result.delta === 0)
  126. }
  127. getGlobalPermissions() {
  128. return _.uniq(_.flatten(_.map(this.groups, 'permissions')))
  129. }
  130. getGroups() {
  131. return _.uniq(_.map(this.groups, 'id'))
  132. }
  133. // ------------------------------------------------
  134. // Model Methods
  135. // ------------------------------------------------
  136. static async processProfile({ profile, providerKey }) {
  137. const provider = _.get(WIKI.auth.strategies, providerKey, {})
  138. provider.info = _.find(WIKI.data.authentication, ['key', providerKey])
  139. // Find existing user
  140. let user = await WIKI.models.users.query().findOne({
  141. providerId: _.toString(profile.id),
  142. providerKey
  143. })
  144. // Parse email
  145. let primaryEmail = ''
  146. if (_.isArray(profile.emails)) {
  147. const e = _.find(profile.emails, ['primary', true])
  148. primaryEmail = (e) ? e.value : _.first(profile.emails).value
  149. } else if (_.isString(profile.email) && profile.email.length > 5) {
  150. primaryEmail = profile.email
  151. } else if (_.isString(profile.mail) && profile.mail.length > 5) {
  152. primaryEmail = profile.mail
  153. } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
  154. primaryEmail = profile.user.email
  155. } else {
  156. throw new Error('Missing or invalid email address from profile.')
  157. }
  158. primaryEmail = _.toLower(primaryEmail)
  159. // Parse display name
  160. let displayName = ''
  161. if (_.isString(profile.displayName) && profile.displayName.length > 0) {
  162. displayName = profile.displayName
  163. } else if (_.isString(profile.name) && profile.name.length > 0) {
  164. displayName = profile.name
  165. } else {
  166. displayName = primaryEmail.split('@')[0]
  167. }
  168. // Parse picture URL
  169. let pictureUrl = _.get(profile, 'picture', _.get(user, 'pictureUrl', null))
  170. // Update existing user
  171. if (user) {
  172. if (!user.isActive) {
  173. throw new WIKI.Error.AuthAccountBanned()
  174. }
  175. if (user.isSystem) {
  176. throw new Error('This is a system reserved account and cannot be used.')
  177. }
  178. user = await user.$query().patchAndFetch({
  179. email: primaryEmail,
  180. name: displayName,
  181. pictureUrl: pictureUrl
  182. })
  183. return user
  184. }
  185. // Self-registration
  186. if (provider.selfRegistration) {
  187. // Check if email domain is whitelisted
  188. if (_.get(provider, 'domainWhitelist', []).length > 0) {
  189. const emailDomain = _.last(primaryEmail.split('@'))
  190. if (!_.includes(provider.domainWhitelist, emailDomain)) {
  191. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  192. }
  193. }
  194. // Create account
  195. user = await WIKI.models.users.query().insertAndFetch({
  196. providerKey: providerKey,
  197. providerId: _.toString(profile.id),
  198. email: primaryEmail,
  199. name: displayName,
  200. pictureUrl: pictureUrl,
  201. localeCode: WIKI.config.lang.code,
  202. defaultEditor: 'markdown',
  203. tfaIsActive: false,
  204. isSystem: false,
  205. isActive: true,
  206. isVerified: true
  207. })
  208. // Assign to group(s)
  209. if (provider.autoEnrollGroups.length > 0) {
  210. await user.$relatedQuery('groups').relate(provider.autoEnrollGroups)
  211. }
  212. return user
  213. }
  214. throw new Error('You are not authorized to login.')
  215. }
  216. static async login (opts, context) {
  217. if (_.has(WIKI.auth.strategies, opts.strategy)) {
  218. const strInfo = _.find(WIKI.data.authentication, ['key', opts.strategy])
  219. // Inject form user/pass
  220. if (strInfo.useForm) {
  221. _.set(context.req, 'body.email', opts.username)
  222. _.set(context.req, 'body.password', opts.password)
  223. }
  224. // Authenticate
  225. return new Promise((resolve, reject) => {
  226. WIKI.auth.passport.authenticate(opts.strategy, {
  227. session: !strInfo.useForm,
  228. scope: strInfo.scopes ? strInfo.scopes : null
  229. }, async (err, user, info) => {
  230. if (err) { return reject(err) }
  231. if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
  232. // Is 2FA required?
  233. if (user.tfaIsActive) {
  234. try {
  235. let loginToken = await securityHelper.generateToken(32)
  236. await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
  237. return resolve({
  238. tfaRequired: true,
  239. tfaLoginToken: loginToken
  240. })
  241. } catch (err) {
  242. WIKI.logger.warn(err)
  243. return reject(new WIKI.Error.AuthGenericError())
  244. }
  245. } else {
  246. // No 2FA, log in user
  247. return context.req.logIn(user, { session: !strInfo.useForm }, async err => {
  248. if (err) { return reject(err) }
  249. const jwtToken = await WIKI.models.users.refreshToken(user)
  250. resolve({
  251. jwt: jwtToken.token,
  252. tfaRequired: false
  253. })
  254. })
  255. }
  256. })(context.req, context.res, () => {})
  257. })
  258. } else {
  259. throw new WIKI.Error.AuthProviderInvalid()
  260. }
  261. }
  262. static async refreshToken(user) {
  263. if (_.isSafeInteger(user)) {
  264. user = await WIKI.models.users.query().findById(user).eager('groups').modifyEager('groups', builder => {
  265. builder.select('groups.id', 'permissions')
  266. })
  267. if (!user) {
  268. WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)
  269. throw new WIKI.Error.AuthGenericError()
  270. }
  271. } else if (_.isNil(user.groups)) {
  272. await user.$relatedQuery('groups').select('groups.id', 'permissions')
  273. }
  274. return {
  275. token: jwt.sign({
  276. id: user.id,
  277. email: user.email,
  278. name: user.name,
  279. pictureUrl: user.pictureUrl,
  280. timezone: user.timezone,
  281. localeCode: user.localeCode,
  282. defaultEditor: user.defaultEditor,
  283. permissions: user.getGlobalPermissions(),
  284. groups: user.getGroups()
  285. }, {
  286. key: WIKI.config.certs.private,
  287. passphrase: WIKI.config.sessionSecret
  288. }, {
  289. algorithm: 'RS256',
  290. expiresIn: WIKI.config.auth.tokenExpiration,
  291. audience: WIKI.config.auth.audience,
  292. issuer: 'urn:wiki.js'
  293. }),
  294. user
  295. }
  296. }
  297. static async loginTFA(opts, context) {
  298. if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
  299. let result = null // await WIKI.redis.get(`tfa:${opts.loginToken}`)
  300. if (result) {
  301. let userId = _.toSafeInteger(result)
  302. if (userId && userId > 0) {
  303. let user = await WIKI.models.users.query().findById(userId)
  304. if (user && user.verifyTFA(opts.securityCode)) {
  305. return Promise.fromCallback(clb => {
  306. context.req.logIn(user, clb)
  307. }).return({
  308. succeeded: true,
  309. message: 'Login Successful'
  310. }).catch(err => {
  311. WIKI.logger.warn(err)
  312. throw new WIKI.Error.AuthGenericError()
  313. })
  314. } else {
  315. throw new WIKI.Error.AuthTFAFailed()
  316. }
  317. }
  318. }
  319. }
  320. throw new WIKI.Error.AuthTFAInvalid()
  321. }
  322. static async register ({ email, password, name, verify = false, bypassChecks = false }, context) {
  323. const localStrg = await WIKI.models.authentication.getStrategy('local')
  324. // Check if self-registration is enabled
  325. if (localStrg.selfRegistration || bypassChecks) {
  326. // Input sanitization
  327. email = _.toLower(email)
  328. // Input validation
  329. const validation = validate({
  330. email,
  331. password,
  332. name
  333. }, {
  334. email: {
  335. email: true,
  336. length: {
  337. maximum: 255
  338. }
  339. },
  340. password: {
  341. presence: {
  342. allowEmpty: false
  343. },
  344. length: {
  345. minimum: 6
  346. }
  347. },
  348. name: {
  349. presence: {
  350. allowEmpty: false
  351. },
  352. length: {
  353. minimum: 2,
  354. maximum: 255
  355. }
  356. }
  357. }, { format: 'flat' })
  358. if (validation && validation.length > 0) {
  359. throw new WIKI.Error.InputInvalid(validation[0])
  360. }
  361. // Check if email domain is whitelisted
  362. if (_.get(localStrg, 'domainWhitelist.v', []).length > 0 && !bypassChecks) {
  363. const emailDomain = _.last(email.split('@'))
  364. if (!_.includes(localStrg.domainWhitelist.v, emailDomain)) {
  365. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  366. }
  367. }
  368. // Check if email already exists
  369. const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' })
  370. if (!usr) {
  371. // Create the account
  372. const newUsr = await WIKI.models.users.query().insert({
  373. provider: 'local',
  374. email,
  375. name,
  376. password,
  377. locale: 'en',
  378. defaultEditor: 'markdown',
  379. tfaIsActive: false,
  380. isSystem: false,
  381. isActive: true,
  382. isVerified: false
  383. })
  384. // Assign to group(s)
  385. if (_.get(localStrg, 'autoEnrollGroups.v', []).length > 0) {
  386. await newUsr.$relatedQuery('groups').relate(localStrg.autoEnrollGroups.v)
  387. }
  388. if (verify) {
  389. // Create verification token
  390. const verificationToken = await WIKI.models.userKeys.generateToken({
  391. kind: 'verify',
  392. userId: newUsr.id
  393. })
  394. // Send verification email
  395. await WIKI.mail.send({
  396. template: 'accountVerify',
  397. to: email,
  398. subject: 'Verify your account',
  399. data: {
  400. preheadertext: 'Verify your account in order to gain access to the wiki.',
  401. title: 'Verify your account',
  402. content: 'Click the button below in order to verify your account and gain access to the wiki.',
  403. buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,
  404. buttonText: 'Verify'
  405. },
  406. text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.host}/verify/${verificationToken}`
  407. })
  408. }
  409. return true
  410. } else {
  411. throw new WIKI.Error.AuthAccountAlreadyExists()
  412. }
  413. } else {
  414. throw new WIKI.Error.AuthRegistrationDisabled()
  415. }
  416. }
  417. static async getGuestUser () {
  418. const user = await WIKI.models.users.query().findById(2).eager('groups').modifyEager('groups', builder => {
  419. builder.select('groups.id', 'permissions')
  420. })
  421. if (!user) {
  422. WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')
  423. process.exit(1)
  424. }
  425. user.permissions = user.getGlobalPermissions()
  426. return user
  427. }
  428. }