From eb5de2c7523ac166ca933bff83ef1e87274f3478 Mon Sep 17 00:00:00 2001 From: Hagop Jamkojian Date: Mon, 14 Sep 2020 23:25:22 +0200 Subject: [PATCH] Add type to token payloads closes #28 --- src/config/passport.js | 4 +++ src/services/token.service.js | 9 +++--- tests/fixtures/token.fixture.js | 5 ++-- tests/integration/auth.test.js | 49 +++++++++++++++++++++------------ 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/config/passport.js b/src/config/passport.js index 6084ac72..63efeaf6 100644 --- a/src/config/passport.js +++ b/src/config/passport.js @@ -1,5 +1,6 @@ const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt'); const config = require('./config'); +const { tokenTypes } = require('./tokens'); const { User } = require('../models'); const jwtOptions = { @@ -9,6 +10,9 @@ const jwtOptions = { const jwtVerify = async (payload, done) => { try { + if (payload.type !== tokenTypes.ACCESS) { + throw new Error('Invalid token type'); + } const user = await User.findById(payload.sub); if (!user) { return done(null, false); diff --git a/src/services/token.service.js b/src/services/token.service.js index b212dc7e..f8c207af 100644 --- a/src/services/token.service.js +++ b/src/services/token.service.js @@ -14,11 +14,12 @@ const { tokenTypes } = require('../config/tokens'); * @param {string} [secret] * @returns {string} */ -const generateToken = (userId, expires, secret = config.jwt.secret) => { +const generateToken = (userId, expires, type, secret = config.jwt.secret) => { const payload = { sub: userId, iat: moment().unix(), exp: expires.unix(), + type, }; return jwt.sign(payload, secret); }; @@ -65,10 +66,10 @@ const verifyToken = async (token, type) => { */ const generateAuthTokens = async (user) => { const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes'); - const accessToken = generateToken(user.id, accessTokenExpires); + const accessToken = generateToken(user.id, accessTokenExpires, tokenTypes.ACCESS); const refreshTokenExpires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = generateToken(user.id, refreshTokenExpires); + const refreshToken = generateToken(user.id, refreshTokenExpires, tokenTypes.REFRESH); await saveToken(refreshToken, user.id, refreshTokenExpires, tokenTypes.REFRESH); return { @@ -94,7 +95,7 @@ const generateResetPasswordToken = async (email) => { throw new ApiError(httpStatus.NOT_FOUND, 'No users found with this email'); } const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes'); - const resetPasswordToken = generateToken(user.id, expires); + const resetPasswordToken = generateToken(user.id, expires, tokenTypes.RESET_PASSWORD); await saveToken(resetPasswordToken, user.id, expires, tokenTypes.RESET_PASSWORD); return resetPasswordToken; }; diff --git a/tests/fixtures/token.fixture.js b/tests/fixtures/token.fixture.js index 9362a3e0..4fe7a209 100644 --- a/tests/fixtures/token.fixture.js +++ b/tests/fixtures/token.fixture.js @@ -1,11 +1,12 @@ const moment = require('moment'); const config = require('../../src/config/config'); +const { tokenTypes } = require('../../src/config/tokens'); const tokenService = require('../../src/services/token.service'); const { userOne, admin } = require('./user.fixture'); const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes'); -const userOneAccessToken = tokenService.generateToken(userOne._id, accessTokenExpires); -const adminAccessToken = tokenService.generateToken(admin._id, accessTokenExpires); +const userOneAccessToken = tokenService.generateToken(userOne._id, accessTokenExpires, tokenTypes.ACCESS); +const adminAccessToken = tokenService.generateToken(admin._id, accessTokenExpires, tokenTypes.ACCESS); module.exports = { userOneAccessToken, diff --git a/tests/integration/auth.test.js b/tests/integration/auth.test.js index c052ad53..b06ea178 100644 --- a/tests/integration/auth.test.js +++ b/tests/integration/auth.test.js @@ -127,7 +127,7 @@ describe('Auth routes', () => { test('should return 204 if refresh token is valid', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = tokenService.generateToken(userOne._id, expires); + const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH); await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH); await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NO_CONTENT); @@ -143,7 +143,7 @@ describe('Auth routes', () => { test('should return 404 error if refresh token is not found in the database', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = tokenService.generateToken(userOne._id, expires); + const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH); await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NOT_FOUND); }); @@ -151,7 +151,7 @@ describe('Auth routes', () => { test('should return 404 error if refresh token is blacklisted', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = tokenService.generateToken(userOne._id, expires); + const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH); await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH, true); await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NOT_FOUND); @@ -162,7 +162,7 @@ describe('Auth routes', () => { test('should return 200 and new auth tokens if refresh token is valid', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = tokenService.generateToken(userOne._id, expires); + const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH); await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH); const res = await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.OK); @@ -186,7 +186,7 @@ describe('Auth routes', () => { test('should return 401 error if refresh token is signed using an invalid secret', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = tokenService.generateToken(userOne._id, expires, 'invalidSecret'); + const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH, 'invalidSecret'); await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH); await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED); @@ -195,7 +195,7 @@ describe('Auth routes', () => { test('should return 401 error if refresh token is not found in the database', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = tokenService.generateToken(userOne._id, expires); + const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH); await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED); }); @@ -203,7 +203,7 @@ describe('Auth routes', () => { test('should return 401 error if refresh token is blacklisted', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = tokenService.generateToken(userOne._id, expires); + const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH); await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH, true); await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED); @@ -220,7 +220,7 @@ describe('Auth routes', () => { test('should return 401 error if user is not found', async () => { const expires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = tokenService.generateToken(userOne._id, expires); + const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH); await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH); await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED); @@ -259,7 +259,7 @@ describe('Auth routes', () => { test('should return 204 and reset the password', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes'); - const resetPasswordToken = tokenService.generateToken(userOne._id, expires); + const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD); await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD); await request(app) @@ -285,7 +285,7 @@ describe('Auth routes', () => { test('should return 401 if reset password token is blacklisted', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes'); - const resetPasswordToken = tokenService.generateToken(userOne._id, expires); + const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD); await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD, true); await request(app) @@ -298,7 +298,7 @@ describe('Auth routes', () => { test('should return 401 if reset password token is expired', async () => { await insertUsers([userOne]); const expires = moment().subtract(1, 'minutes'); - const resetPasswordToken = tokenService.generateToken(userOne._id, expires); + const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD); await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD); await request(app) @@ -310,7 +310,7 @@ describe('Auth routes', () => { test('should return 401 if user is not found', async () => { const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes'); - const resetPasswordToken = tokenService.generateToken(userOne._id, expires); + const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD); await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD); await request(app) @@ -323,7 +323,7 @@ describe('Auth routes', () => { test('should return 400 if password is missing or invalid', async () => { await insertUsers([userOne]); const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes'); - const resetPasswordToken = tokenService.generateToken(userOne._id, expires); + const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD); await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD); await request(app).post('/v1/auth/reset-password').query({ token: resetPasswordToken }).expect(httpStatus.BAD_REQUEST); @@ -387,10 +387,25 @@ describe('Auth middleware', () => { ); }); + test('should call next with unauthorized error if the token is not an access token', async () => { + await insertUsers([userOne]); + const expires = moment().add(config.jwt.accessExpirationMinutes, 'minutes'); + const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH); + const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${refreshToken}` } }); + const next = jest.fn(); + + await auth()(req, httpMocks.createResponse(), next); + + expect(next).toHaveBeenCalledWith(expect.any(ApiError)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' }) + ); + }); + test('should call next with unauthorized error if access token is generated with an invalid secret', async () => { await insertUsers([userOne]); - const tokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes'); - const accessToken = tokenService.generateToken(userOne._id, tokenExpires, 'invalidSecret'); + const expires = moment().add(config.jwt.accessExpirationMinutes, 'minutes'); + const accessToken = tokenService.generateToken(userOne._id, expires, tokenTypes.ACCESS, 'invalidSecret'); const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${accessToken}` } }); const next = jest.fn(); @@ -404,8 +419,8 @@ describe('Auth middleware', () => { test('should call next with unauthorized error if access token is expired', async () => { await insertUsers([userOne]); - const tokenExpires = moment().subtract(1, 'minutes'); - const accessToken = tokenService.generateToken(userOne._id, tokenExpires); + const expires = moment().subtract(1, 'minutes'); + const accessToken = tokenService.generateToken(userOne._id, expires, tokenTypes.ACCESS); const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${accessToken}` } }); const next = jest.fn();