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.

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