Skip to content

Commit

Permalink
feature(logout): User should be able to sign out
Browse files Browse the repository at this point in the history
- add authentication middleware
- add functionality to sign out a user
- write tests for logout route
- add a pretest script

 [Finishes #167164982]
  • Loading branch information
Rythae committed Aug 24, 2019
1 parent b866ecf commit 5942e97
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ after_success:
- npm run coveralls

git:
depth: 20
depth: 50
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"heroku-postbuild": "npm run db:ready",
"start": "babel-node ./src/index.js",
"deleteBlToken": "babel-node ./src/services/deleteBlacklistedToken.js --exit",
"mocha-test": "nyc mocha --require @babel/register tests/*.js --timeout 20000 --exit",
"test": "cross-env NODE_ENV=test npm-run-all db:ready mocha-test",
"dev:start": "cross-env DEBUG=dev nodemon --exec babel-node ./src/index.js",
Expand Down
28 changes: 26 additions & 2 deletions src/controllers/AuthController.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const {
userServices: { findUser, updateUser },
passwordServices: { getPreviousPasswords, deletePreviousPassword, createPreviousPassword }
} = services;
const { User } = models;
const { User, BlacklistedToken } = models;

/**
*
Expand Down Expand Up @@ -112,4 +112,28 @@ const updateStatus = async (req, res) => {
return successResponse(res, 200, { message: 'You have sucessfully verified your email' });
};

export default { forgotPassword, updateStatus, changePassword };
/**
*
* @name logOut
* @param {object} req
* @param {object} res
* @returns {json} - json
*/
const logOut = (req, res) => {
try {
const { token, decoded: { exp } } = req;
BlacklistedToken.create({ token, expTime: exp });
return res.status(200).json({
message: 'Logout was successful'
});
} catch (error) {
return responseMessage(res, 500, { error: 'Something went wrong' });
}
};

export default {
forgotPassword,
updateStatus,
logOut,
changePassword
};
23 changes: 23 additions & 0 deletions src/database/migrations/20190807135714-create-blacklisted-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const up = (queryInterface, Sequelize) => queryInterface.createTable('BlacklistedTokens', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
expTime: {
type: Sequelize.STRING
},
token: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
export const down = queryInterface => queryInterface.dropTable('BlacklistedTokens');
8 changes: 8 additions & 0 deletions src/database/models/blacklistedtoken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default (sequelize, DataTypes) => {
const BlacklistedToken = sequelize.define('BlacklistedToken', {
expTime: DataTypes.STRING,
token: DataTypes.STRING
}, {});
BlacklistedToken.associate = () => {};
return BlacklistedToken;
};
14 changes: 10 additions & 4 deletions src/middlewares/verifyToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dotenv.config();
const { SECRET_KEY, ACCOUNT_VERIFICATION_SECRET } = process.env;
const verifyPath = '/auth/verify/:token';
const { responseMessage } = helpers;
const { userServices: { findUser } } = services;
const { userServices: { findUser }, blacklistedTokenService: { findBlacklistedToken } } = services;

/**
*
Expand All @@ -27,18 +27,24 @@ export default (request, response, next) => {

jwt.verify(token, secret, async (error, decoded) => {
if (error) {
const message = (error.name === 'TokenExpiredError') ? 'token expired, you have to be signed in to continue' : 'you have to be signed in to continue';
return responseMessage(response, 401, { error: message });
const message = (error.name === 'TokenExpiredError') ? 'token expired' : 'invalid token';
responseMessage(response, 401, { error: message });
}
try {
const blacklistedToken = await findBlacklistedToken(token);
if (blacklistedToken) {
return responseMessage(response, 401, { error: 'invalid token' });
}
const user = await findUser(decoded.id);
if (!user) {
return responseMessage(response, 404, { error: 'user not found' });
}
request.user = user;
request.token = token;
request.decoded = decoded;
return next();
} catch (err) {
return responseMessage(response, 500, { error: err.message });
responseMessage(response, 500, { error: err.message });
}
});
};
6 changes: 5 additions & 1 deletion src/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ const auth = express.Router();
const AUTH_URL = '/auth';

const { userValidator, verifyToken } = middlewares;
const { forgotPassword, updateStatus, changePassword } = AuthController;
const {
forgotPassword, updateStatus, logOut, changePassword
} = AuthController;

// forgot password endpoint
auth.post(`${AUTH_URL}/forgotpassword`, userValidator.forgotPassword, forgotPassword);
Expand All @@ -17,4 +19,6 @@ auth.post(`${AUTH_URL}/changepassword`, verifyToken, userValidator.changePasswor
// verifyUser route
auth.patch(`${AUTH_URL}/verify/:token`, verifyToken, updateStatus);

auth.get(`${AUTH_URL}/logout`, verifyToken, logOut);

export default auth;
9 changes: 9 additions & 0 deletions src/services/blacklistedToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import db from '../database/models';

const { BlacklistedToken } = db;

const findBlacklistedToken = token => BlacklistedToken.findOne({ where: { token } });

export default {
findBlacklistedToken
};
23 changes: 23 additions & 0 deletions src/services/deleteBlacklistedToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Sequelize from 'sequelize';
import db from '../database/models';


const { BlacklistedToken } = db;
const { Op } = Sequelize;

const deleteExpiredBlacklistedToken = async () => {
const todaysDate = new Date();
const currentUnixTime = Math.floor(todaysDate.getTime() / 1000);
const tokens = await BlacklistedToken.findAll({
where: { expTime: { [Op.lte]: currentUnixTime.toString() } }
});

const tokenArray = tokens.map(value => value.id);
BlacklistedToken.destroy({
where: {
id: tokenArray
}
});
};

deleteExpiredBlacklistedToken();
4 changes: 3 additions & 1 deletion src/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import novelServices from './novelService';
import commentServices from './commentService';
import notificationServices from './notification';
import passwordServices from './passwordService';
import blacklistedTokenService from './blacklistedToken';

export default {
sendMail,
userServices,
novelServices,
commentServices,
notificationServices,
passwordServices
passwordServices,
blacklistedTokenService
};
69 changes: 68 additions & 1 deletion tests/auth.spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import chai from 'chai';
import chaiHttp from 'chai-http';
import sinon from 'sinon';
import sendgridMail from '@sendgrid/mail';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import bcrypt from 'bcrypt';
import sendgridMail from '@sendgrid/mail';
import server from '../src';
import mockData from './mockData';
import models from '../src/database/models';

chai.use(chaiHttp);
const { expect } = chai;
Expand All @@ -23,6 +24,8 @@ const {
userWithoutPreviousPassword, withoutFivePreviousPassword
} = userMock;

const { BlacklistedToken } = models;

const BASE_URL = '/api/v1';
const FORGOT_PASSWORD_URL = `${BASE_URL}/auth/forgotPassword`;
const CHANGE_PASSWORD_URL = `${BASE_URL}/auth/changepassword`;
Expand All @@ -35,6 +38,10 @@ const loggedInUser2Token = jwt.sign({ id: '8f3e7eda-090a-4c44-9ffe-58443de5e1f8'

// token of loggedIn user Bruce Clifford in the database
const loggedInUser3Token = jwt.sign({ id: '8487ef08-2ac2-4387-8bd6-738b12c75dff' }, SECRET_KEY, { expiresIn: '60s' });
const SIGN_OUT_URL = `${BASE_URL}/auth`;

const logoutToken = jwt.sign({ id: '122a0d86-8b78-4bb8-b28f-8e5f7811c456' }, process.env.SECRET_KEY, { expiresIn: '60 minutes' });
const logoutTokenTwo = jwt.sign({ id: 'ce87299b-0dfa-44ed-bb53-45d434647eb2' }, process.env.SECRET_KEY, { expiresIn: '60 minutes' });

describe('AUTH', () => {
describe('POST /auth/signup', () => {
Expand Down Expand Up @@ -285,3 +292,63 @@ describe('AUTH', () => {
});
});
});
// Logout route
describe('GET api/v1/auth/logout', () => {
it('should return a 401 error accessing the logout route without a token', (done) => {
chai
.request(server)
.get(`${SIGN_OUT_URL}/logout`)
.end((err, res) => {
expect(res).status(401);
expect(res.body.error)
.to.eql('you have to be signed in to continue');
done();
});
});

it('should be able to logout successfully and return a status code of 200', (done) => {
chai
.request(server)
.get(`${SIGN_OUT_URL}/logout`)
.set('Authorization', `${logoutToken}`)
.end((err, res) => {
expect(res).status(200);
expect(res.body)
.to.be.a('object');
expect(res.body.message)
.to.eql('Logout was successful');
done();
});
});

it('should not be allowed to sign in if token is blacklisted', (done) => {
chai
.request(server)
.get(`${SIGN_OUT_URL}/logout`)
.set('Authorization', `${logoutToken}`)
.end((err, res) => {
expect(res).status(401);
expect(res.body)
.to.be.a('object');
expect(res.body.error)
.to.eql('invalid token');
done();
});
});

it('should return a failure response if a server error occurs', (done) => {
const stub = sinon.stub(BlacklistedToken, 'create');
stub.throws(new Error('error occurred!'));

chai.request(server)
.get(`${SIGN_OUT_URL}/logout`)
.set('Authorization', `${logoutTokenTwo}`)
.end((error, response) => {
expect(response).to.have.status(500);
expect(response.body).to.be.an('object');
expect(response.body.error).to.equal('Something went wrong');
stub.restore();
done();
});
});
});
4 changes: 2 additions & 2 deletions tests/comment.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('COMMENT ROUTES', () => {
.end((error, response) => {
expect(response).to.have.status(401);
expect(response.body).to.be.an('object');
expect(response.body.error).to.equal('token expired, you have to be signed in to continue');
expect(response.body.error).to.equal('token expired');
done();
});
});
Expand All @@ -64,7 +64,7 @@ describe('COMMENT ROUTES', () => {
.end((error, response) => {
expect(response).to.have.status(401);
expect(response.body).to.be.an('object');
expect(response.body.error).to.equal('you have to be signed in to continue');
expect(response.body.error).to.equal('invalid token');
done();
});
});
Expand Down
4 changes: 2 additions & 2 deletions tests/novel.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe('Test for novel CRUD', () => {
.set('authorization', 'wrong token')
.end((err, res) => {
expect(res).to.have.status(401);
expect(res.body.error).to.equal('you have to be signed in to continue');
expect(res.body.error).to.equal('invalid token');
done();
});
});
Expand All @@ -137,7 +137,7 @@ describe('Test for novel CRUD', () => {
.set('authorization', invalidToken)
.end((err, res) => {
expect(res).status(401);
expect(res.body).property('error').eq('you have to be signed in to continue');
expect(res.body).property('error').eq('invalid token');
done();
});
});
Expand Down
14 changes: 8 additions & 6 deletions tests/verifyUser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ describe('Test for base api base url', () => {
invalidIdUrl = `/api/v1/auth/verify/${invalidIdTokenData}`;
});

it('should return a sucess message on sucessful change of verified status', async () => {
await User.update({
it('should return a sucess message on sucessful change of verified status', (done) => {
User.update({
verifiedToken: tokenData
}, {
where: {
Expand All @@ -52,11 +52,12 @@ describe('Test for base api base url', () => {
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body.message).to.equal('You have sucessfully verified your email');
done();
});
});

it('should return an error message for invalid token', async () => {
await User.update({
it('should return an error message for invalid token', (done) => {
User.update({
verifiedToken: invalidToken
}, {
where: {
Expand All @@ -67,7 +68,8 @@ describe('Test for base api base url', () => {
.patch(`${invalidTokenUrl}`)
.end((err, res) => {
expect(res).to.have.status(401);
expect(res.body.error).to.equal('you have to be signed in to continue');
expect(res.body.error).to.equal('invalid token');
done();
});
});

Expand Down Expand Up @@ -120,7 +122,7 @@ describe('Test for base api base url', () => {
.patch(`${expiredTokenUrl}`)
.end((err, res) => {
expect(res).to.have.status(401);
expect(res.body.error).to.equal('token expired, you have to be signed in to continue');
expect(res.body.error).to.equal('token expired');
done();
});
});
Expand Down

0 comments on commit 5942e97

Please sign in to comment.