chore: Add tests for jwt authorization

This commit is contained in:
Bouncey
2019-02-20 18:18:50 +00:00
committed by mrugesh mohapatra
parent f5ca6ce5e9
commit 36c4737998
3 changed files with 163 additions and 38 deletions

View File

@ -1,30 +0,0 @@
/* global describe xdescribe it expect */
import { isWhiteListedPath } from './jwt-authorization';
describe('jwt-authorization', () => {
describe('isWhiteListedPath', () => {
const whiteList = [/^\/is-ok\//, /^\/this-is\/also\/ok\//];
it('returns a boolean', () => {
const result = isWhiteListedPath();
expect(typeof result).toBe('boolean');
});
it('returns true for a white listed path', () => {
expect.assertions(2);
const resultA = isWhiteListedPath('/is-ok/should-be/good', whiteList);
const resultB = isWhiteListedPath('/this-is/also/ok/surely', whiteList);
expect(resultA).toBe(true);
expect(resultB).toBe(true);
});
it('returns false for a non-white-listed path', () => {
const result = isWhiteListedPath('/hax0r-42/no-go', whiteList);
expect(result).toBe(false);
});
});
xdescribe('authorizeByJWT');
});

View File

@ -0,0 +1,139 @@
/* global describe it expect */
import sinon from 'sinon';
import { mockReq, mockRes } from 'sinon-express-mock';
import jwt from 'jsonwebtoken';
import createRequestAuthorization, {
isWhiteListedPath
} from './request-authorization';
const validJWTSecret = 'this is a super secret string';
const invalidJWTSecret = 'This is not correct secret';
const now = new Date(Date.now());
const theBeginningOfTime = new Date(0);
const accessToken = {
id: '123abc',
userId: '456def',
ttl: 60000,
created: now
};
const users = {
'456def': {
username: 'camperbot',
progressTimestamps: [1, 2, 3, 4]
}
};
const mockGetUserById = id =>
id in users ? Promise.resolve(users[id]) : Promise.reject('No user found');
describe('request-authorization', () => {
describe('isWhiteListedPath', () => {
const whiteList = [/^\/is-ok\//, /^\/this-is\/also\/ok\//];
it('returns a boolean', () => {
const result = isWhiteListedPath();
expect(typeof result).toBe('boolean');
});
it('returns true for a white listed path', () => {
expect.assertions(2);
const resultA = isWhiteListedPath('/is-ok/should-be/good', whiteList);
const resultB = isWhiteListedPath('/this-is/also/ok/surely', whiteList);
expect(resultA).toBe(true);
expect(resultB).toBe(true);
});
it('returns false for a non-white-listed path', () => {
const result = isWhiteListedPath('/hax0r-42/no-go', whiteList);
expect(result).toBe(false);
});
});
describe('createRequestAuthorization', () => {
const requestAuthorization = createRequestAuthorization({
_jwtSecret: validJWTSecret,
getUserById: mockGetUserById
});
it('is a function', () => {
expect(typeof requestAuthorization).toEqual('function');
});
it('throws when no access token is present', () => {
expect.assertions(2);
const req = mockReq({ path: '/internal/some-path/that-needs/auth' });
const res = mockRes();
const next = sinon.spy();
expect(() => requestAuthorization(req, res, next)).toThrowError(
'Access token is required for this request'
);
expect(next.called).toBe(false);
});
it('throws when the access token is invalid', () => {
expect.assertions(2);
const invalidJWT = jwt.sign({ accessToken }, invalidJWTSecret);
const req = mockReq({
path: '/internal/some-path/that-needs/auth',
// eslint-disable-next-line camelcase
cookie: { jwt_access_token: invalidJWT }
});
const res = mockRes();
const next = sinon.spy();
expect(() => requestAuthorization(req, res, next)).toThrowError(
'invalid signature'
);
expect(next.called).toBe(false);
});
it('throws when the access token has expired', () => {
expect.assertions(2);
const invalidJWT = jwt.sign(
{ accessToken: { ...accessToken, created: theBeginningOfTime } },
validJWTSecret
);
const req = mockReq({
path: '/internal/some-path/that-needs/auth',
// eslint-disable-next-line camelcase
cookie: { jwt_access_token: invalidJWT }
});
const res = mockRes();
const next = sinon.spy();
expect(() => requestAuthorization(req, res, next)).toThrowError(
'Access token is no longer vaild'
);
expect(next.called).toBe(false);
});
it('adds the user to the request object', async done => {
expect.assertions(5);
const validJWT = jwt.sign({ accessToken }, validJWTSecret);
const req = mockReq({
path: '/internal/some-path/that-needs/auth',
// eslint-disable-next-line camelcase
cookie: { jwt_access_token: validJWT }
});
const res = mockRes();
const next = sinon.spy();
await requestAuthorization(req, res, next);
expect(next.called).toBe(true);
expect(req).toHaveProperty('user');
expect(req.user).toEqual(users['456def']);
expect(req.user).toHaveProperty('points');
expect(req.user.points).toEqual(4);
return done();
});
it('calls next if request does not require authorization', async () => {
const req = mockReq({ path: '/unauthenticated/another/route' });
const res = mockRes();
const next = sinon.spy();
await requestAuthorization(req, res, next);
expect(next.called).toBe(true);
});
});
});

View File

@ -1,6 +1,7 @@
import loopback from 'loopback'; import loopback from 'loopback';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { isBefore } from 'date-fns'; import { isBefore } from 'date-fns';
import { isEmpty } from 'lodash';
import { homeLocation } from '../../../config/env'; import { homeLocation } from '../../../config/env';
@ -18,8 +19,11 @@ export function isWhiteListedPath(path, whiteListREs = _whiteListREs) {
return whiteListREs.some(re => re.test(path)); return whiteListREs.some(re => re.test(path));
} }
export default () => export default ({
function authorizeByJWT(req, res, next) { _jwtSecret = process.env.JWT_SECRET,
getUserById = _getUserById
} = {}) =>
function requestAuthorisation(req, res, next) {
const { path } = req; const { path } = req;
if (apiProxyRE.test(path) && !isWhiteListedPath(path)) { if (apiProxyRE.test(path) && !isWhiteListedPath(path)) {
const cookie = const cookie =
@ -39,7 +43,7 @@ export default () =>
} }
let token; let token;
try { try {
token = jwt.verify(cookie, process.env.JWT_SECRET); token = jwt.verify(cookie, _jwtSecret);
} catch (err) { } catch (err) {
throw wrapHandledError(new Error(err.message), { throw wrapHandledError(new Error(err.message), {
type: 'info', type: 'info',
@ -60,9 +64,9 @@ export default () =>
status: 403 status: 403
}); });
} }
if (!req.user) {
const User = loopback.getModelByType('User'); if (isEmpty(req.user)) {
return User.findById(userId) return getUserById(userId)
.then(user => { .then(user => {
if (user) { if (user) {
user.points = user.progressTimestamps.length; user.points = user.progressTimestamps.length;
@ -73,8 +77,20 @@ export default () =>
.then(next) .then(next)
.catch(next); .catch(next);
} else { } else {
return next(); return Promise.resolve(next());
} }
} }
return next(); return Promise.resolve(next());
}; };
export function _getUserById(id) {
const User = loopback.getModelByType('User');
return new Promise((resolve, reject) =>
User.findById(id, (err, instance) => {
if (err) {
return reject(err);
}
return resolve(instance);
})
);
}