feat(auth): Authorise 'external' requests through JWT (#17224)

This commit is contained in:
Stuart Taylor
2018-05-23 21:10:56 +01:00
committed by mrugesh mohapatra
parent 3397fbbf60
commit dfda68fb58
10 changed files with 132 additions and 8 deletions

View File

@ -8,6 +8,7 @@ import path from 'path';
import loopback from 'loopback'; import loopback from 'loopback';
import _ from 'lodash'; import _ from 'lodash';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import jwt from 'jsonwebtoken';
import { themes } from '../utils/themes'; import { themes } from '../utils/themes';
import { saveUser, observeMethod } from '../../server/utils/rx.js'; import { saveUser, observeMethod } from '../../server/utils/rx.js';
@ -379,6 +380,8 @@ module.exports = function(User) {
domain: process.env.COOKIE_DOMAIN || 'localhost' domain: process.env.COOKIE_DOMAIN || 'localhost'
}; };
if (accessToken && accessToken.id) { if (accessToken && accessToken.id) {
const jwtAccess = jwt.sign({accessToken}, process.env.JWT_SECRET);
res.cookie('jwt_access_token', jwtAccess, config);
res.cookie('access_token', accessToken.id, config); res.cookie('access_token', accessToken.id, config);
res.cookie('userId', accessToken.userId, config); res.cookie('userId', accessToken.userId, config);
} }

59
package-lock.json generated
View File

@ -9954,6 +9954,30 @@
"integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
"dev": true "dev": true
}, },
"jsonwebtoken": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.2.1.tgz",
"integrity": "sha512-l8rUBr0fqYYwPc8/ZGrue7GiW7vWdZtZqelxo4Sd5lMvuEeCK8/wS54sEo6tJhdZ6hqfutsj6COgC0d1XdbHGw==",
"requires": {
"jws": "3.1.4",
"lodash.includes": "4.3.0",
"lodash.isboolean": "3.0.3",
"lodash.isinteger": "4.0.4",
"lodash.isnumber": "3.0.3",
"lodash.isplainobject": "4.0.6",
"lodash.isstring": "4.0.1",
"lodash.once": "4.1.1",
"ms": "2.1.1",
"xtend": "4.0.1"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"jsprim": { "jsprim": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@ -11071,6 +11095,11 @@
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI="
}, },
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
},
"lodash.isarguments": { "lodash.isarguments": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@ -11083,6 +11112,11 @@
"integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
"dev": true "dev": true
}, },
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
},
"lodash.isequal": { "lodash.isequal": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
@ -11095,6 +11129,26 @@
"integrity": "sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=", "integrity": "sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=",
"dev": true "dev": true
}, },
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.kebabcase": { "lodash.kebabcase": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
@ -11140,6 +11194,11 @@
"resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz", "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz",
"integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw=" "integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw="
}, },
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"lodash.partialright": { "lodash.partialright": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/lodash.partialright/-/lodash.partialright-4.2.1.tgz", "resolved": "https://registry.npmjs.org/lodash.partialright/-/lodash.partialright-4.2.1.tgz",

View File

@ -84,6 +84,7 @@
"jquery": "~3.1.1", "jquery": "~3.1.1",
"jshint": "~2.9.4", "jshint": "~2.9.4",
"jsonlint-cli": "^1.0.1", "jsonlint-cli": "^1.0.1",
"jsonwebtoken": "^8.2.1",
"lightbox2": "~2.8.2", "lightbox2": "~2.8.2",
"lodash": "^4.1.0", "lodash": "^4.1.0",
"loopback": "^3.11.1", "loopback": "^3.11.1",

View File

@ -13,5 +13,7 @@ export default function bootServices(app) {
Fetchr.registerFetcher(mapUi); Fetchr.registerFetcher(mapUi);
Fetchr.registerFetcher(user); Fetchr.registerFetcher(user);
app.use('/services', Fetchr.middleware()); const middleware = Fetchr.middleware();
app.use('/services', middleware);
app.use('/external/services', middleware);
} }

View File

