From 8c57faa71e7aa072a5823613d913b741df1bfcdf Mon Sep 17 00:00:00 2001 From: Alan Date: Thu, 4 Jul 2019 10:48:09 +1000 Subject: [PATCH] [SDPA-2689] Store authentication token more securely. (#406) * [SDPA-2689] Authenticated token fixes * Token to be store in cookies * Token removed from Vuex Store (as this will output to HTML) * Authenticated state (true / false) to be stored in Vuex. * [SDPA-2689] Split preview and authenticate functions into separate libs. * [SDPA-2689] Move vuex store management into authenticate lib. * [SDPA-2689] Extract and set cookie name to authenticatedContent. * [SDPA-2689] Add authenticatedContent module enabled checks. * [SDPA-2689] Add authenticated preview tests. * [SDPA-2689] Fixed test. Added preview role to created used. * [SDPA-2689] Lint fixes. * [SDPA-2689] isModuleEnabled to return false if no config available. --- .../ripple-nuxt-tide/lib/core/middleware.js | 25 ++--- packages/ripple-nuxt-tide/lib/core/tide.js | 52 +++++----- .../ripple-nuxt-tide/lib/layouts/default.vue | 43 ++++++--- .../ripple-nuxt-tide/lib/templates/plugin.js | 6 +- .../components/TideLogin.vue | 5 +- .../authenticated-content/lib/authenticate.js | 96 +++++++++++++++++++ .../authenticated-content/lib/preview.js | 13 --- .../authenticated-content/localstorage.js | 21 ---- .../modules/authenticated-content/module.js | 2 - .../authenticated-content/pages/Login.vue | 3 +- .../modules/authenticated-content/plugin.js | 13 +-- packages/ripple-nuxt-tide/package.json | 1 + .../test/unit/authenticated-content.test.js | 3 +- packages/ripple-test-tools/page-models.js | 3 +- packages/ripple-test-tools/tide-admin.js | 33 ++++--- .../e2e/fixtures/Pages/LandingPage/draft.json | 21 ++++ .../Preview-content/PreviewContent.feature | 34 +++++++ .../PreviewContent/step_definition.js | 63 ++++++++++++ test/e2e/integration/common/index.js | 3 +- 19 files changed, 328 insertions(+), 112 deletions(-) create mode 100644 packages/ripple-nuxt-tide/modules/authenticated-content/lib/authenticate.js delete mode 100644 packages/ripple-nuxt-tide/modules/authenticated-content/localstorage.js create mode 100644 test/e2e/fixtures/Pages/LandingPage/draft.json create mode 100644 test/e2e/integration/Preview-content/PreviewContent.feature create mode 100644 test/e2e/integration/Preview-content/PreviewContent/step_definition.js diff --git a/packages/ripple-nuxt-tide/lib/core/middleware.js b/packages/ripple-nuxt-tide/lib/core/middleware.js index 6b2a874bd..18fca8765 100644 --- a/packages/ripple-nuxt-tide/lib/core/middleware.js +++ b/packages/ripple-nuxt-tide/lib/core/middleware.js @@ -1,5 +1,6 @@ import { metatagConverter, pathToClass } from './tide-helper' -import { isPreviewPath, isTokenExpired } from '../../modules/authenticated-content/lib/preview' +import { isTokenExpired, getToken, clearToken } from '../../modules/authenticated-content/lib/authenticate' +import { isPreviewPath } from '../../modules/authenticated-content/lib/preview' // Fetch page data from Tide API by current path export default async function (context, results) { @@ -13,27 +14,29 @@ export default async function (context, results) { const mapping = context.app.$tideMapping let tideParams = {} - // Pass the protected content JWT from store so it can be added as a header - const { token: authToken } = context.store.state.tideAuthenticatedContent - if (authToken) { - // If token expired clear the persisted state - if (isTokenExpired(authToken)) { - context.store.dispatch('tideAuthenticatedContent/clearToken') - } else { - tideParams.auth_token = authToken + const authContentEnabled = context.app.$tide.isModuleEnabled('authenticatedContent') + let authToken = null + if (authContentEnabled) { + // Pass the protected content JWT from store so it can be added as a header + authToken = getToken() + if (authToken) { + // If token expired clear the persisted state + if (isTokenExpired(authToken)) { + clearToken(context.store) + } } } try { let response = null - if (isPreviewPath(context.route.path)) { + if (authContentEnabled && isPreviewPath(context.route.path)) { if (!authToken) { return context.redirect('/login?destination=' + context.req.url) } const { 2: type, 3: id, 4: rev } = context.route.path.split('/') const section = context.route.query.section ? context.route.query.section : null - response = await context.app.$tide.getPreviewPage(type, id, rev, section, tideParams) + response = await context.app.$tide.getPreviewPage(type, id, rev, section, tideParams, authToken) } else { response = await context.app.$tide.getPageByPath(context.route.fullPath, tideParams) } diff --git a/packages/ripple-nuxt-tide/lib/core/tide.js b/packages/ripple-nuxt-tide/lib/core/tide.js index e30aea938..7c6947b32 100644 --- a/packages/ripple-nuxt-tide/lib/core/tide.js +++ b/packages/ripple-nuxt-tide/lib/core/tide.js @@ -6,12 +6,19 @@ import _ from 'lodash' import * as helper from './tide-helper' import * as pageTypes from './page-types' import * as middleware from './middleware-helper' -import { isTokenExpired } from '../../modules/authenticated-content/lib/preview' +import { isTokenExpired } from '../../modules/authenticated-content/lib/authenticate' const apiPrefix = '/api/v1/' export const tide = (axios, site, config) => ({ - get: async function (resource, params = {}, id = '') { + /** + * GET request to tide for resources. + * @param {String} resource Resource type e.g. / + * @param {Object} params Object to convert to QueryString. Passed in URL. + * @param {String} id Resource UUID + * @param {String} authToken Authentication token + */ + get: async function (resource, params = {}, id = '', authToken) { const siteParam = 'site=' + site const url = `${apiPrefix}${resource}${id ? `/${id}` : ''}?${siteParam}${Object.keys(params).length ? `&${qs.stringify(params, { indices: false })}` : ''}` let headers = {} @@ -20,25 +27,17 @@ export const tide = (axios, site, config) => ({ console.info(`Tide request url: ${url}`) } - // Set Session cookie if is available in parameters - if (typeof params.session_name !== 'undefined' && typeof params.session_value !== 'undefined') { - _.merge(headers, { Cookie: params.session_name + '=' + params.session_value }) - } - - // Set 'X-CSRF-Token if token parameters is defined - if (typeof params.token !== 'undefined') { - _.merge(headers, { 'X-CSRF-Token': params.token }) - } - - // Set 'X-Authorization' header if auth_token present - if (params.auth_token) { - if (!isTokenExpired(params.auth_token)) { - _.merge(headers, { 'X-Authorization': `Bearer ${params.auth_token}` }) - } else { + if (this.isModuleEnabled('authenticatedContent')) { + // Set 'X-Authorization' header if authToken present + if (authToken) { + if (!isTokenExpired(authToken)) { + _.merge(headers, { 'X-Authorization': `Bearer ${authToken}` }) + } else { + delete config.headers['X-Authorization'] + } + } else if (config.headers && config.headers['X-Authorization']) { delete config.headers['X-Authorization'] } - } else if (config.headers && config.headers['X-Authorization']) { - delete config.headers['X-Authorization'] } // If headers is not empty add to config request @@ -91,8 +90,13 @@ export const tide = (axios, site, config) => ({ return sitesDomainMap }, + /** + * Check if a module is enabled. + * @param {String} checkForModule name of module + * @returns {Boolean} + */ isModuleEnabled: function (checkForModule) { - return config.modules[checkForModule] === 1 + return config && config.modules && config.modules[checkForModule] === 1 }, getSiteData: async function (tid = null) { @@ -233,7 +237,7 @@ export const tide = (axios, site, config) => ({ return pageTypes.getTemplate(type) }, - getEntityByPathData: async function (pathData, query) { + getEntityByPathData: async function (pathData, query, authToken) { const endpoint = `${pathData.entity_type}/${pathData.bundle}/${pathData.uuid}` let include @@ -289,7 +293,7 @@ export const tide = (axios, site, config) => ({ if (!_.isEmpty(query)) { params = _.merge(query, params) } - const entity = await this.get(endpoint, params) + const entity = await this.get(endpoint, params, '', authToken) return entity }, @@ -312,7 +316,7 @@ export const tide = (axios, site, config) => ({ return pageData }, - getPreviewPage: async function (contentType, uuid, revisionId, section, params) { + getPreviewPage: async function (contentType, uuid, revisionId, section, params, authToken) { if (revisionId === 'latest') { params.resourceVersion = 'rel:working-copy' } else { @@ -324,7 +328,7 @@ export const tide = (axios, site, config) => ({ bundle: contentType, uuid: uuid } - const entity = await this.getEntityByPathData(pathData, params) + const entity = await this.getEntityByPathData(pathData, params, authToken) const pageData = jsonapiParse.parse(entity).data // Append the site section to page data diff --git a/packages/ripple-nuxt-tide/lib/layouts/default.vue b/packages/ripple-nuxt-tide/lib/layouts/default.vue index 3b603a93c..069d67f87 100644 --- a/packages/ripple-nuxt-tide/lib/layouts/default.vue +++ b/packages/ripple-nuxt-tide/lib/layouts/default.vue @@ -49,7 +49,8 @@ import { RplBaseLayout } from '@dpc-sdp/ripple-layout' import RplSiteFooter from '@dpc-sdp/ripple-site-footer' import RplSiteHeader from '@dpc-sdp/ripple-site-header' import markupPlugins from '@dpc-sdp/ripple-nuxt-tide/lib/core/markup-plugins' -import { isPreviewPath, isTokenExpired } from '@dpc-sdp/ripple-nuxt-tide/modules/authenticated-content/lib/preview' +import { isTokenExpired, getToken, clearToken, isAuthenticated } from '@dpc-sdp/ripple-nuxt-tide/modules/authenticated-content/lib/authenticate' +import { isPreviewPath } from '@dpc-sdp/ripple-nuxt-tide/modules/authenticated-content/lib/preview' export default { components: { @@ -84,21 +85,33 @@ export default { return false }, showLogout () { - return Boolean(this.$store.state.tideAuthenticatedContent.token) + if (this.$tide.isModuleEnabled('authenticatedContent')) { + return isAuthenticated(this.$store) + } + return false }, preview () { - const token = this.$store.state.tideAuthenticatedContent.token - return isPreviewPath(this.$route.path) && token && !isTokenExpired(token) + if (this.$tide.isModuleEnabled('authenticatedContent')) { + if (isAuthenticated(this.$store)) { + const token = getToken() + return isPreviewPath(this.$route.path) && token && !isTokenExpired(token) + } + } + return false } }, methods: { async logoutFunc () { - try { - await this.$tide.post(`user/logout?_format=json`) - this.$store.dispatch('tideAuthenticatedContent/clearToken') - this.$router.push({ path: '/' }) - } catch (e) { - console.log(`Tide logout failed`) + if (this.$tide.isModuleEnabled('authenticatedContent')) { + try { + await this.$tide.post(`user/logout?_format=json`) + clearToken(this.$store) + this.$router.push({ path: '/' }) + } catch (e) { + console.log(`Tide logout failed`) + } + } else { + console.warn(`Authentication module is disabled - unable to log out`) } }, searchFunc (searchInput) { @@ -134,10 +147,12 @@ export default { this.rplOptions.rplMarkup = { plugins: markupPlugins } - // If logged in and session has expired, logout the user - if (this.showLogout) { - if (isTokenExpired(this.$store.state.tideAuthenticatedContent.token)) { - this.logoutFunc() + if (this.$tide.isModuleEnabled('authenticatedContent')) { + // If logged in and session has expired, logout the user + if (this.showLogout) { + if (isTokenExpired(getToken())) { + this.logoutFunc() + } } } } diff --git a/packages/ripple-nuxt-tide/lib/templates/plugin.js b/packages/ripple-nuxt-tide/lib/templates/plugin.js index ad37ac894..d5916f59e 100644 --- a/packages/ripple-nuxt-tide/lib/templates/plugin.js +++ b/packages/ripple-nuxt-tide/lib/templates/plugin.js @@ -1,6 +1,6 @@ import { tide, Mapping } from '@dpc-sdp/ripple-nuxt-tide/lib/core' import { search } from '@dpc-sdp/ripple-nuxt-tide/modules/search/index.js' -import Cookies from "js-cookie"; +import { serverSetToken } from '@dpc-sdp/ripple-nuxt-tide/modules/authenticated-content/lib/authenticate' export default ({ env, app, req, res, store , route}, inject) => { // We need to serialize functions, so use `serialize` instead of `JSON.stringify`. @@ -101,6 +101,10 @@ export default ({ env, app, req, res, store , route}, inject) => { if (config.modules.alert === 1) { await store.dispatch('tideAlerts/init') } + // Load authenticated content store. + if (config.modules.authenticatedContent === 1) { + serverSetToken(req.headers.cookie, store) + } } }, setCurrentUrl ({ commit }, fullPath) { diff --git a/packages/ripple-nuxt-tide/modules/authenticated-content/components/TideLogin.vue b/packages/ripple-nuxt-tide/modules/authenticated-content/components/TideLogin.vue index ed3e7c792..5ba3d5e24 100644 --- a/packages/ripple-nuxt-tide/modules/authenticated-content/components/TideLogin.vue +++ b/packages/ripple-nuxt-tide/modules/authenticated-content/components/TideLogin.vue @@ -27,6 +27,7 @@