diff --git a/api-server/server/boot/certificate.js b/api-server/server/boot/certificate.js index 81e4768bfd..3e0b91b39b 100644 --- a/api-server/server/boot/certificate.js +++ b/api-server/server/boot/certificate.js @@ -37,7 +37,7 @@ export default function bootCertificate(app) { api.put('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert); api.get('/certificate/showCert/:username/:cert', showCert); - app.use('/internal', api); + app.use(api); } const noNameMessage = dedent` diff --git a/api-server/server/boot/challenge.js b/api-server/server/boot/challenge.js index 3c12293a43..7a0b01fac9 100644 --- a/api-server/server/boot/challenge.js +++ b/api-server/server/boot/challenge.js @@ -61,7 +61,6 @@ export default async function bootChallenge(app, done) { router.get('/map', redirectToLearn); app.use(api); - app.use('/internal', api); app.use(router); done(); } diff --git a/api-server/server/boot/donate.js b/api-server/server/boot/donate.js index e1866a3c31..d2e6af6e85 100644 --- a/api-server/server/boot/donate.js +++ b/api-server/server/boot/donate.js @@ -319,7 +319,6 @@ export default function donateBoot(app, done) { api.post('/update-paypal', updatePaypal); donateRouter.use('/donate', api); app.use(donateRouter); - app.use('/internal', donateRouter); connectToStripe().then(done); } } diff --git a/api-server/server/boot/randomAPIs.js b/api-server/server/boot/randomAPIs.js index 2c9d8e3efe..4868fe66a2 100644 --- a/api-server/server/boot/randomAPIs.js +++ b/api-server/server/boot/randomAPIs.js @@ -9,7 +9,6 @@ const githubSecret = process.env.GITHUB_SECRET; module.exports = function(app) { const router = app.loopback.Router(); - const api = app.loopback.Router(); const User = app.models.User; router.get('/api/github', githubCalls); @@ -22,14 +21,12 @@ module.exports = function(app) { ); router.get('/unsubscribed/:unsubscribeId', unsubscribedWithId); router.get('/unsubscribed', unsubscribed); - api.get('/resubscribe/:unsubscribeId', resubscribe); + router.get('/resubscribe/:unsubscribeId', resubscribe); router.get('/nonprofits', nonprofits); router.get('/coding-bootcamp-cost-calculator', bootcampCalculator); app.use(router); - app.use('/internal', api); - function theFastestWebPageOnTheInternet(req, res) { res.render('resources/the-fastest-web-page-on-the-internet', { title: 'This is the fastest web page on the internet' diff --git a/api-server/server/boot/restApi.js b/api-server/server/boot/restApi.js index d12911326a..73656db713 100644 --- a/api-server/server/boot/restApi.js +++ b/api-server/server/boot/restApi.js @@ -2,5 +2,4 @@ module.exports = function mountRestApi(app) { const restApi = app.loopback.rest(); const restApiRoot = app.get('restApiRoot'); app.use(restApiRoot, restApi); - app.use(`/internal${restApiRoot}`, restApi); }; diff --git a/api-server/server/boot/settings.js b/api-server/server/boot/settings.js index d770a1cfea..47f4660efc 100644 --- a/api-server/server/boot/settings.js +++ b/api-server/server/boot/settings.js @@ -48,7 +48,6 @@ export default function settingsController(app) { api.put('/update-my-username', ifNoUser401, updateMyUsername); api.put('/update-user-flag', ifNoUser401, updateUserFlag); - app.use('/internal', api); app.use(api); } diff --git a/api-server/server/boot/user.js b/api-server/server/boot/user.js index 5a5aec68d5..474a78ad32 100644 --- a/api-server/server/boot/user.js +++ b/api-server/server/boot/user.js @@ -31,7 +31,7 @@ function bootUser(app) { api.post('/account/reset-progress', ifNoUser401, postResetProgress); api.post('/user/report-user/', ifNoUser401, postReportUserProfile); - app.use('/internal', api); + app.use(api); } function createReadSessionUser(app) { diff --git a/api-server/server/middlewares/csurf.js b/api-server/server/middlewares/csurf.js index 6b15345c19..bacdd89f62 100644 --- a/api-server/server/middlewares/csurf.js +++ b/api-server/server/middlewares/csurf.js @@ -6,10 +6,9 @@ export default function() { domain: process.env.COOKIE_DOMAIN || 'localhost' } }); - // Note: paypal webhook goes through /internal return function csrf(req, res, next) { const path = req.path.split('/')[1]; - if (/(^api$|^unauthenticated$|^internal$|^p$)/.test(path)) { + if (/^donate\/update-paypal$/.test(path)) { return next(); } return protection(req, res, next); diff --git a/api-server/server/middlewares/request-authorization.js b/api-server/server/middlewares/request-authorization.js index e2465a57da..512394a734 100644 --- a/api-server/server/middlewares/request-authorization.js +++ b/api-server/server/middlewares/request-authorization.js @@ -11,19 +11,23 @@ import { jwtSecret as _jwtSecret } from '../../../config/secrets'; import { wrapHandledError } from '../utils/create-handled-error'; -// We need to tunnel through a proxy path set up within -// the gatsby app, at this time, that path is /internal -const apiProxyRE = /^\/internal\/|^\/external\//; -const newsShortLinksRE = /^\/internal\/n\/|^\/internal\/p\?/; -const loopbackAPIPathRE = /^\/internal\/api\//; -const showCertRe = /^\/internal\/certificate\/showCert\//; -const updatePaypalRe = /^\/internal\/donate\/update-paypal/; +const newsShortLinksRE = /^\/n\/|^\/p\//; +const showCertRE = /^\/certificate\/showCert\//; +const updatePaypalRE = /^\/donate\/update-paypal/; +// signin may not have a trailing slash +const signinRE = /^\/signin/; +const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//; +const unsubscribedRE = /^\/unsubscribed\//; +const resubscribeRE = /^\/resubscribe\//; const _whiteListREs = [ newsShortLinksRE, - loopbackAPIPathRE, - showCertRe, - updatePaypalRe + showCertRE, + updatePaypalRE, + signinRE, + unsubscribeRE, + unsubscribedRE, + resubscribeRE ]; export function isWhiteListedPath(path, whiteListREs = _whiteListREs) { @@ -33,7 +37,7 @@ export function isWhiteListedPath(path, whiteListREs = _whiteListREs) { export default ({ jwtSecret = _jwtSecret, getUserById = _getUserById } = {}) => function requestAuthorisation(req, res, next) { const { path } = req; - if (apiProxyRE.test(path) && !isWhiteListedPath(path)) { + if (!isWhiteListedPath(path)) { const { accessToken, error, jwt } = getAccessTokenFromRequest( req, jwtSecret diff --git a/api-server/server/middlewares/request-authorization.test.js b/api-server/server/middlewares/request-authorization.test.js index 1d1673db84..c40ac0ccfc 100644 --- a/api-server/server/middlewares/request-authorization.test.js +++ b/api-server/server/middlewares/request-authorization.test.js @@ -64,7 +64,7 @@ describe('request-authorization', () => { describe('cookies', () => { it('throws when no access token is present', () => { expect.assertions(2); - const req = mockReq({ path: '/internal/some-path/that-needs/auth' }); + const req = mockReq({ path: '/some-path/that-needs/auth' }); const res = mockRes(); const next = sinon.spy(); expect(() => requestAuthorization(req, res, next)).toThrowError( @@ -77,7 +77,7 @@ describe('request-authorization', () => { expect.assertions(2); const invalidJWT = jwt.sign({ accessToken }, invalidJWTSecret); const req = mockReq({ - path: '/internal/some-path/that-needs/auth', + path: '/some-path/that-needs/auth', // eslint-disable-next-line camelcase cookie: { jwt_access_token: invalidJWT } }); @@ -97,7 +97,7 @@ describe('request-authorization', () => { validJWTSecret ); const req = mockReq({ - path: '/internal/some-path/that-needs/auth', + path: '/some-path/that-needs/auth', // eslint-disable-next-line camelcase cookie: { jwt_access_token: invalidJWT } }); @@ -114,7 +114,7 @@ describe('request-authorization', () => { expect.assertions(3); const validJWT = jwt.sign({ accessToken }, validJWTSecret); const req = mockReq({ - path: '/internal/some-path/that-needs/auth', + path: '/some-path/that-needs/auth', // eslint-disable-next-line camelcase cookie: { jwt_access_token: validJWT } }); @@ -130,7 +130,7 @@ describe('request-authorization', () => { 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', + path: '/some-path/that-needs/auth', // eslint-disable-next-line camelcase cookie: { jwt_access_token: validJWT } }); @@ -142,7 +142,8 @@ describe('request-authorization', () => { }); it('calls next if request does not require authorization', async () => { - const req = mockReq({ path: '/unauthenticated/another/route' }); + // currently /unsubscribe does not require authorization + const req = mockReq({ path: '/unsubscribe/another/route' }); const res = mockRes(); const next = sinon.spy(); await requestAuthorization(req, res, next); @@ -153,7 +154,7 @@ describe('request-authorization', () => { 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 req = mockReq({ path: '/some-path/that-needs/auth' }); const res = mockRes(); const next = sinon.spy(); expect(() => requestAuthorization(req, res, next)).toThrowError( @@ -166,7 +167,7 @@ describe('request-authorization', () => { expect.assertions(2); const invalidJWT = jwt.sign({ accessToken }, invalidJWTSecret); const req = mockReq({ - path: '/internal/some-path/that-needs/auth', + path: '/some-path/that-needs/auth', headers: { 'X-fcc-access-token': invalidJWT } }); const res = mockRes(); @@ -185,7 +186,7 @@ describe('request-authorization', () => { validJWTSecret ); const req = mockReq({ - path: '/internal/some-path/that-needs/auth', + path: '/some-path/that-needs/auth', headers: { 'X-fcc-access-token': invalidJWT } }); const res = mockRes(); @@ -201,7 +202,7 @@ describe('request-authorization', () => { expect.assertions(3); const validJWT = jwt.sign({ accessToken }, validJWTSecret); const req = mockReq({ - path: '/internal/some-path/that-needs/auth', + path: '/some-path/that-needs/auth', headers: { 'X-fcc-access-token': validJWT } }); const res = mockRes(); @@ -216,7 +217,7 @@ describe('request-authorization', () => { 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', + path: '/some-path/that-needs/auth', // eslint-disable-next-line camelcase cookie: { jwt_access_token: validJWT } }); @@ -228,7 +229,8 @@ describe('request-authorization', () => { }); it('calls next if request does not require authorization', async () => { - const req = mockReq({ path: '/unauthenticated/another/route' }); + // currently /unsubscribe does not require authorization + const req = mockReq({ path: '/unsubscribe/another/route' }); const res = mockRes(); const next = sinon.spy(); await requestAuthorization(req, res, next); diff --git a/api-server/server/views/emails/user-request-sign-in.ejs b/api-server/server/views/emails/user-request-sign-in.ejs index 300999f9e1..71c9f87870 100644 --- a/api-server/server/views/emails/user-request-sign-in.ejs +++ b/api-server/server/views/emails/user-request-sign-in.ejs @@ -1,9 +1,9 @@ Here's your sign in link. It will instantly sign you into freeCodeCamp.org - no password necessary: -<%= host %>/internal/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> +<%= host %>/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> Note: this sign in link will expire after 15 minutes. If you need a new sign in link, go to https://www.freecodecamp.org/signin See you soon! -- The freeCodeCamp.org Team \ No newline at end of file +- The freeCodeCamp.org Team diff --git a/api-server/server/views/emails/user-request-sign-up.ejs b/api-server/server/views/emails/user-request-sign-up.ejs index 414da690bb..42f3b140ed 100644 --- a/api-server/server/views/emails/user-request-sign-up.ejs +++ b/api-server/server/views/emails/user-request-sign-up.ejs @@ -4,10 +4,10 @@ We have created a new account for you. Here's your sign in link. It will instantly sign you into freeCodeCamp.org - no password necessary: -<%= host %>/internal/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> +<%= host %>/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> Note: this sign in link will expire after 15 minutes. If you need a new sign in link, go to https://www.freecodecamp.org/signin See you soon! -- The freeCodeCamp.org Team \ No newline at end of file +- The freeCodeCamp.org Team diff --git a/client/package-lock.json b/client/package-lock.json index 5bfe3e8490..98ebd1b88d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8141,6 +8141,16 @@ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" }, + "csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "requires": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + } + }, "css": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", @@ -22220,6 +22230,11 @@ "ret": "~0.1.10" } }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -23989,6 +24004,11 @@ "inherits": "^2.0.1" } }, + "rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=" + }, "rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", @@ -26173,6 +26193,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "tsutils": { "version": "3.17.1", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", @@ -26286,6 +26311,14 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", diff --git a/client/package.json b/client/package.json index 287721b35f..fd526fa26d 100644 --- a/client/package.json +++ b/client/package.json @@ -22,6 +22,7 @@ "bezier-easing": "^2.1.0", "browser-cookies": "^1.2.0", "chai": "^4.2.0", + "csrf": "^3.1.0", "date-fns": "^1.30.1", "entities": "^1.1.2", "enzyme": "^3.10.0", diff --git a/client/src/client-only-routes/ShowUnsubscribed.js b/client/src/client-only-routes/ShowUnsubscribed.js index 787fa73144..37cd34d62b 100644 --- a/client/src/client-only-routes/ShowUnsubscribed.js +++ b/client/src/client-only-routes/ShowUnsubscribed.js @@ -31,7 +31,7 @@ function ShowUnsubscribed({ unsubscribeId }) { block={true} bsSize='lg' bsStyle='primary' - href={`${apiLocation}/internal/resubscribe/${unsubscribeId}`} + href={`${apiLocation}/resubscribe/${unsubscribeId}`} > You can click here to resubscribe diff --git a/client/src/redux/index.js b/client/src/redux/index.js index b17818bcd5..cd4d8a0382 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -115,6 +115,7 @@ export const preventProgressDonationRequests = createAction( export const onlineStatusChange = createAction(types.onlineStatusChange); +// TODO: re-evaluate this since /internal is no longer used. // `hardGoTo` is used to hit the API server directly // without going through /internal // used for things like /signin and /signout diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js index 973ca7597b..b0776966e1 100644 --- a/client/src/utils/ajax.js +++ b/client/src/utils/ajax.js @@ -1,14 +1,18 @@ import { apiLocation } from '../../config/env.json'; - +import { _csrf } from '../redux/cookieValues'; import axios from 'axios'; +import Tokens from 'csrf'; -const base = apiLocation + '/internal'; -const baseUnauthenticated = apiLocation + '/unauthenticated'; +const base = apiLocation; +const tokens = new Tokens(); axios.defaults.withCredentials = true; -export function postUnauthenticated(path, body) { - return axios.post(`${baseUnauthenticated}${path}`, body); +// _csrf is passed to the client as a cookie. Tokens are sent back to the server +// via headers: +if (_csrf) { + axios.defaults.headers.post['CSRF-Token'] = tokens.create(_csrf); + axios.defaults.headers.put['CSRF-Token'] = tokens.create(_csrf); } function get(path) { diff --git a/tools/scripts/build/__snapshots__/create-redirects.test.js.snap b/tools/scripts/build/__snapshots__/create-redirects.test.js.snap index 29520fedd0..57b660152b 100644 --- a/tools/scripts/build/__snapshots__/create-redirects.test.js.snap +++ b/tools/scripts/build/__snapshots__/create-redirects.test.js.snap @@ -10,10 +10,6 @@ exports[`createRedirects matches the snapshot 1`] = ` https://freecodecamp-dev.netlify.com/* https://www.freecodecamp.dev/:splat 301! https://freecodecamp-org.netlify.com/* https://www.freecodecamp.org/:splat 301! - -#api redirect -/internal/* https://api.example.com/internal/:splat 200! - # pages /about https://news.example.com/about 200 /academic-honesty https://news.example.com/academic-honesty 200 diff --git a/tools/scripts/build/create-redirects.js b/tools/scripts/build/create-redirects.js index da8bcf4ef8..8739694bca 100644 --- a/tools/scripts/build/create-redirects.js +++ b/tools/scripts/build/create-redirects.js @@ -31,10 +31,6 @@ const template = ` https://freecodecamp-dev.netlify.com/* https://www.freecodecamp.dev/:splat 301! https://freecodecamp-org.netlify.com/* https://www.freecodecamp.org/:splat 301! - -#api redirect -/internal/* #{{API}}/internal/:splat 200! - # pages /about #{{NEWS}}/about 200 /academic-honesty #{{NEWS}}/academic-honesty 200 diff --git a/tools/scripts/build/create-redirects.test.js b/tools/scripts/build/create-redirects.test.js index 0ec5988f3e..8a486233af 100644 --- a/tools/scripts/build/create-redirects.test.js +++ b/tools/scripts/build/create-redirects.test.js @@ -18,7 +18,7 @@ describe('createRedirects', () => { }); it('replaces instances of `#{{...}}` with the locations provided', () => { - expect.assertions(5); + expect.assertions(4); const apiPlaceholderRE = /#\{\{API\}\}/; const newsPlaceholderRE = /#\{\{NEWS\}\}/; @@ -33,8 +33,7 @@ describe('createRedirects', () => { expect(hasNewsPlaceholder).toBe(false); expect(hasForumPlaceholder).toBe(false); - const { api, forumProxy } = testLocations; - expect(redirects.includes(`${api}/internal/:splat`)).toBe(true); + const { forumProxy } = testLocations; expect(redirects.includes(`${forumProxy}`)).toBe(true); });