Skip to content

Commit

Permalink
feat: add typeguard for JWT payload
Browse files Browse the repository at this point in the history
  • Loading branch information
mantariksh committed Dec 10, 2020
1 parent 49702e3 commit 7169cfb
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 9 deletions.
14 changes: 10 additions & 4 deletions src/app/modules/spcp/__tests__/spcp.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { SpcpService } from '../spcp.service'

import {
MOCK_CP_JWT_PAYLOAD,
MOCK_CP_SAML,
MOCK_DESTINATION,
MOCK_ERROR_CODE,
Expand All @@ -36,6 +37,7 @@ import {
MOCK_LOGIN_HTML,
MOCK_REDIRECT_URL,
MOCK_SERVICE_PARAMS as MOCK_PARAMS,
MOCK_SP_JWT_PAYLOAD,
MOCK_SP_SAML,
MOCK_SP_SAML_WRONG_HASH,
MOCK_SP_SAML_WRONG_TYPECODE,
Expand Down Expand Up @@ -220,9 +222,11 @@ describe('spcp.service', () => {
const spcpService = new SpcpService(MOCK_PARAMS)
// Assumes that SP auth client was instantiated first
const mockClient = mocked(MockAuthClient.mock.instances[0], true)
mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => cb(null, jwt))
mockClient.verifyJWT.mockImplementationOnce((jwt, cb) =>
cb(null, MOCK_SP_JWT_PAYLOAD),
)
const result = await spcpService.extractJwtPayload(MOCK_JWT, AuthType.SP)
expect(result._unsafeUnwrap()).toEqual(MOCK_JWT)
expect(result._unsafeUnwrap()).toEqual(MOCK_SP_JWT_PAYLOAD)
})

it('should return VerifyJwtError when SingPass JWT is invalid', async () => {
Expand All @@ -240,9 +244,11 @@ describe('spcp.service', () => {
const spcpService = new SpcpService(MOCK_PARAMS)
// Assumes that SP auth client was instantiated first
const mockClient = mocked(MockAuthClient.mock.instances[1], true)
mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => cb(null, jwt))
mockClient.verifyJWT.mockImplementationOnce((jwt, cb) =>
cb(null, MOCK_CP_JWT_PAYLOAD),
)
const result = await spcpService.extractJwtPayload(MOCK_JWT, AuthType.CP)
expect(result._unsafeUnwrap()).toEqual(MOCK_JWT)
expect(result._unsafeUnwrap()).toEqual(MOCK_CP_JWT_PAYLOAD)
})

it('should return VerifyJwtError when CorpPass JWT is invalid', async () => {
Expand Down
6 changes: 6 additions & 0 deletions src/app/modules/spcp/__tests__/spcp.test.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export const MOCK_LOGIN_HTML = 'html'
export const MOCK_ERROR_CODE = 'errorCode'
export const MOCK_TITLE = 'title'
export const MOCK_JWT = 'jwt'

export const MOCK_SP_JWT_PAYLOAD = { userName: 'mockUserName' }
export const MOCK_CP_JWT_PAYLOAD = {
userName: 'mockUserName',
userInfo: 'mockUserInfo',
}
const spPartnerHash = crypto
.createHash('sha1')
.update(MOCK_SERVICE_PARAMS.spIdpId, 'utf8')
Expand Down
11 changes: 11 additions & 0 deletions src/app/modules/spcp/spcp.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,14 @@ export class MissingAttributesError extends ApplicationError {
super(message)
}
}

/**
* JWT has the wrong shape
*/
export class InvalidJwtError extends ApplicationError {
constructor(
message = 'Decoded JWT did not contain the correct SPCP attributes',
) {
super(message)
}
}
11 changes: 9 additions & 2 deletions src/app/modules/spcp/spcp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import axios from 'axios'
import fs from 'fs'
import { StatusCodes } from 'http-status-codes'
import mongoose from 'mongoose'
import { err, errAsync, ok, Result, ResultAsync } from 'neverthrow'
import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow'

import { ISpcpMyInfo } from '../../../config/feature-manager'
import { createLoggerWithLabel } from '../../../config/logger'
Expand All @@ -15,6 +15,7 @@ import {
AuthTypeMismatchError,
CreateRedirectUrlError,
FetchLoginPageError,
InvalidJwtError,
InvalidOOBParamsError,
LoginPageValidationError,
MissingAttributesError,
Expand All @@ -33,6 +34,7 @@ import {
extractFormId,
getAttributesPromise,
getSubstringBetween,
isJwtPayload,
isValidAuthenticationQuery,
verifyJwtPromise,
} from './spcp.util'
Expand Down Expand Up @@ -212,7 +214,12 @@ export class SpcpService {
})
return new VerifyJwtError()
},
)
).andThen((payload) => {
if (isJwtPayload(payload, authType)) {
return okAsync(payload)
}
return errAsync(new InvalidJwtError())
})
}

/**
Expand Down
37 changes: 34 additions & 3 deletions src/app/modules/spcp/spcp.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ProcessedSingleAnswerResponse } from '../submission/submission.types'
import {
CreateRedirectUrlError,
FetchLoginPageError,
InvalidJwtError,
LoginPageValidationError,
VerifyJwtError,
} from './spcp.errors'
Expand Down Expand Up @@ -119,9 +120,9 @@ export const getSubstringBetween = (
export const verifyJwtPromise = (
authClient: SPCPAuthClient,
jwt: string,
): Promise<JwtPayload> => {
return new Promise<JwtPayload>((resolve, reject) => {
authClient.verifyJWT<JwtPayload>(jwt, (error: Error, data: JwtPayload) => {
): Promise<unknown> => {
return new Promise<unknown>((resolve, reject) => {
authClient.verifyJWT<unknown>(jwt, (error: Error, data: unknown) => {
if (error) {
return reject(error)
}
Expand All @@ -130,6 +131,30 @@ export const verifyJwtPromise = (
})
}

/**
* Typeguard for JWT payload.
* @param payload Payload decrypted from JWT
*/
export const isJwtPayload = (
payload: unknown,
authType: AuthType.SP | AuthType.CP,
): payload is JwtPayload => {
if (authType === AuthType.SP) {
return (
!!payload &&
typeof payload === 'object' &&
typeof (payload as Record<string, unknown>).userName === 'string'
)
} else {
return (
!!payload &&
typeof payload === 'object' &&
typeof (payload as Record<string, unknown>).userName === 'string' &&
typeof (payload as Record<string, unknown>).userInfo === 'string'
)
}
}

/**
* Extracts the SP or CP JWT from an object containing cookies
* @param cookies Object containing cookies
Expand Down Expand Up @@ -221,6 +246,12 @@ export const mapRouteError: MapRouteError = (error) => {
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.',
}
default:
logger.error({
message: 'Unknown route error observed',
Expand Down

0 comments on commit 7169cfb

Please sign in to comment.