Skip to content

Commit

Permalink
Merge pull request #918 from opengovsg/release-4.50.3
Browse files Browse the repository at this point in the history
build: release 4.50.3 - hotfix for undefined SPCP info
  • Loading branch information
mantariksh authored Dec 21, 2020
2 parents 3c74ac5 + d0c6583 commit 4d35131
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 35 deletions.
23 changes: 23 additions & 0 deletions src/app/modules/spcp/__tests__/spcp.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ import {
InvalidOOBParamsError,
LoginPageValidationError,
MissingAttributesError,
MissingJwtError,
RetrieveAttributesError,
VerifyJwtError,
} from '../spcp.errors'
import { SpcpService } from '../spcp.service'
import { JwtName } from '../spcp.types'

import {
MOCK_COOKIES,
MOCK_CP_JWT_PAYLOAD,
MOCK_CP_SAML,
MOCK_DESTINATION,
Expand Down Expand Up @@ -799,4 +802,24 @@ describe('spcp.service', () => {
expect(spcpService.getCookieSettings()).toEqual({})
})
})

describe('extractJwt', () => {
it('should return SingPass JWT correctly', () => {
const spcpService = new SpcpService(MOCK_PARAMS)
const result = spcpService.extractJwt(MOCK_COOKIES, AuthType.SP)
expect(result._unsafeUnwrap()).toEqual(MOCK_COOKIES[JwtName.SP])
})

it('should return CorpPass JWT correctly', () => {
const spcpService = new SpcpService(MOCK_PARAMS)
const result = spcpService.extractJwt(MOCK_COOKIES, AuthType.CP)
expect(result._unsafeUnwrap()).toEqual(MOCK_COOKIES[JwtName.CP])
})

it('should return MissingJwtError if there is no JWT', () => {
const spcpService = new SpcpService(MOCK_PARAMS)
const result = spcpService.extractJwt({}, AuthType.CP)
expect(result._unsafeUnwrapErr()).toEqual(new MissingJwtError())
})
})
})
7 changes: 7 additions & 0 deletions src/app/modules/spcp/__tests__/spcp.test.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import crypto from 'crypto'
import { ISpcpMyInfo } from 'src/config/feature-manager'
import { ILoginSchema, IPopulatedForm } from 'src/types'

import { JwtName } from '../spcp.types'

