UNPKG

@krypton-org/krypton-auth

Version:

Express authentication middleware, using GraphQL and JSON Web Tokens.

419 lines 17.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /** * Service logic for user management. * @module controllers/UserController */ const ejs_1 = __importDefault(require("ejs")); const agenda_1 = __importDefault(require("../agenda/agenda")); const config_1 = __importDefault(require("../config")); const TokenGenerator_1 = __importDefault(require("../crypto/TokenGenerator")); const ErrorTypes_1 = require("../error/ErrorTypes"); const SessionModel_1 = __importDefault(require("../model/SessionModel")); const UserModel_1 = __importDefault(require("../model/UserModel")); const TOKEN_LENGTH = 64; const DELAY_TO_CHANGE_PASSWORD_IN_MINUTS = 60; /** * Return true if user issuing the request is logged in. * @param {Request} req * @returns {boolean} user is logged in */ const isUserLoggedIn = (req) => req.user !== undefined; /** * Returns true if a notificaiton should be sent to client with the preview link in case of a mock email. * @param {Request} req * @returns {boolean} true if a notificaiton should be sent to client with the preview link in case of a mock email */ const isMockEmailAndClientCanReceivePreview = (req) => { return !config_1.default.nodemailerConfig && config_1.default.graphiql && req.cookies.clientId; }; /** * Send confirmation email to `user`. * @param {any} user - user * @param {string} confirmationToken * @param {string} host - Service public address * @returns {void} */ const sendConfirmationEmail = (user, confirmationToken, host, clientId) => { agenda_1.default.now('email', { clientId, locals: { link: host + '/user/email/confirmation?token=' + confirmationToken, user, }, recipient: user.email, subject: 'Activate your account', template: config_1.default.verifyEmailTemplate, }); }; /** * Returns the user data of the logged in user. * @throws {UnauthorizedError} User does not exist * @param {Request} req * @param {Response} res * @returns {Promise<{ user: any }>} Promise to the user data */ exports.getUser = (req, res) => __awaiter(void 0, void 0, void 0, function* () { try { return yield UserModel_1.default.findById(req.user._id); } catch (err) { throw new ErrorTypes_1.UnauthorizedError('User not found, please log in.'); } }); /** * Returns true email address not already taken by another user. * @param {string} email * @returns {Promise<boolean>} Promise returning true if`isAvailable` */ exports.checkEmailAvailable = (email) => __awaiter(void 0, void 0, void 0, function* () { const emailExists = yield UserModel_1.default.userExists({ email }); return !emailExists; }); /** * Verifying link clicked by users in the account verification email. * @throws {UnauthorizedError} User does not exist * @param {Request} req * @param {Response} res * @param {NextFunction} next * @renders notification page informing users of the operation success. */ exports.confirmEmail = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { const notifications = []; const token = req.query.token; try { const exists = yield UserModel_1.default.userExists({ verificationToken: token }); if (!exists) { throw new ErrorTypes_1.UnauthorizedError('This link is no longer valid.'); } const user = yield UserModel_1.default.getUser({ verificationToken: token }); yield UserModel_1.default.updateUser({ email: user.email }, { verificationToken: null, email_verified: true }); notifications.push({ type: 'success', message: 'You are now verified.' }); } catch (err) { notifications.push({ type: 'error', message: 'This link is not valid.' }); } finally { ejs_1.default.renderFile(config_1.default.notificationPageTemplate, { notifications }, { notifications }, (err, html) => { if (err) { next(err); } else { res.send(html); } }); } }); /** * Create a new user. * @throws {UsernameAlreadyExistsError} * @throws {EmailAlreadyExistsError} * @throws {UserValidationError} * @param {any} user * @param {Request} req * @returns {Promise<boolean>} Promise returning true on request success */ exports.createUser = (user, req) => __awaiter(void 0, void 0, void 0, function* () { user.verificationToken = TokenGenerator_1.default(TOKEN_LENGTH); if (!user.password) { throw new ErrorTypes_1.UserValidationError('Please provide a password.'); } if (user.password.length < 8) { throw new ErrorTypes_1.UserValidationError('The password must contain at least 8 characters.'); } try { yield UserModel_1.default.createUser(user); let clientId; if (isMockEmailAndClientCanReceivePreview(req)) { clientId = req.cookies.clientId; } sendConfirmationEmail(user, user.verificationToken, config_1.default.getRouterAddress(req), clientId); return true; } catch (err) { if (err.message.includes('email') && err.message.includes('duplicate key')) { throw new ErrorTypes_1.EmailAlreadyExistsError('Email already exists.'); } throw new ErrorTypes_1.UserValidationError(err.message.replace('user validation failed: email: Path ', '')); } }); /** * Resend an account verification email to logged in user. * @throws {UnauthorizedError} * @throws {EmailAlreadyConfirmedError} * @param {Request} req * @returns {Promise<boolean>} Promise returning true on request success */ exports.resendConfirmationEmail = (req) => __awaiter(void 0, void 0, void 0, function* () { if (!isUserLoggedIn(req)) { throw new ErrorTypes_1.UnauthorizedError('Please login.'); } const user = yield UserModel_1.default.getUser({ _id: req.user._id }); if (user.email_verified) { throw new ErrorTypes_1.EmailAlreadyConfirmedError('Your email adress has already been confirmed.'); } else { let clientId; if (isMockEmailAndClientCanReceivePreview(req)) { clientId = req.cookies.clientId; } sendConfirmationEmail(req.user, user.verificationToken, config_1.default.getRouterAddress(req), clientId); } return true; }); /** * Updating user password from the password recovery form. * @throws {UnauthorizedError} * @throws {UpdatePasswordTooLateError} * @param {string} password - new password * @param {string} passwordRecoveryToken - token guaranteeing user identity * @returns {Promise<{ notifications: Notification[] }>} Promise to the notifications of success or failure */ exports.recoverPassword = (password, passwordRecoveryToken) => __awaiter(void 0, void 0, void 0, function* () { const notifications = []; if (password.length < 8) { throw new ErrorTypes_1.UserValidationError('The password must contain at least 8 characters.'); } const userExists = yield UserModel_1.default.userExists({ passwordRecoveryToken }); if (!userExists) { throw new ErrorTypes_1.UnauthorizedError('Unvalid token.'); } const user = yield UserModel_1.default.getUser({ passwordRecoveryToken }); const resetDate = new Date(user.passwordRecoveryRequestDate); const actualDate = new Date(); const diff = Math.abs(actualDate.getTime() - resetDate.getTime()); const minutes = Math.floor(diff / 1000 / 60); if (minutes >= DELAY_TO_CHANGE_PASSWORD_IN_MINUTS) { throw new ErrorTypes_1.UpdatePasswordTooLateError('This link has expired, please ask a new one.'); } yield UserModel_1.default.updateUser({ _id: user.id }, { password, passwordRecoveryToken: undefined, passwordRecoveryRequestDate: undefined }); notifications.push({ type: 'success', message: 'Your password is updated.' }); return { notifications }; }); /** * Update the different user fields of logged in user. * @throws {UnauthorizedError} * @throws {TokenEncryptionError} * @throws {WrongPasswordError} * @throws {UsernameAlreadyExistsError} * @throws {EmailAlreadyExistsError} * @throws {UserValidationError} * @param {any} userUpdates - fields to update * @param {Request} req * @param {Response} res * @returns {Promise<{ user: any; notifications: Notification[] }>} Promise to the new user data and the notifications of success or failure */ exports.updateUser = (userUpdates, req, res) => __awaiter(void 0, void 0, void 0, function* () { if (!isUserLoggedIn(req) || !(yield SessionModel_1.default.isValid(req.user._id, req.cookies.refreshToken))) { res.status(401); throw new ErrorTypes_1.UnauthorizedError('Please login.'); } if (userUpdates.password && userUpdates.password !== userUpdates.previousPassword) { const isValid = yield UserModel_1.default.isPasswordValid({ email: req.user.email }, userUpdates.previousPassword); if (!isValid) { throw new ErrorTypes_1.WrongPasswordError('Your previous password is wrong.'); } if (userUpdates.password.length < 8) { throw new ErrorTypes_1.UserValidationError('The password must contain at least 8 characters.'); } delete userUpdates.previousPassword; } try { let isEmailVerified = true; if (req.user.email_verified && userUpdates.email) { userUpdates.verificationToken = TokenGenerator_1.default(TOKEN_LENGTH); userUpdates.email_verified = false; isEmailVerified = false; } yield UserModel_1.default.updateUser({ _id: req.user._id }, userUpdates); req.user = yield UserModel_1.default.getUserNonInternalFields({ _id: req.user._id }); if (!isEmailVerified) { let clientId; if (isMockEmailAndClientCanReceivePreview(req)) { clientId = req.cookies.clientId; } sendConfirmationEmail(req.user, userUpdates.verificationToken, config_1.default.getRouterAddress(req), clientId); } const payload = yield UserModel_1.default.refreshAuthToken({ _id: req.user._id }, config_1.default.privateKey); const { refreshToken, expiryDate } = yield SessionModel_1.default.updateSession(req.user._id, req.cookies.refreshToken); const params = { httpOnly: true, expires: expiryDate }; if (config_1.default.host) { params.domain = '.' + config_1.default.getDomainAddress(); } res.cookie('refreshToken', refreshToken, params); return payload; } catch (err) { if (err.message.includes('email') && err.message.includes('duplicate key')) { throw new ErrorTypes_1.EmailAlreadyExistsError('Email already exists.'); } throw new ErrorTypes_1.UserValidationError(err.message.replace('user validation failed: email: ', '')); } }); /** * Delete logged in user. * @throws {UnauthorizedError} * @throws {WrongPasswordError} * @param {string} password * @param {Request} req * @returns {Promise<boolean>} */ exports.deleteUser = (password, req) => __awaiter(void 0, void 0, void 0, function* () { if (!isUserLoggedIn(req)) { throw new ErrorTypes_1.UnauthorizedError('Please login.'); } const isValid = yield UserModel_1.default.isPasswordValid({ _id: req.user._id }, password); if (!isValid) { throw new ErrorTypes_1.WrongPasswordError('You entered a wrong password.'); } yield UserModel_1.default.removeUser({ _id: req.user._id }); return true; }); /** * User log-out. * @throws {UnauthorizedError} * @returns {Promise<boolean>} */ exports.logout = (req) => __awaiter(void 0, void 0, void 0, function* () { const { user, session } = yield SessionModel_1.default.getUserAndSessionFromRefreshToken(req.cookies.refreshToken); if (user && session) { yield SessionModel_1.default.removeSession(req.user._id, req.cookies.refreshToken); } else { throw new ErrorTypes_1.UnauthorizedError('Please login.'); } return true; }); /** * User log-in. * @throws {UserNotFoundError} * @throws {TokenEncryptionError} * @param {string} email * @param {string} password * @param {Request} req * @param {Response} res * @returns {Promise<{ token: string; user: any }>} Promise to the user token and user data. */ exports.login = (email, password, req, res) => __awaiter(void 0, void 0, void 0, function* () { let payload; const emailExists = yield UserModel_1.default.userExists({ email }); if (emailExists) { payload = yield UserModel_1.default.sign({ email }, password, config_1.default.privateKey); } else { throw new ErrorTypes_1.UserNotFoundError('Wrong credentials.'); } if (req.cookies.refreshToken) { yield SessionModel_1.default.removeSession(payload.user._id, req.cookies.refreshToken); } yield SessionModel_1.default.removeOutdatedSessions(payload.user._id); const { refreshToken, expiryDate } = yield SessionModel_1.default.createSession(payload.user._id); const params = { httpOnly: true, expires: expiryDate }; if (config_1.default.host) { params.domain = '.' + config_1.default.getDomainAddress(); } res.cookie('refreshToken', refreshToken, params); return payload; }); /** * Send password recovery email, when a user has lost his password. * @throws {AlreadyLoggedInError} * @param {string} email * @param {Request} req * @returns {Promise<boolean} Returns true */ exports.sendPasswordRecoveryEmail = (email, req) => __awaiter(void 0, void 0, void 0, function* () { if (req.user !== undefined) { throw new ErrorTypes_1.AlreadyLoggedInError('Oups, you are already logged in.'); } const exists = yield UserModel_1.default.userExists({ email }); if (!exists) { return true; } const passwordRecoveryToken = TokenGenerator_1.default(TOKEN_LENGTH); const passwordRecoveryRequestDate = new Date(); yield UserModel_1.default.updateUser({ email }, { passwordRecoveryToken, passwordRecoveryRequestDate }); const user = yield UserModel_1.default.getUser({ email }); const host = config_1.default.getRouterAddress(req); let clientId; if (isMockEmailAndClientCanReceivePreview(req)) { clientId = req.cookies.clientId; } agenda_1.default.now('email', { clientId, locals: { link: host + '/form/reset/password?token=' + passwordRecoveryToken, user, }, recipient: email, subject: 'Password Recovery', template: config_1.default.resetPasswordEmailTemplate, }); return true; }); /** * Send an HTML page with the password reset form. * @param {Request} req * @param {Response} res * @param {NextFunction} next * @returns an HTML page with the password reset form */ exports.resetPasswordForm = (req, res, next) => { const notifications = []; if (req.user) { notifications.push({ type: 'error', message: 'Oups, you are already logged in.' }); res.json({ notifications }); return; } const host = config_1.default.getRouterAddress(req); const locals = { link: host, token: req.query.token, }; ejs_1.default.renderFile(config_1.default.resetPasswordFormTemplate, locals, {}, (err, html) => { if (err) { next(err); } else { res.send(html); } }); }; /** * Refresh the auth token and the refresh token. This last one is set in an httpOnly cookie. * @throws {UnauthorizedError} * @throws {TokenEncryptionError} * @param {Request} req * @param {Response} res * @returns {Promise<{ token: string; expiryDate: Date }>} Promise to the new authentication token and its expiry date. */ exports.refreshTokens = (req, res) => __awaiter(void 0, void 0, void 0, function* () { const { user, session } = yield SessionModel_1.default.getUserAndSessionFromRefreshToken(req.cookies.refreshToken); const now = new Date(); if (user && session && now.getTime() < session.expiryDate) { const payload = yield UserModel_1.default.refreshAuthToken({ _id: user._id }, config_1.default.privateKey); const { refreshToken, expiryDate } = yield SessionModel_1.default.updateSession(user._id, session.refreshToken); const params = { httpOnly: true, expires: expiryDate }; if (config_1.default.host) { params.domain = '.' + config_1.default.getDomainAddress(); } res.cookie('refreshToken', refreshToken, params); return payload; } else { throw new ErrorTypes_1.UnauthorizedError('Please login.'); } }); //# sourceMappingURL=UserController.js.map