diff --git a/tests/enforceToken/revoke.js b/tests/enforceToken/revoke.js new file mode 100644 index 0000000000..a6a90f66f9 --- /dev/null +++ b/tests/enforceToken/revoke.js @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2017, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/tokenReq/api/token/createtoken.js + */ +const expect = require('chai').expect; +const supertest = require('supertest'); +const api = supertest(require('../../index').app); +const adminUser = require('../../config').db.adminUser; +const constants = require('../../api/v1/constants'); +const jwtUtil = require('../../utils/jwtUtil'); +const u = require('../testUtils'); +const registerPath = '/v1/register'; +const tokenPath = '/v1/tokens'; + +describe('enforceToken: revoke:', () => { + let defaultToken; + const predefinedAdminUserToken = jwtUtil.createToken( + adminUser.name, adminUser.name + ); + + beforeEach((done) => { + api.post(registerPath) + .send(u.fakeUserCredentials) + .end((err, res) => { + if (err) { + done(err); + } + + defaultToken = res.body.token; + done(); + }); + }); + + afterEach(u.forceDeleteToken); + + it('cannot use revoked token to authorize any API calls', (done) => { + api.post(tokenPath) + .set('Authorization', defaultToken) + .send({ name: 'newToken' }) + .end((err, res) => { + if (err) { + return done(err); + } + + const newToken = res.body.token; + return api.post(`${tokenPath}/${res.body.id}/revoke`) + .set('Authorization', predefinedAdminUserToken) + .send({ }) + .end((err2, res2) => { + if (err2 || res2.body.errors) { + return done(err2); + } + + return api.get('/v1/subjects') + .set('Authorization', newToken) + .expect(constants.httpStatus.FORBIDDEN) + .end((err3, res3) => { + if (err3) { + return done(err3); + } + + expect(res3.body.errors[0].description) + .to.eql('Token was revoked. Please contact your Refocus ' + + 'administrator.'); + return done(); + }); + }); + }); + }); + + it('restored token works to authorize an API call', (done) => { + api.post(tokenPath) + .set('Authorization', defaultToken) + .send({ name: 'newToken' }) + .end((err, res) => { + if (err) { + return done(err); + } + + const newToken = res.body.token; + const tid = res.body.id; + return api.post(`${tokenPath}/${tid}/revoke`) + .set('Authorization', predefinedAdminUserToken) + .send({ }) + .end((err2, res2) => { + expect(res2.body).to.not.have.property('errors'); + if (err2 || res2.body.errors) { + return done(err2); + } + + return api.post(`${tokenPath}/${tid}/restore`) + .set('Authorization', predefinedAdminUserToken) + .send({ }) + .end((err3, res3) => { + expect(res3.body).to.have.property('isRevoked', '0'); + expect(res3.body).to.have.property('name', 'newToken'); + if (err3 || res3.body.errors) { + return done(err3); + } + + return api.get('/v1/subjects') + .set('Authorization', newToken) + .expect(constants.httpStatus.OK) + .end((err4, res4) => { + if (err4 || res4.body.errors) { + return done(err4); + } + + return done(); + }); + }); + }); + }); + }); +}); diff --git a/utils/jwtUtil.js b/utils/jwtUtil.js index 2f88fd2fde..dc2a4bad04 100644 --- a/utils/jwtUtil.js +++ b/utils/jwtUtil.js @@ -14,8 +14,9 @@ const jwt = require('jsonwebtoken'); const apiErrors = require('../api/v1/apiErrors'); const conf = require('../config'); -const env = conf.environment[conf.nodeEnv]; +const secret = conf.environment[conf.nodeEnv].tokenSecret; const User = require('../db/index').User; +const Token = require('../db/index').Token; /** * Attaches the resource type to the error and passes it on to the next @@ -43,7 +44,47 @@ function handleInvalidToken(cb) { } /** - * Verify jwt token. + * Verify jwt token. If verify successful, also check the token record in the + * db (exists AND is not revoked) and load the user record from the db and add + * to the request. (Skip the token record check if the token is the default UI + * token.) + * + * @param {object} t - The Token object from the db + * @returns {boolean} true if ok, otherwise throws error + */ +function checkTokenRecord(t) { + if (t && t.isRevoked === '0') { + return true; + } + + if (!t) { + const err = new apiErrors.ForbiddenError({ + explanation: 'Missing user for the specified token. ' + + 'Please contact your Refocus administrator.', + }); + throw err; + } + + if (t.isRevoked !== '0') { + const err = new apiErrors.ForbiddenError({ + explanation: 'Token was revoked. Please contact your ' + + 'Refocus administrator.', + }); + throw err; + } + + const err = new apiErrors.ForbiddenError({ + explanation: 'Invalid Token.', + }); + throw err; +} // checkTokenRecord + +/** + * Verify jwt token. If verify successful, also check the token record in the + * db (exists AND is not revoked) and load the user record from the db and add + * to the request. (Skip the token record check if the token is the default UI + * token.) + * * @param {object} req - request object * @param {Function} cb - callback function */ @@ -56,25 +97,49 @@ function verifyToken(req, cb) { } if (token) { - jwt.verify(token, env.tokenSecret, {}, (err, decodedData) => { + jwt.verify(token, secret, {}, (err, decodedData) => { if (err) { - handleInvalidToken(cb); - } else { - User.findOne({ where: { name: decodedData.username } }) + return handleInvalidToken(cb); + } else { // eslint-disable-line no-else-return + return User.findOne({ where: { name: decodedData.username } }) .then((user) => { - // set user in request if (user) { - req.user = user; - return cb(); - } + req.user = user; // set user in request - return handleInvalidToken(cb); + /* + * No need to check the token record if this is the default UI + * token. + */ + if (decodedData.username === decodedData.tokenname) { + return cb(); + } + + return Token.findOne({ + where: { + name: decodedData.tokenname, + createdBy: user.id, + } + }) + .then(checkTokenRecord) + .then((ok) => { + if (ok) { + return cb(); + } else { // eslint-disable-line no-else-return + return handleInvalidToken(cb); + } + }) + .catch((tokenError) => { + return handleError(cb, tokenError, 'ApiToken'); + }); + } else { // eslint-disable-line no-else-return + return handleInvalidToken(cb); + } }); } }); } else { const err = new apiErrors.ForbiddenError({ - explanation: 'No authorization token was found', + explanation: 'No authorization token was found.', }); handleError(cb, err, 'ApiToken'); } @@ -90,7 +155,7 @@ function verifyToken(req, cb) { function getTokenDetailsFromTokenString(s) { return new Promise((resolve, reject) => { if (s) { - jwt.verify(s, env.tokenSecret, {}, (err, decodedData) => { + jwt.verify(s, secret, {}, (err, decodedData) => { if (err !== null || !decodedData) { return reject(new apiErrors.ForbiddenError({ explanation: 'No authorization token was found', @@ -138,7 +203,7 @@ function createToken(tokenName, userName) { username: userName, timestamp: Date.now, }; - const createdToken = jwt.sign(jwtClaim, env.tokenSecret); + const createdToken = jwt.sign(jwtClaim, secret); return createdToken; }