export const MOCK_SERVICE_PARAMS: ISpcpMyInfo = {
isSPMaintenance: 'isSPMaintenance',
isCPMaintenance: 'isCPMaintenance',
Expand Down Expand Up @@ -133,3 +135,8 @@ export const MOCK_COOKIE_SETTINGS = {
domain: 'domain',
path: 'path',
}

export const MOCK_COOKIES = {
[JwtName.SP]: 'mockSpJwt',
[JwtName.CP]: 'mockCpJwt',
}
14 changes: 6 additions & 8 deletions src/app/modules/spcp/spcp.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { JwtName, LoginPageValidationResult } from './spcp.types'
import {
createCorppassParsedResponses,
createSingpassParsedResponses,
extractJwt,
mapRouteError,
} from './spcp.util'

Expand Down Expand Up @@ -100,10 +99,11 @@ export const addSpcpSessionInfo: RequestHandler<ParamsDictionary> = async (
const { authType } = (req as WithForm<typeof req>).form
if (authType !== AuthType.SP && authType !== AuthType.CP) return next()

const jwt = extractJwt(req.cookies, authType)
if (!jwt) return next()
const jwtResult = SpcpFactory.extractJwt(req.cookies, authType)
// No action needed if JWT is missing, just means user is not logged in
if (jwtResult.isErr()) return next()

return SpcpFactory.extractJwtPayload(jwt, authType)
return SpcpFactory.extractJwtPayload(jwtResult.value, authType)
.map(({ userName }) => {
res.locals.spcpSession = { userName }
return next()
Expand Down Expand Up @@ -135,10 +135,8 @@ export const isSpcpAuthenticated: RequestHandler<ParamsDictionary> = (
const { authType } = (req as WithForm<typeof req>).form
if (authType !== AuthType.SP && authType !== AuthType.CP) return next()

const jwt = extractJwt(req.cookies, authType)
if (!jwt) return next()

return SpcpFactory.extractJwtPayload(jwt, authType)
return SpcpFactory.extractJwt(req.cookies, authType)
.asyncAndThen((jwt) => SpcpFactory.extractJwtPayload(jwt, authType))
.map(({ userName, userInfo }) => {
res.locals.uinFin = userName
res.locals.userInfo = userInfo
Expand Down
13 changes: 11 additions & 2 deletions src/app/modules/spcp/spcp.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class LoginPageValidationError extends ApplicationError {
}

/**
* Invalid JWT.
* JWT could not be decoded.
*/
export class VerifyJwtError extends ApplicationError {
constructor(message = 'Invalid JWT') {
Expand Down Expand Up @@ -77,7 +77,7 @@ export class MissingAttributesError extends ApplicationError {
}

/**
* JWT has the wrong shape
* JWT could be decoded but has the wrong shape
*/
export class InvalidJwtError extends ApplicationError {
constructor(
Expand All @@ -86,3 +86,12 @@ export class InvalidJwtError extends ApplicationError {
super(message)
}
}

/**
* JWT not present in cookies
*/
export class MissingJwtError extends ApplicationError {
constructor(message = 'No JWT present in cookies') {
super(message)
}
}
2 changes: 2 additions & 0 deletions src/app/modules/spcp/spcp.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface ISpcpFactory {
createRedirectUrl: SpcpService['createRedirectUrl']
fetchLoginPage: SpcpService['fetchLoginPage']
validateLoginPage: SpcpService['validateLoginPage']
extractJwt: SpcpService['extractJwt']
extractJwtPayload: SpcpService['extractJwtPayload']
parseOOBParams: SpcpService['parseOOBParams']
getSpcpAttributes: SpcpService['getSpcpAttributes']
Expand All @@ -32,6 +33,7 @@ export const createSpcpFactory = ({
fetchLoginPage: () => errAsync(error),
validateLoginPage: () => err(error),
extractJwtPayload: () => errAsync(error),
extractJwt: () => err(error),
parseOOBParams: () => err(error),
getSpcpAttributes: () => errAsync(error),
createJWT: () => err(error),
Expand Down
20 changes: 20 additions & 0 deletions src/app/modules/spcp/spcp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ import {
InvalidOOBParamsError,
LoginPageValidationError,
MissingAttributesError,
MissingJwtError,
RetrieveAttributesError,
VerifyJwtError,
} from './spcp.errors'
import {
CorppassAttributes,
JwtName,
JwtPayload,
LoginPageValidationResult,
ParsedSpcpParams,
SingpassAttributes,
SpcpCookies,
SpcpDomainSettings,
} from './spcp.types'
import {
Expand Down Expand Up @@ -191,6 +194,23 @@ export class SpcpService {
}
}

/**
* Extracts the SP or CP JWT from an object containing cookies
* @param cookies Object containing cookies
* @param authType 'SP' or 'CP'
*/
extractJwt(
cookies: SpcpCookies,
authType: AuthType.SP | AuthType.CP,
): Result<string, MissingJwtError> {
const jwtName = authType === AuthType.SP ? JwtName.SP : JwtName.CP
const cookie = cookies[jwtName]
if (!cookie) {
return err(new MissingJwtError())
}
return ok(cookie)
}

/**
* Verifies a JWT and extracts its payload.
* @param jwt The contents of the JWT cookie
Expand Down
29 changes: 4 additions & 25 deletions src/app/modules/spcp/spcp.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import {
FetchLoginPageError,
InvalidJwtError,
LoginPageValidationError,
MissingJwtError,
VerifyJwtError,
} from './spcp.errors'
import { JwtName, JwtPayload, SpcpCookies } from './spcp.types'
import { JwtPayload } from './spcp.types'

const logger = createLoggerWithLabel(module)
const DESTINATION_REGEX = /^\/([\w]+)\/?/
Expand Down Expand Up @@ -155,25 +156,6 @@ export const isJwtPayload = (
}
}

/**
* Extracts the SP or CP JWT from an object containing cookies
* @param cookies Object containing cookies
* @param authType 'SP' or 'CP'
*/
export const extractJwt = (
cookies: SpcpCookies,
authType: AuthType,
): string | undefined => {
switch (authType) {
case AuthType.SP:
return cookies[JwtName.SP]
case AuthType.CP:
return cookies[JwtName.CP]
default:
return undefined
}
}

/**
* Wraps SingPass data in the form of parsed form fields.
* @param uinFin UIN or FIN
Expand Down Expand Up @@ -241,16 +223,13 @@ export const mapRouteError: MapRouteError = (error) => {
statusCode: StatusCodes.BAD_GATEWAY,
errorMessage: 'Error while contacting SingPass. Please try again.',
}
case MissingJwtError:
case VerifyJwtError:
return {
statusCode: StatusCodes.UNAUTHORIZED,
errorMessage: 'User is not SPCP authenticated',
}
case InvalidJwtError:
return {
statusCode: StatusCodes.UNAUTHORIZED,
errorMessage:
'Sorry, something went wrong with your login. Please refresh and try again.',
'Something went wrong with your login. Please try logging in and submitting again.',
}
default:
logger.error({
Expand Down

0 comments on commit 4d35131

Please sign in to comment.