UNPKG

pallies

Version:

Robust user management for Hapi

244 lines (192 loc) 7.19 kB
'use strict'; const Boom = require('@hapi/boom'); const Schmervice = require('@hapipal/schmervice'); const Argon2 = require('argon2'); const Jwt = require('@hapi/jwt'); const Csprng = require('csprng'); const Moment = require('moment'); const Hashids = require('hashids/cjs'); const hashids = new Hashids('SALTY LAD', 16); /** * @class AuthService */ class AuthService extends Schmervice.Service { initialize() { this.PallieService = this.server.services().pallieService; this.TokenService = this.server.services().tokenService; } /** * Register a new user * @async * @param {object} user - The new user object * @param {object} [txn] - An instance of a Knex transaction * @returns {User} The newly created User */ async register(user, txn) { try { return await this.PallieService.create(user, txn); } catch (err) { throw Boom.badRequest(`${user.username} is already registered`); } } /** * Log a user in * @async * @param {string} username - The user's username * @param {string} password - The user's password * @returns {object} A token object with a access and refresh tokens, and the token type */ async login(username, password) { const { pallieService, tokenService } = this.server.services(); const user = await pallieService.getByUsername(username); if (!user) { throw Boom.unauthorized('Username or password is incorrect'); } const isValidPassword = await this.verifyPassword(user, password); if (!isValidPassword) { throw Boom.unauthorized('Username or password is invalid'); } const accessToken = await tokenService.createAccessToken(user); const refreshToken = await tokenService.createRefreshToken(user); return { accessToken, refreshToken }; } /** * Log a user out * @async * @param {object} credentials - The hapi credentials object */ async logout(credentials) { const user = await this.PallieService.getById(credentials.id); this.TokenService.clearRefreshTokens(user); } /** * Verify a user's password * @async * @param {object} user - The user's full name * @param {string} user.username - The user's username * @param {string} password - The user's password * @returns {bool} */ async verifyPassword({ username }, password) { const user = await this.PallieService.getByUsername(username); return await Argon2.verify(user.password.toString(), password); } /** * Validate a request's bearer token * @param {string} token - A JSON web token * @param {object} auth - The hapi auth object * @returns {object} */ validate(token, auth) { if (!token) { return { isValid: false }; } try { Jwt.token.verify( Jwt.token.decode(auth.token), { key: this.options.tokenSecret, algorithm: 'HS256' } ); return { isValid: true, credentials: token }; } catch (err) { return { isValid: false }; } } /** * Update user's authentication using refresh token * @async * @param {object} token - A refresh token * @returns {object} A token object with a access and refresh tokens, and the token type */ async reauthorize(token, txn) { if (!token) { throw Boom.badRequest('No token passed for reauthorization'); } return await this.TokenService.validateRefreshToken(token, txn); } /** * Get a user invite token * @async * @param {User} user - The new user object * @param {bool} force - If true will remove an existing user and send a new invite * @param {object} [txn] - An instance of a Knex transaction * @returns {User} The newly created user */ async invite(user, force, txn) { const { username } = user; const existingUser = await this.PallieService.getByUsername(username, txn); if (existingUser && force) { // For use with "Resend invite" await this.PallieService.remove(existingUser.id, txn); } else if (existingUser && !force) { throw Boom.badRequest(`${username} is already registered`); } // Register new user with temporary password const tempPassword = Csprng(128, 32); const newUser = await this.register({ ...user, password: tempPassword }, txn); await this.TokenService.createForgotPasswordToken(newUser); return { hash: hashids.encode(newUser.id), token: newUser.forgotPasswordToken }; } /** * Get a forgot password token * @async * @param {string} username - The user's username * @param {object} [txn] - An instance of a Knex transaction * @returns {User} A user with a forgot password token */ async forgotPassword(username, txn) { const user = await this.PallieService.getByUsername(username, txn); if (!user) { throw Boom.badRequest(`${username} does not exist`); } await this.TokenService.createForgotPasswordToken(user); return { hash: hashids.encode(user.id), token: user.forgotPasswordToken }; } /** * Reset an existing user's password or set a new user's password * @async * @param {object} credentials - The hapi credentials object * @param {string} [oldPassword] - The user's old password, if resetting * @param {string} [userHash] - The user hash from an invite email * @param {string} forgotPasswordToken - Forgot password token from email * @param {string} newPassword - The user's new password * @param {object} [txn] - An instance of a Knex transaction */ async resetPassword(credentials, oldPassword, userHash, forgotPasswordToken, newPassword, txn) { if (oldPassword) { const user = await this.PallieService.getById(credentials.id); const isValidPassword = await this.verifyPassword(user, oldPassword); if (!isValidPassword) { throw Boom.unauthorized('Password is invalid'); } return await this.TokenService.resetPassword(user, newPassword, txn); } const user = await this.PallieService.getById(hashids.decode(userHash)[0]); const expiration = Moment(user.forgotPasswordExpiresAt); if (!userHash || !forgotPasswordToken) { throw Boom.badRequest('Invalid request'); } if (expiration.isBefore(Moment().format())) { throw Boom.badRequest('Token has expired'); } if (forgotPasswordToken !== user.forgotPasswordToken) { throw Boom.badRequest('Invalid token'); } return await this.TokenService.resetPassword(user, newPassword, txn); } } module.exports = AuthService;