diff --git a/common/models/user.js b/common/models/user.js index af2a03b77a..24eebbf111 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -8,6 +8,7 @@ import path from 'path'; import loopback from 'loopback'; import _ from 'lodash'; import { ObjectId } from 'mongodb'; +import jwt from 'jsonwebtoken'; import { themes } from '../utils/themes'; import { saveUser, observeMethod } from '../../server/utils/rx.js'; @@ -379,6 +380,8 @@ module.exports = function(User) { domain: process.env.COOKIE_DOMAIN || 'localhost' }; 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('userId', accessToken.userId, config); } diff --git a/package-lock.json b/package-lock.json index a5233aa023..e246e00815 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9954,6 +9954,30 @@ "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", "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": { "version": "1.4.1", "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", "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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -11083,6 +11112,11 @@ "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", "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": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -11095,6 +11129,26 @@ "integrity": "sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=", "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": { "version": "4.1.1", "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", "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": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.partialright/-/lodash.partialright-4.2.1.tgz", diff --git a/package.json b/package.json index 7ae8b12752..36962a72e7 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "jquery": "~3.1.1", "jshint": "~2.9.4", "jsonlint-cli": "^1.0.1", + "jsonwebtoken": "^8.2.1", "lightbox2": "~2.8.2", "lodash": "^4.1.0", "loopback": "^3.11.1", diff --git a/server/boot/a-services.js b/server/boot/a-services.js index 4bbe43a936..9286814d60 100644 --- a/server/boot/a-services.js +++ b/server/boot/a-services.js @@ -13,5 +13,7 @@ export default function bootServices(app) { Fetchr.registerFetcher(mapUi); Fetchr.registerFetcher(user); - app.use('/services', Fetchr.middleware()); + const middleware = Fetchr.middleware(); + app.use('/services', middleware); + app.use('/external/services', middleware); } diff --git a/server/boot/challenge.js b/server/boot/challenge.js index e1d76294f2..9b84814380 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -20,7 +20,7 @@ function buildUserUpdate( timezone ) { let finalChallenge; - const updateData = { $set: {}, $push: {} }; + const updateData = { $push: {} }; const { timezone: userTimezone, completedChallenges = [] } = user; const oldChallenge = _.find( @@ -127,13 +127,12 @@ export default function(app) { router.get('/map', redirectToLearn); app.use(api); + app.use('/external', api); app.use(router); function modernChallengeCompleted(req, res, next) { const type = accepts(req).type('html', 'json', 'text'); 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); if (errors) { diff --git a/server/component-passport.js b/server/component-passport.js index 796d779d4a..7e76b97f08 100644 --- a/server/component-passport.js +++ b/server/component-passport.js @@ -3,6 +3,7 @@ import { PassportConfigurator } from '@freecodecamp/loopback-component-passport'; import passportProviders from './passport-providers'; import url from 'url'; +import jwt from 'jsonwebtoken'; const passportOptions = { emailOptional: true, @@ -143,6 +144,8 @@ export default function setupPassport(app) { maxAge: accessToken.ttl, 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('userId', accessToken.userId, cookieConfig); req.login(user); diff --git a/server/middleware.json b/server/middleware.json index a3a63e13fc..23ef0d32e0 100644 --- a/server/middleware.json +++ b/server/middleware.json @@ -55,7 +55,8 @@ "./middlewares/csp": {}, "./middlewares/jade-helpers": {}, "./middlewares/flash-cheaters": {}, - "./middlewares/passport-login": {} + "./middlewares/passport-login": {}, + "./middlewares/jwt-authorization": {} }, "files": {}, "final:after": { diff --git a/server/middlewares/csurf.js b/server/middlewares/csurf.js index 02c19e301e..50b2821ceb 100644 --- a/server/middlewares/csurf.js +++ b/server/middlewares/csurf.js @@ -1,10 +1,17 @@ import csurf from 'csurf'; export default function() { - const protection = csurf({ cookie: true }); + const protection = csurf( + { + cookie: { + domain: process.env.COOKIE_DOMAIN || 'localhost' + } + } + ); return function csrf(req, res, next) { + const path = req.path.split('/')[1]; - if (/api/.test(path)) { + if (/(api|external)/.test(path)) { return next(); } return protection(req, res, next); diff --git a/server/middlewares/jwt-authorization.js b/server/middlewares/jwt-authorization.js new file mode 100644 index 0000000000..c373ef8912 --- /dev/null +++ b/server/middlewares/jwt-authorization.js @@ -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(); +}; diff --git a/server/views/partials/meta.jade b/server/views/partials/meta.jade index d01894a261..d635729733 100644 --- a/server/views/partials/meta.jade +++ b/server/views/partials/meta.jade @@ -1,7 +1,6 @@ meta(charset='utf-8') meta(http-equiv='X-UA-Compatible', content='IE=edge') meta(name='viewport', content='width=device-width, initial-scale=1.0') -meta(name='csrf-token', content=_csrf) title #{title} | freeCodeCamp link(rel='canonical', href='https://www.freecodecamp.org') meta(charset='utf-8')