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.

214 lines
6.1 KiB

  1. /* global wiki */
  2. const Promise = require('bluebird')
  3. const bcrypt = require('bcryptjs-then')
  4. const _ = require('lodash')
  5. const tfa = require('node-2fa')
  6. const securityHelper = require('../helpers/security')
  7. /**
  8. * Users schema
  9. */
  10. module.exports = (sequelize, DataTypes) => {
  11. let userSchema = sequelize.define('user', {
  12. email: {
  13. type: DataTypes.STRING,
  14. allowNull: false,
  15. validate: {
  16. isEmail: true
  17. }
  18. },
  19. provider: {
  20. type: DataTypes.STRING,
  21. allowNull: false
  22. },
  23. providerId: {
  24. type: DataTypes.STRING,
  25. allowNull: true
  26. },
  27. password: {
  28. type: DataTypes.STRING,
  29. allowNull: true
  30. },
  31. name: {
  32. type: DataTypes.STRING,
  33. allowNull: true
  34. },
  35. role: {
  36. type: DataTypes.ENUM('admin', 'user', 'guest'),
  37. allowNull: false
  38. },
  39. tfaIsActive: {
  40. type: DataTypes.BOOLEAN,
  41. allowNull: false,
  42. defaultValue: false
  43. },
  44. tfaSecret: {
  45. type: DataTypes.STRING,
  46. allowNull: true
  47. }
  48. }, {
  49. timestamps: true,
  50. version: true,
  51. indexes: [
  52. {
  53. unique: true,
  54. fields: ['provider', 'email']
  55. }
  56. ]
  57. })
  58. userSchema.prototype.validatePassword = async function (rawPwd) {
  59. if (await bcrypt.compare(rawPwd, this.password) === true) {
  60. return true
  61. } else {
  62. throw new wiki.Error.AuthLoginFailed()
  63. }
  64. }
  65. userSchema.prototype.enableTFA = async function () {
  66. let tfaInfo = tfa.generateSecret({
  67. name: wiki.config.site.title
  68. })
  69. this.tfaIsActive = true
  70. this.tfaSecret = tfaInfo.secret
  71. return this.save()
  72. }
  73. userSchema.prototype.disableTFA = async function () {
  74. this.tfaIsActive = false
  75. this.tfaSecret = ''
  76. return this.save()
  77. }
  78. userSchema.prototype.verifyTFA = function (code) {
  79. let result = tfa.verifyToken(this.tfaSecret, code)
  80. return (result && _.has(result, 'delta') && result.delta === 0)
  81. }
  82. userSchema.login = async (opts, context) => {
  83. if (_.has(wiki.config.auth.strategies, opts.provider)) {
  84. _.set(context.req, 'body.email', opts.username)
  85. _.set(context.req, 'body.password', opts.password)
  86. // Authenticate
  87. return new Promise((resolve, reject) => {
  88. wiki.auth.passport.authenticate(opts.provider, async (err, user, info) => {
  89. if (err) { return reject(err) }
  90. if (!user) { return reject(new wiki.Error.AuthLoginFailed()) }
  91. // Is 2FA required?
  92. if (user.tfaIsActive) {
  93. try {
  94. let loginToken = await securityHelper.generateToken(32)
  95. await wiki.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
  96. return resolve({
  97. succeeded: true,
  98. message: 'Login Successful. Awaiting 2FA security code.',
  99. tfaRequired: true,
  100. tfaLoginToken: loginToken
  101. })
  102. } catch (err) {
  103. wiki.logger.warn(err)
  104. return reject(new wiki.Error.AuthGenericError())
  105. }
  106. } else {
  107. // No 2FA, log in user
  108. return context.req.logIn(user, err => {
  109. if (err) { return reject(err) }
  110. resolve({
  111. succeeded: true,
  112. message: 'Login Successful',
  113. tfaRequired: false
  114. })
  115. })
  116. }
  117. })(context.req, context.res, () => {})
  118. })
  119. } else {
  120. throw new wiki.Error.AuthProviderInvalid()
  121. }
  122. }
  123. userSchema.loginTFA = async (opts, context) => {
  124. if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
  125. let result = await wiki.redis.get(`tfa:${opts.loginToken}`)
  126. if (result) {
  127. let userId = _.toSafeInteger(result)
  128. if (userId && userId > 0) {
  129. let user = await wiki.db.User.findById(userId)
  130. if (user && user.verifyTFA(opts.securityCode)) {
  131. return Promise.fromCallback(clb => {
  132. context.req.logIn(user, clb)
  133. }).return({
  134. succeeded: true,
  135. message: 'Login Successful'
  136. }).catch(err => {
  137. wiki.logger.warn(err)
  138. throw new wiki.Error.AuthGenericError()
  139. })
  140. } else {
  141. throw new wiki.Error.AuthTFAFailed()
  142. }
  143. }
  144. }
  145. }
  146. throw new wiki.Error.AuthTFAInvalid()
  147. }
  148. userSchema.processProfile = (profile) => {
  149. let primaryEmail = ''
  150. if (_.isArray(profile.emails)) {
  151. let e = _.find(profile.emails, ['primary', true])
  152. primaryEmail = (e) ? e.value : _.first(profile.emails).value
  153. } else if (_.isString(profile.email) && profile.email.length > 5) {
  154. primaryEmail = profile.email
  155. } else if (_.isString(profile.mail) && profile.mail.length > 5) {
  156. primaryEmail = profile.mail
  157. } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
  158. primaryEmail = profile.user.email
  159. } else {
  160. return Promise.reject(new Error(wiki.lang.t('auth:errors.invaliduseremail')))
  161. }
  162. profile.provider = _.lowerCase(profile.provider)
  163. primaryEmail = _.toLower(primaryEmail)
  164. return wiki.db.User.findOneAndUpdate({
  165. email: primaryEmail,
  166. provider: profile.provider
  167. }, {
  168. email: primaryEmail,
  169. provider: profile.provider,
  170. providerId: profile.id,
  171. name: profile.displayName || _.split(primaryEmail, '@')[0]
  172. }, {
  173. new: true
  174. }).then((user) => {
  175. // Handle unregistered accounts
  176. if (!user && profile.provider !== 'local' && (wiki.config.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
  177. let nUsr = {
  178. email: primaryEmail,
  179. provider: profile.provider,
  180. providerId: profile.id,
  181. password: '',
  182. name: profile.displayName || profile.name || profile.cn,
  183. rights: [{
  184. role: 'read',
  185. path: '/',
  186. exact: false,
  187. deny: false
  188. }]
  189. }
  190. return wiki.db.User.create(nUsr)
  191. }
  192. return user || Promise.reject(new Error(wiki.lang.t('auth:errors:notyetauthorized')))
  193. })
  194. }
  195. userSchema.hashPassword = (rawPwd) => {
  196. return bcrypt.hash(rawPwd)
  197. }
  198. return userSchema
  199. }