Initial commit
This commit is contained in:
396
server/src/api/routes/AuthRoutes.js
Normal file
396
server/src/api/routes/AuthRoutes.js
Normal file
@@ -0,0 +1,396 @@
|
||||
import passport from 'passport'
|
||||
import credential from 'credential'
|
||||
import createError from 'http-errors'
|
||||
import config from 'config'
|
||||
import crypto from 'crypto'
|
||||
import urlSafeBase64 from 'urlsafe-base64'
|
||||
import util from 'util'
|
||||
import * as loginToken from './loginToken'
|
||||
import autoBind from 'auto-bind2'
|
||||
import url from 'url'
|
||||
|
||||
export class AuthRoutes {
|
||||
constructor(container) {
|
||||
const app = container.app
|
||||
|
||||
this.mq = container.mq
|
||||
this.db = container.db
|
||||
this.maxEmailTokenAgeInHours = config.get('email.maxEmailTokenAgeInHours')
|
||||
this.maxPasswordTokenAgeInHours = config.get('email.maxPasswordTokenAgeInHours')
|
||||
this.sendEmailDelayInSeconds = config.get('email.sendEmailDelayInSeconds')
|
||||
this.supportEmail = config.get('email.supportEmail')
|
||||
this.sendEmail = config.get('email.sendEmail')
|
||||
autoBind(this)
|
||||
app.route('/auth/login')
|
||||
// Used to login. Email must be confirmed.
|
||||
.post(this.login)
|
||||
// Used to logout
|
||||
.delete(passport.authenticate('bearer', { session: false }), this.logout)
|
||||
|
||||
// Send change email confirmation email
|
||||
app.route('/auth/email/send')
|
||||
.post(passport.authenticate('bearer', { session: false }), this.sendChangeEmailEmail)
|
||||
|
||||
// Confirm email address
|
||||
app.route('/auth/email/confirm')
|
||||
.post(this.confirmEmail)
|
||||
|
||||
// Change the logged in users password, leaving user still logged in
|
||||
app.route('/auth/password/change')
|
||||
.post(passport.authenticate('bearer', { session: false }), this.changePassword)
|
||||
|
||||
// Send a password reset email
|
||||
app.route('/auth/password/send')
|
||||
.post(this.sendPasswordResetEmail)
|
||||
|
||||
// Finish a password reset
|
||||
app.route('/auth/password/reset')
|
||||
.post(this.resetPassword)
|
||||
|
||||
// Indicate who the currently logged in user is
|
||||
app.route('/auth/who')
|
||||
.get(passport.authenticate('bearer', { session: false }), this.whoAmI)
|
||||
}
|
||||
|
||||
login(req, res, next) {
|
||||
const email = req.body.email
|
||||
const password = req.body.password
|
||||
|
||||
if (!email || !password) {
|
||||
return next(new createError.BadRequest('Must supply user name and password'))
|
||||
}
|
||||
|
||||
let User = this.db.User
|
||||
|
||||
// Lookup the user
|
||||
User.findOne({ email }).then((user) => {
|
||||
if (!user) {
|
||||
// NOTE: Don't return NotFound as that gives too much information away to hackers
|
||||
return Promise.reject(createError.BadRequest("Email or password incorrect"))
|
||||
} else if (user.emailToken || !user.passwordHash) {
|
||||
return Promise.reject(createError.Forbidden("Must confirm email and set password"))
|
||||
} else {
|
||||
let cr = credential()
|
||||
|
||||
return Promise.all([
|
||||
Promise.resolve(user),
|
||||
cr.verify(JSON.stringify(user.passwordHash), req.body.password)
|
||||
])
|
||||
}
|
||||
}).then((arr) => {
|
||||
const [user, isValid] = arr
|
||||
if (isValid) {
|
||||
user.loginToken = loginToken.pack(user._id.toString(), user.email)
|
||||
} else {
|
||||
user.loginToken = null // A bad login removes existing token for this user...
|
||||
}
|
||||
return user.save()
|
||||
}).then((savedUser) => {
|
||||
if (savedUser.loginToken) {
|
||||
res.set('Authorization', `Bearer ${savedUser.loginToken}`)
|
||||
res.json(savedUser.toClient())
|
||||
} else {
|
||||
return Promise.reject(createError.BadRequest('Email or password incorrect'))
|
||||
}
|
||||
}).catch((err) => {
|
||||
if (err instanceof createError.HttpError) {
|
||||
next(err)
|
||||
} else {
|
||||
next(createError.InternalServerError(`Unable to login. ${err ? err.message : ''}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logout(req, res, next) {
|
||||
let User = this.db.User
|
||||
|
||||
User.findById({ _id: req.user._id }).then((user) => {
|
||||
if (!user) {
|
||||
return next(createError.BadRequest())
|
||||
}
|
||||
|
||||
user.loginToken = null
|
||||
user.save().then((savedUser) => {
|
||||
res.json({})
|
||||
})
|
||||
}).catch((err) => {
|
||||
next(createError.InternalServerError(`Unable to login. ${err ? err.message : ''}`))
|
||||
})
|
||||
}
|
||||
|
||||
whoAmI(req, res, next) {
|
||||
res.json(req.user.toClient())
|
||||
}
|
||||
|
||||
sendChangeEmailEmail(req, res, next) {
|
||||
let existingEmail = req.body.existingEmail
|
||||
const newEmail = req.body.newEmail
|
||||
let User = this.db.User
|
||||
const role = req.user.role
|
||||
const isAdminOrExec = (role === 'executive' || role === 'administrator')
|
||||
|
||||
if (existingEmail) {
|
||||
if (!isAdminOrExec) {
|
||||
return next(createError.Forbidden('Only admins can resend change email to any user'))
|
||||
}
|
||||
} else {
|
||||
existingEmail = req.user.email
|
||||
}
|
||||
|
||||
let promiseArray = [User.findOne({ email: existingEmail })]
|
||||
|
||||
if (newEmail) {
|
||||
promiseArray.push(User.findOne({ email: newEmail }))
|
||||
}
|
||||
|
||||
Promise.all(promiseArray).then((arr) => {
|
||||
const [user, conflictingUser] = arr
|
||||
|
||||
if (!user) {
|
||||
return Promise.reject(createError.NotFound(`User with email '${existingEmail}' was not found`))
|
||||
} else if (conflictingUser) {
|
||||
return Promise.reject(createError.BadRequest(`A user with '${newEmail}' already exists`))
|
||||
} else if (!isAdminOrExec && user.emailToken && (new Date() - user.emailToken.created) < this.sendEmailDelayInSeconds) {
|
||||
return Promise.reject(createError.BadRequest('Cannot request email confirmation again so soon'))
|
||||
}
|
||||
|
||||
return Promise.all([Promise.resolve(user), util.promisify(crypto.randomBytes)(32)])
|
||||
}).then((arr) => {
|
||||
let [ user, buf ] = arr
|
||||
|
||||
user.emailToken = {
|
||||
value: urlSafeBase64.encode(buf),
|
||||
created: new Date()
|
||||
}
|
||||
|
||||
if (newEmail) {
|
||||
user.newEmail = newEmail
|
||||
}
|
||||
|
||||
return user.save()
|
||||
}).then((savedUser) => {
|
||||
const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
|
||||
const siteUrl = url.parse(req.headers.referer)
|
||||
let msgs = []
|
||||
|
||||
if (savedUser.newEmail) {
|
||||
msgs.push({
|
||||
toEmail: savedUser.email,
|
||||
templateName: 'changeEmailOld',
|
||||
templateData: {
|
||||
recipientFullName: userFullName,
|
||||
recipientNewEmail: savedUser.newEmail,
|
||||
supportEmail: this.supportEmail
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
msgs.push({
|
||||
toEmail: savedUser.newEmail || savedUser.email,
|
||||
templateName: 'changeEmailNew',
|
||||
templateData: {
|
||||
recipientFullName: userFullName,
|
||||
confirmEmailLink: `${siteUrl.protocol}//${siteUrl.host}/confirm-email?email-token%3D${savedUser.emailToken.value}`,
|
||||
supportEmail: this.supportEmail
|
||||
}
|
||||
})
|
||||
return this.sendEmail ? this.mq.request('dar-email', 'sendEmail', msgs) : Promise.resolve()
|
||||
}).then(() => {
|
||||
res.json({})
|
||||
}).catch((err) => {
|
||||
if (err instanceof createError.HttpError) {
|
||||
next(err)
|
||||
} else {
|
||||
next(createError.InternalServerError(`Unable to send change email email. ${err.message}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
confirmEmail(req, res, next) {
|
||||
const token = req.body.emailToken
|
||||
let User = this.db.User
|
||||
|
||||
if (!token) {
|
||||
return next(createError.BadRequest('Invalid request parameters'))
|
||||
}
|
||||
|
||||
User.findOne({ 'emailToken.value': token }).then((user) => {
|
||||
if (!user) {
|
||||
return Promise.reject(createError.BadRequest(`The token was not found`))
|
||||
}
|
||||
|
||||
// Token must not be too old
|
||||
const ageInHours = (new Date() - user.emailToken.created) / 3600000
|
||||
|
||||
if (ageInHours > this.maxEmailTokenAgeInHours) {
|
||||
return Promise.reject(createError.BadRequest(`Token has expired`))
|
||||
}
|
||||
|
||||
// Remove the email token & any login token as it will become invalid
|
||||
user.emailToken = undefined
|
||||
user.loginToken = undefined
|
||||
|
||||
// Switch in any new email now
|
||||
if (user.newEmail) {
|
||||
user.email = user.newEmail
|
||||
user.newEmail = undefined
|
||||
}
|
||||
|
||||
let promiseArray = [ Promise.resolve(user) ]
|
||||
|
||||
if (!user.passwordHash) {
|
||||
// User has no password, create reset token for them
|
||||
promiseArray.push(util.promisify(crypto.randomBytes)(32))
|
||||
}
|
||||
|
||||
return Promise.all(promiseArray)
|
||||
}).then((arr) => {
|
||||
let [ user, buf ] = arr
|
||||
|
||||
if (buf) {
|
||||
user.passwordToken = {
|
||||
value: urlSafeBase64.encode(buf),
|
||||
created: new Date()
|
||||
}
|
||||
}
|
||||
|
||||
return user.save()
|
||||
}).then((savedUser) => {
|
||||
let obj = {}
|
||||
|
||||
// Only because the user has sent us a valid email reset token can we respond with an password reset token
|
||||
if (savedUser.passwordToken) {
|
||||
obj.passwordToken = savedUser.passwordToken.value
|
||||
}
|
||||
|
||||
res.json(obj)
|
||||
}).catch((err) => {
|
||||
if (err instanceof createError.HttpError) {
|
||||
next(err)
|
||||
} else {
|
||||
next(createError.InternalServerError(`Unable to confirm set email token. ${err.message}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resetPassword(req, res, next) {
|
||||
const token = req.body.passwordToken
|
||||
const newPassword = req.body.newPassword
|
||||
let User = this.db.User
|
||||
let cr = credential()
|
||||
|
||||
if (!token || !newPassword) {
|
||||
return next(createError.BadRequest('Invalid request parameters'))
|
||||
}
|
||||
|
||||
User.findOne({ 'passwordToken.value': token }).then((user) => {
|
||||
if (!user) {
|
||||
return Promise.reject(createError.BadRequest(`The token was not found`))
|
||||
}
|
||||
|
||||
// Token must not be too old
|
||||
const ageInHours = (new Date() - user.passwordToken.created) / (3600 * 1000)
|
||||
|
||||
if (ageInHours > this.maxPasswordTokenAgeInHours) {
|
||||
return Promise.reject(createError.BadRequest(`Token has expired`))
|
||||
}
|
||||
|
||||
// Remove the password token & any login token
|
||||
user.passwordToken = undefined
|
||||
user.loginToken = undefined
|
||||
|
||||
return Promise.all([
|
||||
Promise.resolve(user),
|
||||
cr.hash(newPassword)
|
||||
])
|
||||
}).then((arr) => {
|
||||
const [user, json] = arr
|
||||
user.passwordHash = JSON.parse(json)
|
||||
return user.save()
|
||||
}).then((savedUser) => {
|
||||
res.json({})
|
||||
}).catch((err) => {
|
||||
if (err instanceof createError.HttpError) {
|
||||
next(err)
|
||||
} else {
|
||||
next(createError.InternalServerError(`Unable to confirm password reset token. ${err.message}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
changePassword(req, res, next) {
|
||||
let User = this.db.User
|
||||
let cr = credential()
|
||||
User.findById({ _id: req.user._id }).then((user) => {
|
||||
if (!user) {
|
||||
return next(createError.NotFound(`User ${req.user._id} not found`))
|
||||
}
|
||||
return Promise.all([
|
||||
Promise.resolve(user),
|
||||
cr.verify(JSON.stringify(user.passwordHash), req.body.oldPassword)
|
||||
])
|
||||
}).then((arr) => {
|
||||
const [user, ok] = arr
|
||||
return Promise.all([Promise.resolve(user), cr.hash(req.body.newPassword)])
|
||||
}).then((arr) => {
|
||||
const [user, obj] = arr
|
||||
user.passwordHash = JSON.parse(obj)
|
||||
return user.save()
|
||||
}).then((savedUser) => {
|
||||
res.json({})
|
||||
}).catch((err) => {
|
||||
return next(createError.InternalServerError(err.message))
|
||||
})
|
||||
}
|
||||
|
||||
sendPasswordResetEmail(req, res, next){
|
||||
const email = req.body.email
|
||||
let User = this.db.User
|
||||
|
||||
if (!email) {
|
||||
return next(createError.BadRequest('Invalid request parameters'))
|
||||
}
|
||||
|
||||
User.findOne({ email }).then((user) => {
|
||||
// User must exist their email must be confirmed
|
||||
if (!user || user.emailToken) {
|
||||
// Don't give away any information about why we rejected the request
|
||||
return Promise.reject(createError.BadRequest('Not a valid request'))
|
||||
} else if (user.passwordToken && (new Date() - user.emailToken.created) < this.sendEmailDelayInSeconds) {
|
||||
return Promise.reject(createError.BadRequest('Cannot request password reset so soon'))
|
||||
}
|
||||
|
||||
return Promise.all([Promise.resolve(user), util.promisify(crypto.randomBytes)(32)])
|
||||
}).then((arr) => {
|
||||
let [ user, buf ] = arr
|
||||
|
||||
user.passwordToken = {
|
||||
value: urlSafeBase64.encode(buf),
|
||||
created: new Date()
|
||||
}
|
||||
|
||||
return user.save()
|
||||
}).then((savedUser) => {
|
||||
const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
|
||||
const siteUrl = url.parse(req.headers.referer)
|
||||
const msg = {
|
||||
toEmail: savedUser.email,
|
||||
templateName: 'forgotPassword',
|
||||
templateData: {
|
||||
recipientFullName: userFullName,
|
||||
resetPasswordLink: `${siteUrl.protocol}//${siteUrl.host}/reset-password?password-token%3D${savedUser.passwordToken.value}`,
|
||||
supportEmail: this.supportEmail
|
||||
}
|
||||
}
|
||||
return this.sendEmail ? this.mq.request('dar-email', 'sendEmail', msg) : Promise.resolve()
|
||||
}).then(() => {
|
||||
res.json({})
|
||||
}).catch((err) => {
|
||||
if (err instanceof createError.HttpError) {
|
||||
next(err)
|
||||
} else {
|
||||
next(createError.InternalServerError(`Unable to send password reset email. ${err.message}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user