Skip to content

Commit

Permalink
Merge branch 'master' into EXUI-2079-UpgradeDependenciesWithYarnChanges
Browse files Browse the repository at this point in the history
  • Loading branch information
RiteshHMCTS authored Jan 13, 2025
2 parents 5992beb + f1c479d commit 1e1b592
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 10 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## [2.30.1](https://github.com/hmcts/rpx-xui-node-lib/compare/v2.30.0...v2.30.1) (2025-01-07)


### Bug Fixes

* #exui-112: rediect to link expired login link page of state or nonce is stale ([#258](https://github.com/hmcts/rpx-xui-node-lib/issues/258)) ([f366fd2](https://github.com/hmcts/rpx-xui-node-lib/commit/f366fd262eea9a6ef9af8fd8f8051cdeb3383434)), closes [#exui-112](https://github.com/hmcts/rpx-xui-node-lib/issues/exui-112) [#exui-112](https://github.com/hmcts/rpx-xui-node-lib/issues/exui-112)

# [2.30.0](https://github.com/hmcts/rpx-xui-node-lib/compare/v2.29.7...v2.30.0) (2025-01-06)


### Features

* Tech/ex UI 2249 content security ([#256](https://github.com/hmcts/rpx-xui-node-lib/issues/256)) ([1dd3a8a](https://github.com/hmcts/rpx-xui-node-lib/commit/1dd3a8a30342d981bf4a1036ec7ad6c1881625ca))

## [2.29.7](https://github.com/hmcts/rpx-xui-node-lib/compare/v2.29.6...v2.29.7) (2024-12-18)


Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hmcts/rpx-xui-node-lib",
"version": "2.29.5-exui-2079-rc15",
"version": "2.29.5-exui-2079-rc16",
"description": "Common nodejs library components for XUI",
"main": "dist/index",
"types": "dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/auth/auth.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export const AUTH = {
KEEPALIVE_ROUTE: '/auth/keepalive',
OAUTH_CALLBACK: '/oauth2/callback',
LOGOUT: '/auth/logout',
EXPIRED_LOGIN_LINK: '/expired-login-link',
},
}
26 changes: 17 additions & 9 deletions src/auth/models/strategy.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ export abstract class Strategy extends events.EventEmitter {
this.emit(AUTH.EVENT.AUTHENTICATE_FAILURE, req, res, next)
}

const redirectWithFailure = (errorMessages: string[], INVALID_STATE_ERROR: string, uri: string) => {
errorMessages.push(INVALID_STATE_ERROR)
emitAuthenticationFailure(errorMessages)
return res.redirect(uri)
}

