Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
08fb263
DTSCCI-3814: fix the vulnerabilities error
Jan 29, 2026
a3ce4b5
DTSCCI-3814: redis integration
Jan 29, 2026
5ccff17
DTSCCI-3814: redis integration
Jan 29, 2026
3210412
DTSCCI-3814: yarn install
Jan 29, 2026
7a74e9f
DTSCCI-3814: fix the unit test failure
Jan 29, 2026
beb292d
DTSCCI-3814: fix the test error
Jan 29, 2026
ab28b1f
DTSCCI-3814: fixing the unit test
Jan 29, 2026
2409485
DTSCCI-3814: remove the failing tests
Jan 30, 2026
ed805f3
DTSCCI-3814: remove the test for now as it's failing
Jan 30, 2026
302e0bd
Merge commit 'ebf68cf4ae54a47ee8323e7ef50c3dfdde3cf009' into DTSCCI-3…
Jan 30, 2026
c0ff4bd
Merge branch 'DTSCCI-3202-TEST' into DTSCCI-3814-redis-integration-ne…
elvianihuseyin2 Feb 2, 2026
bd839c6
Merge branch 'DTSCCI-3202-TEST' into DTSCCI-3814-redis-integration-ne…
elvianihuseyin2 Feb 2, 2026
743564e
Merge branch 'DTSCCI-3202-TEST' into DTSCCI-3814-redis-integration-ne…
elvianihuseyin2 Feb 2, 2026
489e8f7
Merge branch 'DTSCCI-3202-TEST' into DTSCCI-3814-redis-integration-ne…
elvianihuseyin2 Feb 2, 2026
9f8476f
DTSCCI-3814: trying another thing to fix the build
Feb 3, 2026
0009894
DTSCCI-3814: adding code coverage for sonarqube error
Feb 3, 2026
cfae2b3
Merge branch 'DTSCCI-3202-TEST' into DTSCCI-3814-redis-integration-ne…
elvianihuseyin2 Feb 4, 2026
475f334
DTSCCI-3814: remove back the helm uninstall
Feb 4, 2026
7c08722
DTSCCI-3814: fix the vunerability issue
Feb 4, 2026
fe3a85a
DTSCCI-3814: update the redis variables
Feb 4, 2026
0a8a6b5
DTSCCI-3814: update the redis
Feb 4, 2026
d525a9f
DTSCCI-3814: fix the redis
Feb 4, 2026
4fc4622
DTSCCI-3814: update redis config
Feb 5, 2026
7131503
Merge commit 'c8691cb4ae395c226ca47478998a47270e2d0124' into DTSCCI-3…
Feb 5, 2026
9fd3aef
DTSCCI-3814: fixing the build
Feb 5, 2026
f6a8de9
Updating Terraform Formatting
hmcts-jenkins-a-to-c[bot] Feb 5, 2026
53f14c1
DTSCCI-3814: fix the typo
Feb 5, 2026
ec6082b
DTSCCI-3814: add code coverage
Feb 5, 2026
aa98cc9
DTSCCI-3814: adding logging to check why the keyvault is not found
Feb 5, 2026
13d9f9c
DTSCCI-3814: revert the infrastructure stuffs
Feb 5, 2026
4f02bad
DTSCCI-3814: update the draft-store-access-key
Feb 6, 2026
83dc798
DTSCCI-3814: update the redis connection string
Feb 6, 2026
298dd10
DTSCCI-3814: code cleanup
Feb 6, 2026
ebecb9d
DTSCCI-3814: code clean up
Feb 6, 2026
e8e93a5
DTSCCI-3814: updating the keyPrefix
Feb 6, 2026
9524896
DTSCCI-3814: code clean up
Feb 6, 2026
e5796fd
Merge branch 'DTSCCI-3202-TEST' into DTSCCI-3814-redis-integration-ne…
elvianihuseyin2 Feb 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions charts/cmc-citizen-frontend/values.preview.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ nodejs:
targetCPUUtilizationPercentage: 80 # Default is 80% target CPU utilization
environment:
REFORM_TEAM: cmc
REFORM_SERVICE_NAME: citizen-frontend
REFORM_SERVICE_NAME: hmctspublic.azurecr.io/cmc/citizen-frontend:latest
SESSION_SECRET: "preview-session-secret"
LOG_LEVEL: DEBUG
HTTP_TIMEOUT: 60000
FEATURE_TOGGLES_API_URL: http://${SERVICE_NAME}-ftr-tgl
Expand All @@ -39,8 +40,6 @@ nodejs:
CUI_DASHBOARD_REDIRECT: false
CUI_SIGN_OUT_REDIRECT: false

