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.

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