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.

290 lines
8.4 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 bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/
  9. /**
  10. * Users model
  11. */
  12. module.exports = class User extends Model {
  13. static get tableName() { return 'users' }
  14. static get jsonSchema () {
  15. return {
  16. type: 'object',
  17. required: ['email', 'name', 'provider'],
  18. properties: {
  19. id: {type: 'integer'},
  20. email: {type: 'string', format: 'email'},
  21. name: {type: 'string', minLength: 1, maxLength: 255},
  22. providerId: {type: 'number'},
  23. password: {type: 'string'},
  24. role: {type: 'string', enum: ['admin', 'guest', 'user']},
  25. tfaIsActive: {type: 'boolean', default: false},
  26. tfaSecret: {type: 'string'},
  27. jobTitle: {type: 'string'},
  28. location: {type: 'string'},
  29. pictureUrl: {type: 'string'},
  30. createdAt: {type: 'string'},
  31. updatedAt: {type: 'string'}
  32. }
  33. }
  34. }
  35. static get relationMappings() {
  36. return {
  37. groups: {
  38. relation: Model.ManyToManyRelation,
  39. modelClass: require('./groups'),
  40. join: {
  41. from: 'users.id',
  42. through: {
  43. from: 'userGroups.userId',
  44. to: 'userGroups.groupId'
  45. },
  46. to: 'groups.id'
  47. }
  48. },
  49. provider: {
  50. relation: Model.BelongsToOneRelation,
  51. modelClass: require('./authentication'),
  52. join: {
  53. from: 'users.providerKey',
  54. to: 'authentication.key'
  55. }
  56. },
  57. defaultEditor: {
  58. relation: Model.BelongsToOneRelation,
  59. modelClass: require('./editors'),
  60. join: {
  61. from: 'users.editorKey',
  62. to: 'editors.key'
  63. }
  64. },
  65. locale: {
  66. relation: Model.BelongsToOneRelation,
  67. modelClass: require('./locales'),
  68. join: {
  69. from: 'users.localeCode',
  70. to: 'locales.code'
  71. }
  72. }
  73. }
  74. }
  75. async $beforeUpdate(opt, context) {
  76. await super.$beforeUpdate(opt, context)
  77. this.updatedAt = new Date().toISOString()
  78. if (!(opt.patch && this.password === undefined)) {
  79. await this.generateHash()
  80. }
  81. }
  82. async $beforeInsert(context) {
  83. await super.$beforeInsert(context)
  84. this.createdAt = new Date().toISOString()
  85. this.updatedAt = new Date().toISOString()
  86. await this.generateHash()
  87. }
  88. async generateHash() {
  89. if (this.password) {
  90. if (bcryptRegexp.test(this.password)) { return }
  91. this.password = await bcrypt.hash(this.password, 12)
  92. }
  93. }
  94. async verifyPassword(pwd) {
  95. if (await bcrypt.compare(pwd, this.password) === true) {
  96. return true
  97. } else {
  98. throw new WIKI.Error.AuthLoginFailed()
  99. }
  100. }
  101. async enableTFA() {
  102. let tfaInfo = tfa.generateSecret({
  103. name: WIKI.config.site.title
  104. })
  105. return this.$query.patch({
  106. tfaIsActive: true,
  107. tfaSecret: tfaInfo.secret
  108. })
  109. }
  110. async disableTFA() {
  111. return this.$query.patch({
  112. tfaIsActive: false,
  113. tfaSecret: ''
  114. })
  115. }
  116. async verifyTFA(code) {
  117. let result = tfa.verifyToken(this.tfaSecret, code)
  118. return (result && _.has(result, 'delta') && result.delta === 0)
  119. }
  120. static async processProfile(profile) {
  121. let primaryEmail = ''
  122. if (_.isArray(profile.emails)) {
  123. let e = _.find(profile.emails, ['primary', true])
  124. primaryEmail = (e) ? e.value : _.first(profile.emails).value
  125. } else if (_.isString(profile.email) && profile.email.length > 5) {
  126. primaryEmail = profile.email
  127. } else if (_.isString(profile.mail) && profile.mail.length > 5) {
  128. primaryEmail = profile.mail
  129. } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
  130. primaryEmail = profile.user.email
  131. } else {
  132. return Promise.reject(new Error(WIKI.lang.t('auth:errors.invaliduseremail')))
  133. }
  134. profile.provider = _.lowerCase(profile.provider)
  135. primaryEmail = _.toLower(primaryEmail)
  136. let user = await WIKI.models.users.query().findOne({
  137. email: primaryEmail,
  138. provider: profile.provider
  139. })
  140. if (user) {
  141. user.$query().patchAdnFetch({
  142. email: primaryEmail,
  143. provider: profile.provider,
  144. providerId: profile.id,
  145. name: profile.displayName || _.split(primaryEmail, '@')[0]
  146. })
  147. } else {
  148. user = await WIKI.models.users.query().insertAndFetch({
  149. email: primaryEmail,
  150. provider: profile.provider,
  151. providerId: profile.id,
  152. name: profile.displayName || _.split(primaryEmail, '@')[0]
  153. })
  154. }
  155. // Handle unregistered accounts
  156. // if (!user && profile.provider !== 'local' && (WIKI.config.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
  157. // let nUsr = {
  158. // email: primaryEmail,
  159. // provider: profile.provider,
  160. // providerId: profile.id,
  161. // password: '',
  162. // name: profile.displayName || profile.name || profile.cn,
  163. // rights: [{
  164. // role: 'read',
  165. // path: '/',
  166. // exact: false,
  167. // deny: false
  168. // }]
  169. // }
  170. // return WIKI.models.users.query().insert(nUsr)
  171. // }
  172. return user
  173. }
  174. static async login (opts, context) {
  175. if (_.has(WIKI.auth.strategies, opts.strategy)) {
  176. _.set(context.req, 'body.email', opts.username)
  177. _.set(context.req, 'body.password', opts.password)
  178. // Authenticate
  179. return new Promise((resolve, reject) => {
  180. WIKI.auth.passport.authenticate(opts.strategy, { session: false }, async (err, user, info) => {
  181. if (err) { return reject(err) }
  182. if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
  183. // Is 2FA required?
  184. if (user.tfaIsActive) {
  185. try {
  186. let loginToken = await securityHelper.generateToken(32)
  187. await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
  188. return resolve({
  189. tfaRequired: true,
  190. tfaLoginToken: loginToken
  191. })
  192. } catch (err) {
  193. WIKI.logger.warn(err)
  194. return reject(new WIKI.Error.AuthGenericError())
  195. }
  196. } else {
  197. // No 2FA, log in user
  198. return context.req.logIn(user, { session: false }, async err => {
  199. if (err) { return reject(err) }
  200. const jwtToken = await WIKI.models.users.refreshToken(user)
  201. resolve({
  202. jwt: jwtToken.token,
  203. tfaRequired: false
  204. })
  205. })
  206. }
  207. })(context.req, context.res, () => {})
  208. })
  209. } else {
  210. throw new WIKI.Error.AuthProviderInvalid()
  211. }
  212. }
  213. static async refreshToken(user) {
  214. if (_.isSafeInteger(user)) {
  215. user = await WIKI.models.users.query().findById(user)
  216. if (!user) {
  217. WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)
  218. throw new WIKI.Error.AuthGenericError()
  219. }
  220. }
  221. return {
  222. token: jwt.sign({
  223. id: user.id,
  224. email: user.email,
  225. name: user.name,
  226. pictureUrl: user.pictureUrl,
  227. timezone: user.timezone,
  228. localeCode: user.localeCode,
  229. defaultEditor: user.defaultEditor,
  230. permissions: ['manage:system']
  231. }, WIKI.config.sessionSecret, {
  232. expiresIn: '30m',
  233. audience: 'urn:wiki.js', // TODO: use value from admin
  234. issuer: 'urn:wiki.js'
  235. }),
  236. user
  237. }
  238. }
  239. static async loginTFA(opts, context) {
  240. if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
  241. let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
  242. if (result) {
  243. let userId = _.toSafeInteger(result)
  244. if (userId && userId > 0) {
  245. let user = await WIKI.models.users.query().findById(userId)
  246. if (user && user.verifyTFA(opts.securityCode)) {
  247. return Promise.fromCallback(clb => {
  248. context.req.logIn(user, clb)
  249. }).return({
  250. succeeded: true,
  251. message: 'Login Successful'
  252. }).catch(err => {
  253. WIKI.logger.warn(err)
  254. throw new WIKI.Error.AuthGenericError()
  255. })
  256. } else {
  257. throw new WIKI.Error.AuthTFAFailed()
  258. }
  259. }
  260. }
  261. }
  262. throw new WIKI.Error.AuthTFAInvalid()
  263. }
  264. }