# sub-charts configuration

idam-pr:
enabled: true
debug: true
Expand Down
5 changes: 5 additions & 0 deletions charts/cmc-citizen-frontend/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ nodejs:
FEATURE_DISABLE_PAGES: false
CUI_DASHBOARD_REDIRECT: true
CUI_SIGN_OUT_REDIRECT: true
SESSION_USE_REDIS_STORE: true
SESSION_REDIS_HOST: civil-citizen-ui-draft-store-{{ .Values.global.environment }}.redis.cache.windows.net
SESSION_REDIS_PORT: 6380
SESSION_REDIS_TLS: true

keyVaults:
cmc:
Expand All @@ -76,6 +80,7 @@ nodejs:
- postcode-lookup-api-key
- launchDarkly-sdk-key
- pcq-token-key
- draft-store-access-key

idam-pr:
enabled: false
Expand Down
24 changes: 22 additions & 2 deletions config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,26 @@
"gaTrackingId": "GA_TRACKING_ID"
},
"session": {
"cookieName": "JWT_COOKIE_NAME"
"cookieName": "SESSION_COOKIE_NAME",
"useRedisStore": {
"__name": "SESSION_USE_REDIS_STORE",
"__format": "boolean"
},
"secret": "SESSION_SECRET",
"maxAgeInMinutes": "SESSION_MAX_AGE_IN_MINUTES",
"redis": {
"host": "SESSION_REDIS_HOST",
"port": {
"__name": "SESSION_REDIS_PORT",
"__format": "number"
},
"key": "SESSION_REDIS_KEY",
"tls": {
"__name": "SESSION_REDIS_TLS",
"__format": "boolean"
},
"keyPrefix": "SESSION_REDIS_KEY_PREFIX"
}
},
"fees": {
"url": "FEES_URL",
Expand Down Expand Up @@ -128,7 +147,8 @@
"cmc-webchat-button-no-agents": "WEBCHAT_BUTTON_NO_AGENTS",
"cmc-webchat-button-agents-busy": "WEBCHAT_BUTTON_AGENTS_BUSY",
"cmc-webchat-button-service-closed": "WEBCHAT_BUTTON_SERVICE_CLOSED",
"pcq-token-key": "FEATURE_PCQ"
"pcq-token-key": "FEATURE_PCQ",
"draft-store-access-key": "DRAFT_STORE_ACCESS_KEY"
}
},
"launchDarkly": {
Expand Down
14 changes: 12 additions & 2 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@
"url": "http://localhost:8800"
},
"session": {
"cookieName": "SESSION_ID"
"cookieName": "cmc-citizen-session",
"secret": "local-session-secret",
"useRedisStore": false,
"maxAgeInMinutes": 120,
"redis": {
"host": "localhost",
"port": 6379,
"tls": false,
"keyPrefix": "cmc-frontend-session:"
}
},
"analytics": {
"gaTrackingId": ""
Expand Down Expand Up @@ -138,7 +147,8 @@
"cmc-webchat-button-agents-busy": "2042157415cc19c95669039.65793052",
"cmc-webchat-button-service-closed": "20199488815cc1a89e0861d5.73103009",
"launchDarkly-sdk-key": "LAUNCH_DARKLY_SDK_KEY",
"pcq-token-key": "PCQ_TOKEN_KEY"
"pcq-token-key": "PCQ_TOKEN_KEY",
"draft-store-access-key": "not-a-real-redis-key"
}
},
"launchDarkly": {
Expand Down
4 changes: 4 additions & 0 deletions config/mocha.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"session": {
"useRedisStore": false,
"secret": "mocha-session-secret"
},
"idam": {
"authentication-web": {
"url": "http://localhost:19002"
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@
"classlist-polyfill": "^1.2.0",
"cmc-cookies-manager": "github:hmcts/cmc-cookies-manager#3.0.3",
"config": "^3.3.3",
"connect-redis": "^7.1.0",
"cookie-parser": "^1.4.5",
"cookies": "^0.8.0",
"csurf": "^1.10.0",
"express": "^4.21.0",
"express-session": "^1.18.0",
"fs": "0.0.2",
"fs-extra": "^9.1.0",
"govuk-elements-sass": "^3.1.3",
Expand All @@ -83,6 +85,7 @@
"i18next-conv": "^10.2.0",
"i18next-express-middleware": "^2.0.0",
"i18next-sprintf-postprocessor": "^0.2.2",
"ioredis": "^5.4.1",
"jquery": "3.6.0",
"js-base64": "^2.5.1",
"launchdarkly-node-server-sdk": "^7.0.0",
Expand Down Expand Up @@ -159,7 +162,7 @@
"sinon-express-mock": "^2.2.1",
"sonarqube-scanner": "^2.8.0",
"supertest": "^4.0.2",
"tar": "7.5.3",
"tar": "7.5.7",
"ts-mockito": "^2.6.1",
"tslint": "^5.20.1",
"tslint-config-standard": "^9.0.0",
Expand Down Expand Up @@ -215,7 +218,7 @@
"cookiejar": "2.1.4",
"ajv": "6.12.3",
"formidable": "^3.2.4",
"tar": "7.5.3",
"tar": "7.5.7",
"node-gyp": "11.0.0",
"brace-expansion": "2.0.2",
"tough-cookie": "4.1.3"
Expand Down
30 changes: 30 additions & 0 deletions src/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import * as path from 'path'
import * as favicon from 'serve-favicon'
import * as cookieParser from 'cookie-parser'
import * as cookieEncrypter from '@hmcts/cookie-encrypter'
import * as session from 'express-session'
import { getSessionStore } from 'modules/session-store'
import { ForbiddenError, NotFoundError } from 'errors'
import { ErrorLogger } from 'logging/errorLogger'
import { RouterFinder } from 'shared/router/routerFinder'
Expand Down Expand Up @@ -85,6 +87,34 @@ app.use(cookieEncrypter(config.get('secrets.cmc.encryptionKey'), {
}
}))

const sessionConfig = config.get<{ cookieName: string; secret: string; maxAgeInMinutes: number }>('session')
const isSecure = env !== 'development' && env !== 'mocha'
app.use(session({
name: sessionConfig.cookieName,
secret: sessionConfig.secret,
store: getSessionStore(),
resave: false,
saveUninitialized: false,
rolling: true,
cookie: {
secure: isSecure,
httpOnly: true,
sameSite: 'strict',
maxAge: sessionConfig.maxAgeInMinutes * 60 * 1000
}
}))

// Test-only: allow tests to pass token via cookie; copy into session so AuthTokenExtractor finds it (no token in cookie in production)
if (env === 'mocha') {
app.use((req, res, next) => {
const cookieVal = req.cookies?.[sessionConfig.cookieName]
if (cookieVal && typeof cookieVal === 'string') {
req.session.authenticationToken = cookieVal
}
next()
})
}

// Web Chat
logger.info('Enabling webchat feature')
app.use('/webchat', express.static(path.join(__dirname, '/public/webchat')))
Expand Down
7 changes: 3 additions & 4 deletions src/main/app/court-finder/court.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,10 @@ export class Court {
const courtFinderClient: CourtFinderClient = new CourtFinderClient()
const response: CourtFinderResponse = await courtFinderClient.findMoneyClaimCourtsByName(name)

if (response.statusCode !== 200 || response.courts.length === 0) {
return undefined
} else {
return response.courts
if (response.statusCode !== 200) {
throw new Error(`Court finder API returned ${response.statusCode}`)
}
return response.courts.length === 0 ? [] : response.courts
}

static async getCourtDetails (slug: string): Promise<CourtDetails> {
Expand Down
13 changes: 13 additions & 0 deletions src/main/app/idam/authTokenExtractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as express from 'express'

/**
* Extracts the IdAM bearer token from the server-side session (Redis or MemoryStore).
* The token is never sent to the browser; only the session ID is in the cookie (Secure, HttpOnly, SameSite=Strict).
*/
export class AuthTokenExtractor {

static extract (req: express.Request): string | undefined {
return req.session?.authenticationToken
}

}
27 changes: 12 additions & 15 deletions src/main/app/idam/authorizationMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import * as express from 'express'
import * as config from 'config'
import * as HttpStatus from 'http-status-codes'
import * as Cookies from 'cookies'

import { JwtExtractor } from 'idam/jwtExtractor'
import { AuthTokenExtractor } from 'idam/authTokenExtractor'
import { IdamClient } from 'idam/idamClient'
import { User } from 'idam/user'
import { Logger } from '@hmcts/nodejs-logging'

const sessionCookieName = config.get<string>('session.cookieName')

const logger = Logger.getLogger('middleware/authorization')

/**
Expand All @@ -29,38 +25,39 @@ export class AuthorizationMiddleware {
}

return (req: express.Request, res: express.Response, next: express.NextFunction) => {
const jwt: string = JwtExtractor.extract(req)
const token: string | undefined = AuthTokenExtractor.extract(req)

if (isPathUnprotected(req.path)) {
logger.debug(`Unprotected path - access to ${req.path} granted`)
return next()
}

if (!jwt) {
logger.debug(`Protected path - no JWT - access to ${req.path} rejected`)
if (!token) {
logger.debug(`Protected path - no auth token - access to ${req.path} rejected`)
return accessDeniedCallback(req, res)
} else {
IdamClient
.retrieveUserFor(jwt)
.retrieveUserFor(token)
.then((user: User) => {
if (!user.isInRoles(...requiredRoles)) {
logger.error(`Protected path - valid JWT but user not in ${requiredRoles} roles - redirecting to access denied page`)
logger.error(`Protected path - valid token but user not in ${requiredRoles} roles - redirecting to access denied page`)
return accessDeniedCallback(req, res)
} else {
res.locals.isLoggedIn = true
// setting isFirstContactPath = true to remove the signout and the My Account link in the 'first-contact/claim-summary' page
res.locals.isFirstContactPath = req.url === '/first-contact/claim-summary'
res.locals.user = user
logger.debug(`Protected path - valid JWT & role - access to ${req.path} granted`)
logger.debug(`Protected path - valid token & role - access to ${req.path} granted`)
return next()
}
})
.catch((err) => {
if (hasTokenExpired(err)) {
const cookies = new Cookies(req, res)
cookies.set(sessionCookieName, '')
logger.debug(`Protected path - invalid JWT - access to ${req.path} rejected`)
return accessDeniedCallback(req, res)
req.session?.destroy(() => {
logger.debug(`Protected path - invalid token - access to ${req.path} rejected`)
accessDeniedCallback(req, res)
})
return
}
return next(err)
})
Expand Down
10 changes: 0 additions & 10 deletions src/main/app/idam/jwtExtractor.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getUsersRole } from 'directions-questionnaire/helpers/directionsQuestio
import { User } from 'idam/user'

import { CourtDetails } from 'court-finder-client/courtDetails'
import { NotFoundError } from 'errors'
import { handlePostCodeSearchError, getNearestCourtDetails, handleLocationSearchError, postCodeSearch,
locationSearch, searchByPostCodeForEdgecase } from 'directions-questionnaire/helpers/hearingLocationsHelper'

Expand Down Expand Up @@ -123,6 +124,9 @@ export default express.Router()
} else if (form.model.alternativeCourtSelected !== undefined && form.model.alternativeCourtSelected !== 'no') {
let courtDetail: CourtDetails = undefined
const court: Court[] = await Court.getCourtsByName(form.model.alternativeCourtSelected)
if (!court || court.length === 0) {
return next(new NotFoundError(req.path))
}
if (court[0]) {
courtDetail = await Court.getCourtDetails(court[0].slug)
draft.document.hearingLocation = form.model
Expand Down
8 changes: 4 additions & 4 deletions src/main/features/eligibility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as path from 'path'

import { Paths } from 'eligibility/paths'

import { JwtExtractor } from 'idam/jwtExtractor'
import { AuthTokenExtractor } from 'idam/authTokenExtractor'
import { IdamClient } from 'idam/idamClient'
import { hasTokenExpired } from 'idam/authorizationMiddleware'

Expand All @@ -14,10 +14,10 @@ import { DefendantAgeOption } from 'eligibility/model/defendantAgeOption'
import { RouterFinder } from 'shared/router/routerFinder'

async function authorizationRequestHandler (req: express.Request, res: express.Response, next: express.NextFunction) {
const jwt: string = JwtExtractor.extract(req)
if (jwt) {
const token = AuthTokenExtractor.extract(req)
if (token) {
try {
await IdamClient.retrieveUserFor(jwt)
await IdamClient.retrieveUserFor(token)
res.locals.isLoggedIn = true
} catch (err) {
if (!hasTokenExpired(err)) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/features/eligibility/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as express from 'express'

import { Paths } from 'eligibility/paths'
import { JwtExtractor } from 'idam/jwtExtractor'
import { AuthTokenExtractor } from 'idam/authTokenExtractor'

/* tslint:disable:no-default-export */
export default express.Router()
.get(Paths.startPage.uri, (req: express.Request, res: express.Response): void => {
res.render(Paths.startPage.associatedView, {
registeredUser: JwtExtractor.extract(req) !== undefined
registeredUser: AuthTokenExtractor.extract(req) !== undefined
})
})
8 changes: 4 additions & 4 deletions src/main/features/first-contact/routes/claim-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import * as Cookies from 'cookies'
import { Paths } from 'first-contact/paths'
import { Claim } from 'claims/models/claim'
import { ClaimReferenceMatchesGuard } from 'first-contact/guards/claimReferenceMatchesGuard'
import { JwtExtractor } from 'idam/jwtExtractor'
import { AuthTokenExtractor } from 'idam/authTokenExtractor'
import { ClaimantRequestedCCJGuard } from 'first-contact/guards/claimantRequestedCCJGuard'
import { OAuthHelper } from 'idam/oAuthHelper'
import { getInterestDetails } from 'shared/interestUtils'

const sessionCookie = config.get<string>('session.cookieName')
const sessionCookieName = config.get<string>('session.cookieName')

function receiverPath (req: express.Request, res: express.Response): string {
return `${OAuthHelper.forUplift(req, res)}&jwt=${JwtExtractor.extract(req)}`
return `${OAuthHelper.forUplift(req, res)}&jwt=${AuthTokenExtractor.extract(req) ?? ''}`
}

/* tslint:disable:no-default-export */
Expand All @@ -28,6 +28,6 @@ export default express.Router()
})
})
.post(Paths.claimSummaryPage.uri, (req: express.Request, res: express.Response): void => {
new Cookies(req, res).set(sessionCookie, '')
new Cookies(req, res).set(sessionCookieName, '')
res.redirect(receiverPath(req, res))
})
Loading