@ -20,7 +20,7 @@ function buildUserUpdate(
timezone timezone
) { ) {
let finalChallenge; let finalChallenge;
const updateData = { $set: {}, $push: {} }; const updateData = { $push: {} };
const { timezone: userTimezone, completedChallenges = [] } = user; const { timezone: userTimezone, completedChallenges = [] } = user;
const oldChallenge = _.find( const oldChallenge = _.find(
@ -127,13 +127,12 @@ export default function(app) {
router.get('/map', redirectToLearn); router.get('/map', redirectToLearn);
app.use(api); app.use(api);
app.use('/external', api);
app.use(router); app.use(router);
function modernChallengeCompleted(req, res, next) { function modernChallengeCompleted(req, res, next) {
const type = accepts(req).type('html', 'json', 'text'); const type = accepts(req).type('html', 'json', 'text');
req.checkBody('id', 'id must be an ObjectId').isMongoId(); req.checkBody('id', 'id must be an ObjectId').isMongoId();
req.checkBody('files', 'files must be an object with polyvinyls for keys')
.isFiles();
const errors = req.validationErrors(true); const errors = req.validationErrors(true);
if (errors) { if (errors) {

View File

@ -3,6 +3,7 @@ import { PassportConfigurator } from
'@freecodecamp/loopback-component-passport'; '@freecodecamp/loopback-component-passport';
import passportProviders from './passport-providers'; import passportProviders from './passport-providers';
import url from 'url'; import url from 'url';
import jwt from 'jsonwebtoken';
const passportOptions = { const passportOptions = {
emailOptional: true, emailOptional: true,
@ -143,6 +144,8 @@ export default function setupPassport(app) {
maxAge: accessToken.ttl, maxAge: accessToken.ttl,
domain: process.env.COOKIE_DOMAIN || 'localhost' domain: process.env.COOKIE_DOMAIN || 'localhost'
}; };
const jwtAccess = jwt.sign({accessToken}, process.env.JWT_SECRET);
res.cookie('jwt_access_token', jwtAccess, cookieConfig);
res.cookie('access_token', accessToken.id, cookieConfig); res.cookie('access_token', accessToken.id, cookieConfig);
res.cookie('userId', accessToken.userId, cookieConfig); res.cookie('userId', accessToken.userId, cookieConfig);
req.login(user); req.login(user);

View File

@ -55,7 +55,8 @@
"./middlewares/csp": {}, "./middlewares/csp": {},
"./middlewares/jade-helpers": {}, "./middlewares/jade-helpers": {},
"./middlewares/flash-cheaters": {}, "./middlewares/flash-cheaters": {},
"./middlewares/passport-login": {} "./middlewares/passport-login": {},
"./middlewares/jwt-authorization": {}
}, },
"files": {}, "files": {},
"final:after": { "final:after": {

View File

@ -1,10 +1,17 @@
import csurf from 'csurf'; import csurf from 'csurf';
export default function() { export default function() {
const protection = csurf({ cookie: true }); const protection = csurf(
{
cookie: {
domain: process.env.COOKIE_DOMAIN || 'localhost'
}
}
);
return function csrf(req, res, next) { return function csrf(req, res, next) {
const path = req.path.split('/')[1]; const path = req.path.split('/')[1];
if (/api/.test(path)) { if (/(api|external)/.test(path)) {
return next(); return next();
} }
return protection(req, res, next); return protection(req, res, next);

View File

@ -0,0 +1,50 @@
import jwt from 'jsonwebtoken';
import { isBefore } from 'date-fns';
import { wrapHandledError } from '../utils/create-handled-error';
export default () => function authorizeByJWT(req, res, next) {
const path = req.path.split('/')[1];
if (/external/.test(path)) {
const cookie = req.signedCookies && req.signedCookies['jwt_access_token'];
if (!cookie) {
throw wrapHandledError(
new Error('Access token is required for this request'),
{
type: 'info',
redirect: '/signin',
message: 'Access token is required for this request',
status: 403
}
);
}
let token;
try {
token = jwt.verify(cookie, process.env.JWT_SECRET);
} catch (err) {
throw wrapHandledError(
new Error(err.message),
{
type: 'info',
redirct: '/signin',
message: 'Your access token is invalid',
status: 403
}
);
}
const { accessToken: {created, ttl }} = token;
const valid = isBefore(Date.now(), Date.parse(created) + ttl);
if (!valid) {
throw wrapHandledError(
new Error('Access token is no longer vaild'),
{
type: 'info',
redirect: '/signin',
message: 'Access token is no longer vaild',
status: 403
}
);
}
return next();
}
return next();
};

View File

@ -1,7 +1,6 @@
meta(charset='utf-8') meta(charset='utf-8')
meta(http-equiv='X-UA-Compatible', content='IE=edge') meta(http-equiv='X-UA-Compatible', content='IE=edge')
meta(name='viewport', content='width=device-width, initial-scale=1.0') meta(name='viewport', content='width=device-width, initial-scale=1.0')
meta(name='csrf-token', content=_csrf)
title #{title} | freeCodeCamp title #{title} | freeCodeCamp
link(rel='canonical', href='https://www.freecodecamp.org') link(rel='canonical', href='https://www.freecodecamp.org')
meta(charset='utf-8') meta(charset='utf-8')