Skip to content

Commit

Permalink
refactor: migrate isSpcpAuthenticated to TypeScript (#666)
Browse files Browse the repository at this point in the history
* ref: add mapRouteError case for VerifyJwtError

* ref: add mapRouteError case for InvalidAuthType

* ref: implement isSpcpAuthenticated

* ref: remove old isSpcpAuthenticated

* ref: use new isSpcpAuthenticated

* test: remove unused tests
  • Loading branch information
mantariksh committed Nov 19, 2020
1 parent 4665751 commit fe82a39
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 109 deletions.
41 changes: 0 additions & 41 deletions src/app/controllers/spcp.server.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,44 +305,3 @@ exports.appendVerifiedSPCPResponses = function (req, res, next) {
}
return next()
}

/**
* Checks if user is SPCP-authenticated before allowing submission
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Object} next - Express next middleware function
*/
exports.isSpcpAuthenticated = (authClients) => {
return (req, res, next) => {
const { authType } = req.form
let authClient = authClients[authType] ? authClients[authType] : undefined
if (authType && authClient) {
// form requires spcp authentication
let jwtName = jwtNames[authType]
let jwt = req.cookies[jwtName]
authClient.verifyJWT(jwt, (err, payload) => {
if (err) {
logger.error({
message: 'Failed to verify JWT with auth client',
meta: {
action: 'isSpcpAuthenticated',
...createReqMeta(req),
},
error: err,
})
res.status(StatusCodes.UNAUTHORIZED).json({
message: 'User is not SPCP authenticated',
spcpSubmissionFailure: true,
})
} else {
res.locals.uinFin = payload.userName
res.locals.userInfo = payload.userInfo
return next()
}
})
} else {
// form does not require spcp authentication
return next()
}
}
}
2 changes: 0 additions & 2 deletions src/app/factories/spcp.factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ const spcpFactory = ({ isEnabled, props }) => {
passThroughSpcp: admin.passThroughSpcp,
singPassLogin: spcp.singPassLogin(ndiConfig),
corpPassLogin: spcp.corpPassLogin(ndiConfig),
isSpcpAuthenticated: spcp.isSpcpAuthenticated(authClients),
}
} else {
const errMsg = 'SPCP/MyInfo feature is not enabled'
Expand All @@ -80,7 +79,6 @@ const spcpFactory = ({ isEnabled, props }) => {
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: errMsg }),
corpPassLogin: (req, res) =>
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: errMsg }),
isSpcpAuthenticated: (req, res, next) => next(),
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/app/modules/spcp/spcp.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,44 @@ export const addSpcpSessionInfo: RequestHandler<ParamsDictionary> = async (
return next()
})
}

/**
* Checks if user is SPCP-authenticated before allowing submission
* @param req - Express request object
* @param res - Express response object
* @param next - Express next middleware function
*/
export const isSpcpAuthenticated: RequestHandler<ParamsDictionary> = (
req,
res,
next,
) => {
const { authType } = (req as WithForm<typeof req>).form
if (!authType) return next()

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

return SpcpFactory.extractJwtPayload(jwt, authType)
.map(({ userName, userInfo }) => {
res.locals.uinFin = userName
res.locals.userInfo = userInfo
return next()
})
.mapErr((error) => {
const { statusCode, errorMessage } = mapRouteError(error)
logger.error({
message: 'Failed to verify JWT with auth client',
meta: {
action: 'isSpcpAuthenticated',
...createReqMeta(req),
authType,
},
error,
})
return res.status(statusCode).json({
message: errorMessage,
spcpSubmissionFailure: true,
})
})
}
8 changes: 8 additions & 0 deletions src/app/modules/spcp/spcp.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { MissingFeatureError } from '../core/core.errors'
import {
CreateRedirectUrlError,
FetchLoginPageError,
InvalidAuthTypeError,
LoginPageValidationError,
VerifyJwtError,
} from './spcp.errors'
import { JwtName, JwtPayload, SpcpCookies } from './spcp.types'

