Skip to content

Commit 3030f87

Browse files
velimdhmcts-jenkins-a-to-c[bot]nigeldunne
authored
DTSCCI-136 authorise user coming from cui (#3695)
* DTSCCI-136 use openid connect to use existing session to connect. * clean up * Revert "clean up" This reverts commit 29e0fb0. * fix test * fix logout * config redirect when signing out so that it depends on the environment. * Bumping chart version/ fixing aliases * fix unit test * add unit test * clean up * fix linking error * redirect to claims detail page from CUI * clean up * fix test * fix test (2) * fix test (3) * fix test (4) * fix test (5) * fix test (6) * fix test (7) * fix test + add more * correct check for ocmc redirect url * consider state not encoded * fix test * clean up import * clean up * update yarn-audit-known-issues * set default redirect to false * update yarn-audit-known-issues * logout after scenario is done * set parallel FT to 1 * Bumping chart version/ fixing aliases * record step by step failed tests * undo single FT run * remove unnecessary chart config + clean up * click sign out instead of using the url * clean up test after method * clean up config * set custom env variable confirm value to boolean * Add some logging * Bumping chart version/ fixing aliases * make sure flag is converted to boolean * clean up after merging master * update yarn-audit-known-issues * remove logout call after certain test and just restart browser between tests * Bumping chart version/ fixing aliases * clean up * clean up * undo unnecessary changes * test with toggle switched on * Revert "test with toggle switched on" This reverts commit 1d9e97b. * remove logs * clean up name for toggle --------- Co-authored-by: hmcts-jenkins-a-to-c <62422075+hmcts-jenkins-a-to-c[bot]@users.noreply.github.com> Co-authored-by: Nigel Dunne <12184413+nigeldunne@users.noreply.github.com>
1 parent f47ae94 commit 3030f87

File tree

12 files changed

+181
-54
lines changed

12 files changed

+181
-54
lines changed

charts/cmc-citizen-frontend/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
apiVersion: v2
22
name: cmc-citizen-frontend
33
home: https://github.com/hmcts/cmc-citizen-frontend
4-
version: 4.1.57
4+
version: 4.1.58
55
description: Helm chart for the HMCTS CMC Citizen Frontend service
66
# be aware when bumping version that it is used elsewhere, e.g.:
77
# chart-cmc - demo: https://github.com/hmcts/chart-cmc/tree/master/cmc

charts/cmc-citizen-frontend/values.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ nodejs:
3030
DRAFT_STORE_URL: http://draft-store-service-{{ .Values.global.environment }}.service.core-compute-{{ .Values.global.environment }}.internal
3131
CLAIM_STORE_URL: http://cmc-claim-store-{{ .Values.global.environment }}.service.core-compute-{{ .Values.global.environment }}.internal
3232
CUI_DASHBOARD_URL: https://civil-citizen-ui.{{ .Values.global.environment }}.platform.hmcts.net/dashboard
33-
CUI_DASHBOARD_REDIRECT: false
33+
CUI_URL: https://civil-citizen-ui.{{ .Values.global.environment }}.platform.hmcts.net
3434

3535
UV_THREADPOOL_SIZE: 64
3636

@@ -54,6 +54,8 @@ nodejs:
5454
FEATURE_BREATHING_SPACE: false
5555
FEES_API_KEYWORDS_ENABLE: false
5656
FEATURE_DISABLE_PAGES: false
57+
CUI_DASHBOARD_REDIRECT: false
58+
CUI_SIGN_OUT_REDIRECT: false
5759

5860
keyVaults:
5961
cmc:

config/custom-environment-variables.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,16 @@
2828
"apiVersion": "DRAFT_STORE_API_VERSION"
2929
},
3030
"cui": {
31+
"url": "CUI_URL",
32+
"signOutRedirect": {
33+
"__name": "CUI_SIGN_OUT_REDIRECT",
34+
"__format": "boolean"
35+
},
36+
"dashboardUrl": "CUI_DASHBOARD_URL",
3137
"dashboardRedirect": {
3238
"__name": "CUI_DASHBOARD_REDIRECT",
3339
"__format": "boolean"
34-
},
35-
"dashboardUrl": "CUI_DASHBOARD_URL"
40+
}
3641
},
3742
"analytics": {
3843
"gaTrackingId": "GA_TRACKING_ID"

config/default.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
"referrerPolicy": "origin"
44
},
55
"cui": {
6-
"dashboardRedirect": false,
7-
"dashboardUrl": "https://www.moneyclaims.service.gov.uk/dashboard"
6+
"url": "http://localhost:3001",
7+
"signOutRedirect": false,
8+
"dashboardUrl": "https://www.moneyclaims.service.gov.uk/dashboard",
9+
"dashboardRedirect": false
810
},
911
"idam": {
1012
"authentication-web": {
11-
"url": "http://localhost:9002"
13+
"url": "http://localhost:9002",
14+
"scope": "openid profile roles"
1215
},
1316
"api": {
1417
"url": "http://localhost:5000"

default.conf.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ exports.config = {
3131
browser: process.env.BROWSER || 'chrome',
3232
url: process.env.CITIZEN_APP_URL || 'https://localhost:3000',
3333
waitForTimeout: 60000,
34-
restart: false,
34+
restart: true,
3535
smartWait:5000,
3636
desiredCapabilities: {
3737
chromeOptions: {

src/main/app/idam/oAuthHelper.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,45 @@ import * as config from 'config'
22
import * as uuid from 'uuid'
33
import * as Cookies from 'cookies'
44
import * as express from 'express'
5+
import * as toBoolean from 'to-boolean'
56
import { buildURL } from 'utils/callbackBuilder'
67
import { Paths } from 'paths'
78
import { RoutablePath } from 'shared/router/routablePath'
89
import { User } from 'idam/user'
10+
import { Base64 } from 'js-base64'
911

1012
const clientId = config.get<string>('oauth.clientId')
11-
12-
const loginPath = `${config.get('idam.authentication-web.url')}/login`
13+
const scope = config.get('idam.authentication-web.scope')
14+
const baseCivilCitizenUrl = config.get('cui.url')
15+
const redirectToCivil = toBoolean(config.get('cui.signOutRedirect'))
16+
const idamWebUrl = config.get('idam.authentication-web.url')
17+
const loginPath = `${idamWebUrl}/login`
18+
const authorizePath = `${idamWebUrl}/o/authorize`
19+
const logoutPath = `${idamWebUrl}/o/endSession`
20+
export const redirectToClaimRegex = /^\/dashboard\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/(claimant|defendant)$/
1321

1422
export class OAuthHelper {
1523

1624
static forLogin (req: express.Request,
1725
res: express.Response,
1826
receiver: RoutablePath = Paths.receiver): string {
1927
const redirectUri = buildURL(req, receiver.uri)
20-
const state = uuid()
21-
OAuthHelper.storeStateCookie(req, res, state)
28+
const stateId = uuid()
29+
OAuthHelper.storeStateCookie(req, res, stateId)
30+
let stateObj = { 'state': stateId }
31+
if (req.originalUrl.match(redirectToClaimRegex)) {
32+
stateObj['redirectToClaim'] = req.originalUrl
33+
}
34+
const state = Base64.encode(JSON.stringify(stateObj))
2235

23-
return `${loginPath}?response_type=code&state=${state}&client_id=${clientId}&redirect_uri=${redirectUri}`
36+
return `${authorizePath}?response_type=code&state=${state}&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`
37+
}
38+
39+
static forLogout (req: express.Request,
40+
authToken: string,
41+
receiver: RoutablePath = Paths.receiver): string {
42+
const redirectUri = redirectToCivil ? `${baseCivilCitizenUrl}/logout` : buildURL(req, receiver.uri)
43+
return `${logoutPath}?id_token_hint=${authToken}&post_logout_redirect_uri=${redirectUri}`
2444
}
2545

2646
static forPin (req: express.Request, res: express.Response, claimReference: string): string {

src/main/routes/logout.ts

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,19 @@ import * as express from 'express'
22
import * as config from 'config'
33
import * as Cookies from 'cookies'
44

5-
import { IdamClient } from 'idam/idamClient'
6-
import { Logger } from '@hmcts/nodejs-logging'
7-
85
import { Paths } from 'paths'
9-
import { JwtExtractor } from 'idam/jwtExtractor'
10-
import { JwtUtils } from 'shared/utils/jwtUtils'
116
import { ErrorHandling } from 'shared/errorHandling'
7+
import { OAuthHelper } from 'idam/oAuthHelper'
128

139
const sessionCookie = config.get<string>('session.cookieName')
14-
const logger = Logger.getLogger('routes/logout')
1510

1611
/* tslint:disable:no-default-export */
1712
export default express.Router()
1813
.get(Paths.logoutReceiver.uri,
1914
ErrorHandling.apply(async (req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> => {
20-
const jwt: string = JwtExtractor.extract(req)
21-
22-
if (jwt) {
23-
try {
24-
const user: User = await IdamClient.retrieveUserFor(jwt)
25-
await IdamClient.invalidateSession(jwt, user.bearerToken)
26-
} catch (error) {
27-
const { id } = JwtUtils.decodePayload(jwt)
28-
logger.error(`Failed invalidating JWT for userId ${id}`)
29-
}
30-
31-
const cookies = new Cookies(req, res)
32-
cookies.set(sessionCookie, '')
33-
}
34-
35-
res.redirect(Paths.homePage.uri)
15+
const cookies = new Cookies(req, res)
16+
const authToken = cookies.get(sessionCookie)
17+
cookies.set(sessionCookie, '')
18+
res.redirect(OAuthHelper.forLogout(req, authToken))
3619
})
3720
)

src/main/routes/receiver.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ import { JwtExtractor } from 'idam/jwtExtractor'
1919
import { RoutablePath } from 'shared/router/routablePath'
2020
import { hasTokenExpired } from 'idam/authorizationMiddleware'
2121
import { Logger } from '@hmcts/nodejs-logging'
22-
import { OAuthHelper } from 'idam/oAuthHelper'
22+
import { OAuthHelper, redirectToClaimRegex } from 'idam/oAuthHelper'
2323
import { User } from 'idam/user'
2424
import { DraftService } from 'services/draftService'
2525
import { trackCustomEvent } from 'logging/customEventTracker'
2626
import { FeatureToggles } from 'utils/featureToggles'
2727
import { LaunchDarklyClient } from 'shared/clients/launchDarklyClient'
28+
import { Base64 } from 'js-base64'
2829

2930
const logger = Logger.getLogger('router/receiver')
3031

@@ -38,11 +39,24 @@ const eligibilityStore = new CookieEligibilityStore()
3839

3940
const featureToggles: FeatureToggles = new FeatureToggles(new LaunchDarklyClient())
4041

42+
function getPropertyFromQueryState (req: express.Request, property: string = 'state') {
43+
const state = req.query.state as string
44+
if (state) {
45+
try {
46+
return JSON.parse(Base64.decode(req.query.state))[property] as string
47+
} catch {
48+
return state
49+
}
50+
}
51+
return undefined
52+
}
53+
4154
async function getOAuthAccessToken (req: express.Request, receiver: RoutablePath): Promise<string> {
42-
if (req.query.state !== OAuthHelper.getStateCookie(req)) {
55+
const state = getPropertyFromQueryState(req)
56+
if (state !== OAuthHelper.getStateCookie(req)) {
4357
trackCustomEvent('State cookie mismatch (citizen)',
4458
{
45-
requestValue: req.query.state,
59+
requestValue: state,
4660
cookieValue: OAuthHelper.getStateCookie(req)
4761
})
4862
}
@@ -70,7 +84,7 @@ async function getAuthToken (req: express.Request,
7084

7185
function isDefendantFirstContactPinLogin (req: express.Request): boolean {
7286
if (req.query) {
73-
const state = req.query.state as string
87+
const state = getPropertyFromQueryState(req)
7488
return state ? !!state.match(/[\d]{3}MC[\d]{3}/) : false
7589
}
7690
}
@@ -91,6 +105,10 @@ function loginErrorHandler (req: express.Request,
91105
}
92106

93107
async function retrieveRedirectForLandingPage (req: express.Request, res: express.Response): Promise<string> {
108+
const redirectToClaim = getPropertyFromQueryState(req, 'redirectToClaim')
109+
if (redirectToClaim && redirectToClaimRegex.test(redirectToClaim)) {
110+
return redirectToClaim
111+
}
94112
const eligibility: Eligibility = eligibilityStore.read(req, res)
95113
if (eligibility.eligible) {
96114
const redirectUri = ClaimPaths.taskListPage.uri
@@ -146,7 +164,8 @@ export default express.Router()
146164
if (res.locals.isLoggedIn) {
147165
if (isDefendantFirstContactPinLogin(req)) {
148166
// re-set state cookie as it was cleared above, we need it in this case
149-
cookies.set(stateCookieName, req.query.state as string)
167+
const state = getPropertyFromQueryState(req)
168+
cookies.set(stateCookieName, state)
150169
return res.redirect(FirstContactPaths.claimSummaryPage.uri)
151170
} else {
152171
if (await featureToggles.isDashboardPaginationEnabled()) {
@@ -163,8 +182,9 @@ export default express.Router()
163182
}
164183
} else {
165184
if (res.locals.code) {
185+
const state = getPropertyFromQueryState(req)
166186
trackCustomEvent('Authentication token undefined (jwt defined)',
167-
{ requestValue: req.query.state })
187+
{ requestValue: state })
168188
}
169189
res.redirect(OAuthHelper.forLogin(req, res))
170190
}

src/test/app/idam/oAuthHelper.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { OAuthHelper } from 'idam/oAuthHelper'
2+
import { Request } from 'express'
3+
import * as sinon from 'sinon'
4+
import { mockRes as res } from 'sinon-express-mock'
5+
import * as Cookies from 'cookies'
6+
import { expect } from 'chai'
7+
import * as config from 'config'
8+
import { Base64 } from 'js-base64'
9+
import { beforeEach } from 'mocha'
10+
import * as uuid from 'uuid'
11+
12+
function extractStateValue (inputString: string): string {
13+
const match = inputString.match(/state=([^\s&]+)/)
14+
return match ? match[1] : ''
15+
}
16+
17+
describe('oAuthHelper', () => {
18+
describe('forLogin', () => {
19+
const defaultLoginPagePattern = new RegExp(`${config.get('idam.authentication-web.url')}/o/authorize\\?response_type=code&state=(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?&client_id=cmc_citizen&redirect_uri=https://localhost:[0-9]{1,5}/receiver&scope=openid profile roles`)
20+
21+
beforeEach(() => {
22+
sinon.stub(Cookies.prototype, 'set')
23+
})
24+
25+
afterEach(() => {
26+
Cookies.prototype.set.restore()
27+
})
28+
29+
it('should return login Idam page', () => {
30+
const req = {
31+
headers: {
32+
host: 'localhost:3000'
33+
},
34+
originalUrl: ''
35+
} as Request
36+
const loginUrl = OAuthHelper.forLogin(req, res)
37+
expect(loginUrl).to.match(defaultLoginPagePattern)
38+
})
39+
40+
it('should return login Idam page with redirectToClaim to claimant claims detail', () => {
41+
const redirectToClaim = `/dashboard/${uuid()}/claimant`
42+
const req = {
43+
headers: {
44+
host: 'localhost:3000'
45+
},
46+
originalUrl: redirectToClaim
47+
} as Request
48+
const loginUrl = OAuthHelper.forLogin(req, res)
49+
const state = extractStateValue(loginUrl)
50+
const results = JSON.parse(Base64.decode(state))['redirectToClaim']
51+
expect(results).to.equal(redirectToClaim)
52+
})
53+
54+
it('should return login Idam page with redirectToClaim to defendant claims detail', () => {
55+
const redirectToClaim = `/dashboard/${uuid()}/defendant`
56+
const req = {
57+
headers: {
58+
host: 'localhost:3000'
59+
},
60+
originalUrl: redirectToClaim
61+
} as Request
62+
const loginUrl = OAuthHelper.forLogin(req, res)
63+
const state = extractStateValue(loginUrl)
64+
const results = JSON.parse(Base64.decode(state))['redirectToClaim']
65+
expect(results).to.equal(redirectToClaim)
66+
})
67+
68+
it('should return login Idam page without redirectToClaim to claims detail if it doesnt match the expect url', () => {
69+
const redirectToClaim = '/dashboard/12345678906/defendant'
70+
const req = {
71+
headers: {
72+
host: 'localhost:3000'
73+
},
74+
originalUrl: redirectToClaim
75+
} as Request
76+
const loginUrl = OAuthHelper.forLogin(req, res)
77+
const state = extractStateValue(loginUrl)
78+
const results = JSON.parse(Base64.decode(state))['redirectToClaim']
79+
expect(results).to.equal(undefined)
80+
})
81+
})
82+
})

src/test/routes/authorization-check.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'test/routes/expectations'
88
import * as idamServiceMock from 'test/http-mocks/idam'
99

1010
const cookieName: string = config.get<string>('session.cookieName')
11-
export const defaultAccessDeniedPagePattern = new RegExp(`${config.get('idam.authentication-web.url')}/login\\?response_type=code&state=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}&client_id=cmc_citizen&redirect_uri=https://127.0.0.1:[0-9]{1,5}/receiver`)
11+
export const defaultAccessDeniedPagePattern = new RegExp(`${config.get('idam.authentication-web.url')}/o/authorize\\?response_type=code&state=(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?&client_id=cmc_citizen&redirect_uri=https://127.0.0.1:[0-9]{1,5}/receiver&scope=openid%20profile%20roles`)
1212

1313
export function checkAuthorizationGuards (app: any,
1414
method: string,

0 commit comments

Comments
 (0)