passport.authenticate(
this.strategyName,
{
Expand All @@ -310,19 +316,21 @@ export abstract class Strategy extends events.EventEmitter {
}

if (info) {
if (info.message === INVALID_STATE_ERROR) {
errorMessages.push(INVALID_STATE_ERROR)
}
this.logger.info('Authenticate callback info', info)
}

if (!user) {
const message = 'No user details returned by the authentication service, redirecting to login'
errorMessages.push(message)
this.logger.log(message)

emitAuthenticationFailure(errorMessages)
return res.redirect(AUTH.ROUTE.LOGIN)
const MISMATCH_NONCE = 'nonce mismatch'
const MISMATCH_STATE = 'state mismatch'
if (info?.message === INVALID_STATE_ERROR) {
return redirectWithFailure(errorMessages, INVALID_STATE_ERROR, AUTH.ROUTE.EXPIRED_LOGIN_LINK)
} else if (info?.message.includes(MISMATCH_NONCE) || info?.message.includes(MISMATCH_STATE)) {
return redirectWithFailure(errorMessages, info.message, AUTH.ROUTE.EXPIRED_LOGIN_LINK)
} else {
const message = 'No user details returned by the authentication service, redirecting to login'
this.logger.log(message)
return redirectWithFailure(errorMessages, message, AUTH.ROUTE.LOGIN)
}
}
emitAuthenticationFailure(errorMessages)
this.verifyLogin(req, user, next, res)
Expand Down
79 changes: 79 additions & 0 deletions src/auth/oauth2/models/oauth2.class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createMock } from 'ts-auto-mock'
import { Request, Response, Router } from 'express'
import { AuthOptions } from '../../models'
import { XuiLogger } from '../../../common'
import { AUTH } from '../../auth.constants'

describe('OAUTH2 Auth', () => {
const mockRequestRequired = {
Expand Down Expand Up @@ -207,4 +208,82 @@ describe('OAUTH2 Auth', () => {
expect(mockRequest.headers.Authorization).toEqual(authToken)
expect(next).toHaveBeenCalled()
})

test('should handle INVALID_STATE_ERROR in info', () => {
const info = { message: 'Invalid authorization request state.' }
jest.spyOn(passport, 'authenticate').mockImplementation((strategy, options, callback) => {
if (callback) {
callback(null, null, info)
}
return jest.fn()
})

const mockRouter = createMock<Router>()
const logger = {
log: jest.fn(),
error: jest.fn(),
info: jest.fn(),
} as unknown as XuiLogger
const oAuth2 = new OAuth2(mockRouter, logger)
const mockRequest = createMock<Request>()
const mockResponse = createMock<Response>()
const next = jest.fn()

oAuth2.callbackHandler(mockRequest, mockResponse, next)

expect(mockResponse.redirect).toHaveBeenCalledWith(AUTH.ROUTE.EXPIRED_LOGIN_LINK)
})

test('should handle MISMATCH_NONCE or MISMATCH_STATE in info', () => {
const info = { message: 'nonce mismatch' }
jest.spyOn(passport, 'authenticate').mockImplementation((strategy, options, callback) => {
if (callback) {
callback(null, null, info)
}
return jest.fn()
})

const mockRouter = createMock<Router>()
const logger = {
log: jest.fn(),
error: jest.fn(),
info: jest.fn(),
} as unknown as XuiLogger
const oAuth2 = new OAuth2(mockRouter, logger)
const mockRequest = createMock<Request>()
const mockResponse = createMock<Response>()
const next = jest.fn()

oAuth2.callbackHandler(mockRequest, mockResponse, next)

expect(mockResponse.redirect).toHaveBeenCalledWith(AUTH.ROUTE.EXPIRED_LOGIN_LINK)
})

test('should handle no user returned', () => {
const info = { message: 'Some other error' }
jest.spyOn(passport, 'authenticate').mockImplementation((strategy, options, callback) => {
if (callback) {
callback(null, null, info)
}
return jest.fn()
})

const mockRouter = createMock<Router>()
const logger = {
log: jest.fn(),
error: jest.fn(),
info: jest.fn(),
} as unknown as XuiLogger
const oAuth2 = new OAuth2(mockRouter, logger)
const mockRequest = createMock<Request>()
const mockResponse = createMock<Response>()
const next = jest.fn()

oAuth2.callbackHandler(mockRequest, mockResponse, next)

expect(logger.log).toHaveBeenCalledWith(
'No user details returned by the authentication service, redirecting to login',
)
expect(mockResponse.redirect).toHaveBeenCalledWith(AUTH.ROUTE.LOGIN)
})
})
13 changes: 13 additions & 0 deletions src/common/util/contentSecurityPolicy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getContentSecurityPolicy } from './'
import { SECURITY_POLICY } from './contentSecurityPolicy'

describe('getContentSecurityPolicy(helmet)', () => {
it('should correctly call the content security policy', () => {
const helmet: any = {
contentSecurityPolicy: (policy: any) => {
return policy
},
}
expect(getContentSecurityPolicy(helmet)).toBe(SECURITY_POLICY)
})
})
53 changes: 53 additions & 0 deletions src/common/util/contentSecurityPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export const SECURITY_POLICY = {
directives: {
connectSrc: [
"'self' blob: data:",
'*.gov.uk',
'dc.services.visualstudio.com',
'*.launchdarkly.com',
'https://*.google-analytics.com',
'https://*.googletagmanager.com',
'https://*.analytics.google.com',
'*.hmcts.net',
'wss://*.webpubsub.azure.com',
'https://*.in.applicationinsights.azure.com',
'https://*.monitor.azure.com',
],
defaultSrc: ["'self'"],
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'data:'],
formAction: ["'none'"],
frameAncestors: ["'self'"],
frameSrc: ["'self'"],
imgSrc: [
"'self'",
'data:',
'https://*.google-analytics.com',
'https://*.googletagmanager.com',
'https://raw.githubusercontent.com/hmcts/',
'https://stats.g.doubleclick.net/',
'https://ssl.gstatic.com/',
'https://www.gstatic.com/',
'https://fonts.gstatic.com',
],
mediaSrc: ["'self'"],
scriptSrc: [
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
'https://*.google-analytics.com',
'https://*.googletagmanager.com',
'az416426.vo.msecnd.net',
],
styleSrc: [
"'self'",
"'unsafe-inline'",
'https://fonts.googleapis.com',
'https://fonts.gstatic.com',
'https://www.googletagmanager.com',
],
},
}

export const getContentSecurityPolicy = (helmet: any) => {
return helmet.contentSecurityPolicy(SECURITY_POLICY)
}
1 change: 1 addition & 0 deletions src/common/util/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { hasKey } from './hasKey'
export { getLogger, XuiLogger } from './debug.logger'
export { getContentSecurityPolicy } from './contentSecurityPolicy'
export { sortArray } from './sortArray'
export { isStringPatternMatch } from './stringPatternMatch'
export { arrayPatternMatch } from './arrayPatternMatch'
Expand Down

0 comments on commit 1e1b592

Please sign in to comment.