diff --git a/api-server/server/middleware.json b/api-server/server/middleware.json index ba86d7c355..e9eebff045 100644 --- a/api-server/server/middleware.json +++ b/api-server/server/middleware.json @@ -31,7 +31,7 @@ "auth:before": { "./middlewares/add-return-to": {}, "./middlewares/cookie-parser": {}, - "./middlewares/jwt-authorization": {} + "./middlewares/request-authorization": {} }, "parse": { "body-parser#json": {}, diff --git a/api-server/server/middlewares/request-authorizaion.test.js b/api-server/server/middlewares/request-authorizaion.test.js index 9648d57151..735db4aeb7 100644 --- a/api-server/server/middlewares/request-authorizaion.test.js +++ b/api-server/server/middlewares/request-authorizaion.test.js @@ -53,7 +53,7 @@ describe('request-authorization', () => { describe('createRequestAuthorization', () => { const requestAuthorization = createRequestAuthorization({ - _jwtSecret: validJWTSecret, + jwtSecret: validJWTSecret, getUserById: mockGetUserById }); @@ -61,79 +61,183 @@ describe('request-authorization', () => { 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 } + describe('cookies', () => { + 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); }); - 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 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(); - 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 } + expect(() => requestAuthorization(req, res, next)).toThrowError( + 'Access token is invalid' + ); + expect(next.called).toBe(false); }); - 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('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(); - 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 } + 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('adds the jwt to the headers', async done => { + 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(res.set.calledWith('X-fcc-access-token', validJWT)).toBe(true); + 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); }); - 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); + describe('Auth header', () => { + 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', + headers: { 'X-fcc-access-token': invalidJWT } + }); + const res = mockRes(); + const next = sinon.spy(); + + expect(() => requestAuthorization(req, res, next)).toThrowError( + 'Access token is invalid' + ); + 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', + headers: { 'X-fcc-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', + headers: { 'X-fcc-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('adds the jwt to the headers', async done => { + 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(res.set.calledWith('X-fcc-access-token', validJWT)).toBe(true); + 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); + }); }); }); }); diff --git a/api-server/server/middlewares/request-authorization.js b/api-server/server/middlewares/request-authorization.js index ef408882f0..caf419af9b 100644 --- a/api-server/server/middlewares/request-authorization.js +++ b/api-server/server/middlewares/request-authorization.js @@ -1,9 +1,12 @@ import loopback from 'loopback'; -import jwt from 'jsonwebtoken'; -import { isBefore } from 'date-fns'; import { isEmpty } from 'lodash'; - +import { + getAccessTokenFromRequest, + errorTypes, + authHeaderNS +} from '../utils/getSetAccessToken'; import { homeLocation } from '../../../config/env'; +import { jwtSecret as _jwtSecret } from '../../../config/secrets'; import { wrapHandledError } from '../utils/create-handled-error'; @@ -19,18 +22,15 @@ export function isWhiteListedPath(path, whiteListREs = _whiteListREs) { return whiteListREs.some(re => re.test(path)); } -export default ({ - _jwtSecret = process.env.JWT_SECRET, - getUserById = _getUserById -} = {}) => +export default ({ jwtSecret = _jwtSecret, getUserById = _getUserById } = {}) => function requestAuthorisation(req, res, next) { const { path } = req; if (apiProxyRE.test(path) && !isWhiteListedPath(path)) { - const cookie = - (req.signedCookies && req.signedCookies['jwt_access_token']) || - (req.cookie && req.cookie['jwt_access_token']); - - if (!cookie) { + const { accessToken, error, jwt } = getAccessTokenFromRequest( + req, + jwtSecret + ); + if (!accessToken && error === errorTypes.noTokenFound) { throw wrapHandledError( new Error('Access token is required for this request'), { @@ -41,22 +41,15 @@ export default ({ } ); } - let token; - try { - token = jwt.verify(cookie, _jwtSecret); - } catch (err) { - throw wrapHandledError(new Error(err.message), { + if (!accessToken && error === errorTypes.invalidToken) { + throw wrapHandledError(new Error('Access token is invalid'), { type: 'info', redirect: `${homeLocation}/signin`, message: 'Your access token is invalid', status: 403 }); } - const { - accessToken: { created, ttl, userId } - } = token; - const valid = isBefore(Date.now(), Date.parse(created) + ttl); - if (!valid) { + if (!accessToken && error === errorTypes.expiredToken) { throw wrapHandledError(new Error('Access token is no longer vaild'), { type: 'info', redirect: `${homeLocation}/signin`, @@ -64,8 +57,9 @@ export default ({ status: 403 }); } - + res.set(authHeaderNS, jwt); if (isEmpty(req.user)) { + const { userId } = accessToken; return getUserById(userId) .then(user => { if (user) {