users.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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', 'name', 'provider'],
  19. properties: {
  20. id: {type: 'integer'},
  21. email: {type: 'string', format: 'email'},
  22. name: {type: 'string', minLength: 1, maxLength: 255},
  23. providerId: {type: 'number'},
  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. async generateHash() {
  93. if (this.password) {
  94. if (bcryptRegexp.test(this.password)) { return }
  95. this.password = await bcrypt.hash(this.password, 12)
  96. }
  97. }
  98. async verifyPassword(pwd) {
  99. if (await bcrypt.compare(pwd, this.password) === true) {
  100. return true
  101. } else {
  102. throw new WIKI.Error.AuthLoginFailed()
  103. }
  104. }
  105. async enableTFA() {
  106. let tfaInfo = tfa.generateSecret({
  107. name: WIKI.config.site.title
  108. })
  109. return this.$query.patch({
  110. tfaIsActive: true,
  111. tfaSecret: tfaInfo.secret
  112. })
  113. }
  114. async disableTFA() {
  115. return this.$query.patch({
  116. tfaIsActive: false,
  117. tfaSecret: ''
  118. })
  119. }
  120. async verifyTFA(code) {
  121. let result = tfa.verifyToken(this.tfaSecret, code)
  122. return (result && _.has(result, 'delta') && result.delta === 0)
  123. }
  124. static async processProfile(profile) {
  125. let primaryEmail = ''
  126. if (_.isArray(profile.emails)) {
  127. let e = _.find(profile.emails, ['primary', true])
  128. primaryEmail = (e) ? e.value : _.first(profile.emails).value
  129. } else if (_.isString(profile.email) && profile.email.length > 5) {
  130. primaryEmail = profile.email
  131. } else if (_.isString(profile.mail) && profile.mail.length > 5) {
  132. primaryEmail = profile.mail
  133. } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
  134. primaryEmail = profile.user.email
  135. } else {
  136. return Promise.reject(new Error(WIKI.lang.t('auth:errors.invaliduseremail')))
  137. }
  138. profile.provider = _.lowerCase(profile.provider)
  139. primaryEmail = _.toLower(primaryEmail)
  140. let user = await WIKI.models.users.query().findOne({
  141. email: primaryEmail,
  142. provider: profile.provider
  143. })
  144. if (user) {
  145. user.$query().patchAdnFetch({
  146. email: primaryEmail,
  147. provider: profile.provider,
  148. providerId: profile.id,
  149. name: profile.displayName || _.split(primaryEmail, '@')[0]
  150. })
  151. } else {
  152. user = await WIKI.models.users.query().insertAndFetch({
  153. email: primaryEmail,
  154. provider: profile.provider,
  155. providerId: profile.id,
  156. name: profile.displayName || _.split(primaryEmail, '@')[0]
  157. })
  158. }
  159. // Handle unregistered accounts
  160. // if (!user && profile.provider !== 'local' && (WIKI.config.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
  161. // let nUsr = {
  162. // email: primaryEmail,
  163. // provider: profile.provider,
  164. // providerId: profile.id,
  165. // password: '',
  166. // name: profile.displayName || profile.name || profile.cn,
  167. // rights: [{
  168. // role: 'read',
  169. // path: '/',
  170. // exact: false,
  171. // deny: false
  172. // }]
  173. // }
  174. // return WIKI.models.users.query().insert(nUsr)
  175. // }
  176. return user
  177. }
  178. static async login (opts, context) {
  179. if (_.has(WIKI.auth.strategies, opts.strategy)) {
  180. _.set(context.req, 'body.email', opts.username)
  181. _.set(context.req, 'body.password', opts.password)
  182. // Authenticate
  183. return new Promise((resolve, reject) => {
  184. WIKI.auth.passport.authenticate(opts.strategy, { session: false }, async (err, user, info) => {
  185. if (err) { return reject(err) }
  186. if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
  187. // Is 2FA required?
  188. if (user.tfaIsActive) {
  189. try {
  190. let loginToken = await securityHelper.generateToken(32)
  191. await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
  192. return resolve({
  193. tfaRequired: true,
  194. tfaLoginToken: loginToken
  195. })
  196. } catch (err) {
  197. WIKI.logger.warn(err)
  198. return reject(new WIKI.Error.AuthGenericError())
  199. }
  200. } else {
  201. // No 2FA, log in user
  202. return context.req.logIn(user, { session: false }, async err => {
  203. if (err) { return reject(err) }
  204. const jwtToken = await WIKI.models.users.refreshToken(user)
  205. resolve({
  206. jwt: jwtToken.token,
  207. tfaRequired: false
  208. })
  209. })
  210. }
  211. })(context.req, context.res, () => {})
  212. })
  213. } else {
  214. throw new WIKI.Error.AuthProviderInvalid()
  215. }
  216. }
  217. static async refreshToken(user) {
  218. if (_.isSafeInteger(user)) {
  219. user = await WIKI.models.users.query().findById(user)
  220. if (!user) {
  221. WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)
  222. throw new WIKI.Error.AuthGenericError()
  223. }
  224. }
  225. return {
  226. token: jwt.sign({
  227. id: user.id,
  228. email: user.email,
  229. name: user.name,
  230. pictureUrl: user.pictureUrl,
  231. timezone: user.timezone,
  232. localeCode: user.localeCode,
  233. defaultEditor: user.defaultEditor,
  234. permissions: ['manage:system']
  235. }, {
  236. key: WIKI.config.certs.private,
  237. passphrase: WIKI.config.sessionSecret
  238. }, {
  239. algorithm: 'RS256',
  240. expiresIn: '30m',
  241. audience: 'urn:wiki.js', // TODO: use value from admin
  242. issuer: 'urn:wiki.js'
  243. }),
  244. user
  245. }
  246. }
  247. static async loginTFA(opts, context) {
  248. if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
  249. let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
  250. if (result) {
  251. let userId = _.toSafeInteger(result)
  252. if (userId && userId > 0) {
  253. let user = await WIKI.models.users.query().findById(userId)
  254. if (user && user.verifyTFA(opts.securityCode)) {
  255. return Promise.fromCallback(clb => {
  256. context.req.logIn(user, clb)
  257. }).return({
  258. succeeded: true,
  259. message: 'Login Successful'
  260. }).catch(err => {
  261. WIKI.logger.warn(err)
  262. throw new WIKI.Error.AuthGenericError()
  263. })
  264. } else {
  265. throw new WIKI.Error.AuthTFAFailed()
  266. }
  267. }
  268. }
  269. }
  270. throw new WIKI.Error.AuthTFAInvalid()
  271. }
  272. static async register ({ email, password, name }, context) {
  273. const localStrg = await WIKI.models.authentication.getStrategy('local')
  274. // Check if self-registration is enabled
  275. if (localStrg.selfRegistration) {
  276. // Input validation
  277. const validation = validate({
  278. email,
  279. password,
  280. name
  281. }, {
  282. email: {
  283. email: true,
  284. length: {
  285. maximum: 255
  286. }
  287. },
  288. password: {
  289. presence: {
  290. allowEmpty: false
  291. },
  292. length: {
  293. minimum: 6
  294. }
  295. },
  296. name: {
  297. presence: {
  298. allowEmpty: false
  299. },
  300. length: {
  301. minimum: 2,
  302. maximum: 255
  303. }
  304. },
  305. }, { format: 'flat' })
  306. if (validation && validation.length > 0) {
  307. throw new WIKI.Error.InputInvalid(validation[0])
  308. }
  309. // Check if email domain is whitelisted
  310. if (_.get(localStrg, 'domainWhitelist.v', []).length > 0) {
  311. const emailDomain = _.last(email.split('@'))
  312. if (!_.includes(localStrg.domainWhitelist.v, emailDomain)) {
  313. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  314. }
  315. }
  316. // Check if email already exists
  317. const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' })
  318. if (!usr) {
  319. // Create the account
  320. const newUsr = await WIKI.models.users.query().insert({
  321. provider: 'local',
  322. email,
  323. name,
  324. password,
  325. locale: 'en',
  326. defaultEditor: 'markdown',
  327. tfaIsActive: false,
  328. isSystem: false,
  329. isActive: true,
  330. isVerified: false
  331. })
  332. // Create verification token
  333. const verificationToken = await WIKI.models.userKeys.generateToken({
  334. kind: 'verify',
  335. userId: newUsr.id
  336. })
  337. // Send verification email
  338. await WIKI.mail.send({
  339. template: 'accountVerify',
  340. to: email,
  341. subject: 'Verify your account',
  342. data: {
  343. preheadertext: 'Verify your account in order to gain access to the wiki.',
  344. title: 'Verify your account',
  345. content: 'Click the button below in order to verify your account and gain access to the wiki.',
  346. buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,
  347. buttonText: 'Verify'
  348. },
  349. 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}`
  350. })
  351. return true
  352. } else {
  353. throw new WIKI.Error.AuthAccountAlreadyExists()
  354. }
  355. } else {
  356. throw new WIKI.Error.AuthRegistrationDisabled()
  357. }
  358. }
  359. }