Expand Down Expand Up @@ -60,6 +62,7 @@ export const mapRouteError: MapRouteError = (error) => {
switch (error.constructor) {
case MissingFeatureError:
case CreateRedirectUrlError:
case InvalidAuthTypeError:
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorMessage: 'Sorry, something went wrong. Please try again.',
Expand All @@ -74,6 +77,11 @@ export const mapRouteError: MapRouteError = (error) => {
statusCode: StatusCodes.BAD_GATEWAY,
errorMessage: 'Error while contacting SingPass. Please try again.',
}
case VerifyJwtError:
return {
statusCode: StatusCodes.UNAUTHORIZED,
errorMessage: 'User is not SPCP authenticated',
}
default:
logger.error({
message: 'Unknown route error observed',
Expand Down
4 changes: 2 additions & 2 deletions src/app/routes/public-forms.server.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ module.exports = function (app) {
forms.formById,
publicForms.isFormPublic,
CaptchaFactory.captchaCheck,
spcpFactory.isSpcpAuthenticated,
SpcpController.isSpcpAuthenticated,
emailSubmissions.receiveEmailSubmissionUsingBusBoy,
celebrate({
body: Joi.object({
Expand Down Expand Up @@ -267,7 +267,7 @@ module.exports = function (app) {
publicForms.isFormPublic,
CaptchaFactory.captchaCheck,
encryptSubmissions.validateEncryptSubmission,
spcpFactory.isSpcpAuthenticated,
SpcpController.isSpcpAuthenticated,
myInfoController.verifyMyInfoVals,
submissions.injectAutoReplyInfo,
webhookVerifiedContentFactory.encryptedVerifiedFields,
Expand Down
64 changes: 0 additions & 64 deletions tests/unit/backend/controllers/spcp.server.controller.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,70 +164,6 @@ describe('SPCP Controller', () => {
}
}

describe('isSpcpAuthenticated', () => {
let next

beforeEach(() => {
req = {
form: { authType: 'SP' },
cookies: { jwtSp: 'spCookie' },
headers: {},
ip: '127.0.0.1',
get: (key) => this[key],
}
next = jasmine.createSpy()
})

const replyWith = (error, data) => {
singPassAuthClient.verifyJWT.and.callFake((jwt, cb) => {
expect(jwt).toEqual('spCookie')
cb(error, data)
})
}

const passThroughWith = (data) => {
next = jasmine.createSpy().and.callFake(() => {
expect(res.locals.uinFin).toEqual(data.userName)
expect(res.locals.userInfo).toEqual(data.userInfo)
})
}

it('should call next if authType undefined', () => {
req.form.authType = ''
Controller.isSpcpAuthenticated(authClients)(req, res, next)
expect(next).toHaveBeenCalled()
})

it('should call next if authType is NIL', () => {
req.form.authType = 'NIL'
Controller.isSpcpAuthenticated(authClients)(req, res, next)
expect(next).toHaveBeenCalled()
})

it('should return 401 if verify returns error', () => {
replyWith('error', {})
Controller.isSpcpAuthenticated(authClients)(req, res, next)
expect(next).not.toHaveBeenCalled()
expectResponse(StatusCodes.UNAUTHORIZED, {
message: 'User is not SPCP authenticated',
spcpSubmissionFailure: true,
})
})

it('should call next and set username if verify succeeds', () => {
replyWith(null, {
userName: '123',
userInfo: 'abc',
})
passThroughWith({
userName: '123',
userInfo: 'abc',
})
Controller.isSpcpAuthenticated(authClients)(req, res, next)
expect(next).toHaveBeenCalled()
})
})

describe('singPassLogin/corpPassLogin - validation', () => {
let spB64Artifact
let expectedRelayState
Expand Down

0 comments on commit fe82a39

Please sign in to comment.