Skip to content

Commit

Permalink
chore: refactors
Browse files Browse the repository at this point in the history
  • Loading branch information
justynoh committed Aug 18, 2023
1 parent 7ca931a commit ad06fe7
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 89 deletions.
7 changes: 2 additions & 5 deletions src/app/modules/auth/sgid/auth-sgid.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { generatePkcePair } from '@opengovsg/sgid-client'
import { StatusCodes } from 'http-status-codes'
import { ErrorDto, GetSgidAuthUrlResponseDto } from 'shared/types'

Expand All @@ -24,10 +23,8 @@ export const generateAuthUrl: ControllerHandler<
...createReqMeta(req),
}

const { codeChallenge, codeVerifier } = generatePkcePair()

return AuthSgidService.createRedirectUrl(codeChallenge)
.map((redirectUrl) =>
return AuthSgidService.createRedirectUrl()
.map(({ redirectUrl, codeVerifier }) =>
res
.status(StatusCodes.OK)
.cookie(SGID_CODE_VERIFIER_COOKIE_NAME, codeVerifier)
Expand Down
14 changes: 9 additions & 5 deletions src/app/modules/auth/sgid/auth-sgid.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SgidClient } from '@opengovsg/sgid-client'
import { generatePkcePair, SgidClient } from '@opengovsg/sgid-client'
import fs from 'fs'
import { err, ok, Result, ResultAsync } from 'neverthrow'

Expand Down Expand Up @@ -41,22 +41,26 @@ export class AuthSgidServiceClass {

/**
* Create a URL to sgID which is used to redirect the user for authentication
* @returns The redirectUrl and the associated code verifier
*/
createRedirectUrl(
codeChallenge: string,
): Result<string, SgidCreateRedirectUrlError> {
createRedirectUrl(): Result<
{ redirectUrl: string; codeVerifier: string },
SgidCreateRedirectUrlError
> {
const logMeta = {
action: 'createRedirectUrl',
}

const { codeChallenge, codeVerifier } = generatePkcePair()

try {
const result = this.client.authorizationUrl({
state: SGID_LOGIN_OAUTH_STATE,
scope: ['openid', SGID_OGP_WORK_EMAIL_SCOPE].join(' '),
nonce: null,
codeChallenge,
})
return ok(result.url)
return ok({ redirectUrl: result.url, codeVerifier })
} catch (error) {
logger.error({
message: 'Error while creating redirect URL',
Expand Down
179 changes: 105 additions & 74 deletions src/app/modules/form/public-form/public-form.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { generatePkcePair } from '@opengovsg/sgid-client'
import { celebrate, Joi, Segments } from 'celebrate'
import { StatusCodes } from 'http-status-codes'
import { err } from 'neverthrow'
import { err, ok, Result } from 'neverthrow'
import { UnreachableCaseError } from 'ts-essentials'

import {
Expand Down Expand Up @@ -39,7 +38,11 @@ import {
SGID_MYINFO_COOKIE_NAME,
SGID_MYINFO_LOGIN_COOKIE_NAME,
} from '../../sgid/sgid.constants'
import { SgidInvalidJwtError, SgidVerifyJwtError } from '../../sgid/sgid.errors'
import {
SgidInvalidJwtError,
SgidMalformedMyInfoCookieError,
SgidVerifyJwtError,
} from '../../sgid/sgid.errors'
import { SgidService } from '../../sgid/sgid.service'
import { validateSgidForm } from '../../sgid/sgid.util'
import { InvalidJwtError, VerifyJwtError } from '../../spcp/spcp.errors'
Expand Down Expand Up @@ -276,55 +279,79 @@ export const handleGetPublicForm: ControllerHandler<
return res.json({ form: publicForm, isIntranetUser })
})
case FormAuthType.SGID_MyInfo: {
const { jwt: accessToken = '', sub = '' } = JSON.parse(
req.cookies[SGID_MYINFO_COOKIE_NAME] ?? '{}',
)
if (!accessToken) {
return res.json({
form: publicForm,
isIntranetUser,
})
}
res.clearCookie(SGID_MYINFO_COOKIE_NAME)
res.clearCookie(SGID_MYINFO_LOGIN_COOKIE_NAME)
return SgidService.extractSgidJwtMyInfoPayload(accessToken)
.asyncAndThen((auth) =>
SgidService.retrieveUserInfo({ accessToken: auth.accessToken, sub }),
)
.andThen((userInfo) => {
const data = new SGIDMyInfoData(userInfo.data)
return MyInfoService.prefillAndSaveMyInfoFields(
form._id,
data,
form.toJSON().form_fields,
).map((prefilledFields) => {
return res
.cookie(
SGID_MYINFO_LOGIN_COOKIE_NAME,
createMyInfoLoginCookie(data.getUinFin()),
MYINFO_LOGIN_COOKIE_OPTIONS,
)
.json({
form: {
...publicForm,
form_fields: prefilledFields as FormFieldDto[],
},
spcpSession: { userName: data.getUinFin() },
isIntranetUser,
})
})
})
.mapErr((error) => {
const parseSgidMyInfoCookie = Result.fromThrowable(
() =>
JSON.parse(req.cookies[SGID_MYINFO_COOKIE_NAME] ?? '{}') as {
jwt?: string
sub?: string
},
(error) => {
logger.error({
message: 'sgID: MyInfo login error',
message: 'Error while calling JSON.parse on SGID MyInfo cookie',
meta: logMeta,
error,
})
return res.json({
return new SgidMalformedMyInfoCookieError()
},
)
return parseSgidMyInfoCookie()
.mapErr(() =>
res.json({
form: publicForm,
myInfoError: true,
isIntranetUser,
})
}),
)
.map(({ jwt: accessToken = '', sub = '' }) => {
if (!accessToken) {
return res.json({
form: publicForm,
isIntranetUser,
})
}
res.clearCookie(SGID_MYINFO_COOKIE_NAME)
res.clearCookie(SGID_MYINFO_LOGIN_COOKIE_NAME)
return SgidService.extractSgidJwtMyInfoPayload(accessToken)
.asyncAndThen((auth) =>
SgidService.retrieveUserInfo({
accessToken: auth.accessToken,
sub,
}),
)
.andThen((userInfo) => {
const data = new SGIDMyInfoData(userInfo.data)
return MyInfoService.prefillAndSaveMyInfoFields(
form._id,
data,
form.toJSON().form_fields,
).map((prefilledFields) => {
return res
.cookie(
SGID_MYINFO_LOGIN_COOKIE_NAME,
createMyInfoLoginCookie(data.getUinFin()),
MYINFO_LOGIN_COOKIE_OPTIONS,
)
.json({
form: {
...publicForm,
form_fields: prefilledFields as FormFieldDto[],
},
spcpSession: { userName: data.getUinFin() },
isIntranetUser,
})
})
})
.mapErr((error) => {
logger.error({
message: 'sgID: MyInfo login error',
meta: logMeta,
error,
})
return res.json({
form: publicForm,
myInfoError: true,
isIntranetUser,
})
})
})
}
default:
Expand Down Expand Up @@ -459,37 +486,41 @@ export const _handleFormAuthRedirect: ControllerHandler<
})
}
case FormAuthType.SGID:
return validateSgidForm(form).andThen(() => {
const { codeChallenge, codeVerifier } = generatePkcePair()
res.cookie(
SGID_CODE_VERIFIER_COOKIE_NAME,
codeVerifier,
SgidService.getCookieSettings(),
return validateSgidForm(form)
.andThen(() =>
SgidService.createRedirectUrl(
formId,
Boolean(isPersistentLogin),
[],
encodedQuery,
),
)
return SgidService.createRedirectUrl(
formId,
Boolean(isPersistentLogin),
[],
codeChallenge,
encodedQuery,
)
})
.andThen(({ redirectUrl, codeVerifier }) => {
res.cookie(
SGID_CODE_VERIFIER_COOKIE_NAME,
codeVerifier,
SgidService.getCookieSettings(),
)
return ok(redirectUrl)
})
case FormAuthType.SGID_MyInfo:
return validateSgidForm(form).andThen(() => {
const { codeChallenge, codeVerifier } = generatePkcePair()
res.cookie(
SGID_CODE_VERIFIER_COOKIE_NAME,
codeVerifier,
SgidService.getCookieSettings(),
return validateSgidForm(form)
.andThen(() =>
SgidService.createRedirectUrl(
formId,
false,
form.getUniqueMyInfoAttrs(),
encodedQuery,
),
)
return SgidService.createRedirectUrl(
formId,
false,
form.getUniqueMyInfoAttrs(),
codeChallenge,
encodedQuery,
)
})
.andThen(({ redirectUrl, codeVerifier }) => {
res.cookie(
SGID_CODE_VERIFIER_COOKIE_NAME,
codeVerifier,
SgidService.getCookieSettings(),
)
return ok(redirectUrl)
})
default:
return err<never, AuthTypeMismatchError>(
new AuthTypeMismatchError(form.authType),
Expand Down
1 change: 0 additions & 1 deletion src/app/modules/sgid/sgid.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export const handleLogin: ControllerHandler<
return res.redirect(target)
}

// const codeVerifier = req.cookies.get(SGID_CODE_VERIFIER_COOKIE_NAME)
const codeVerifier = req.cookies[SGID_CODE_VERIFIER_COOKIE_NAME]
if (!codeVerifier) {
logger.error({
Expand Down
6 changes: 6 additions & 0 deletions src/app/modules/sgid/sgid.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export class SgidFetchUserInfoError extends ApplicationError {
}
}

export class SgidMalformedMyInfoCookieError extends ApplicationError {
constructor(message = 'SGID MyInfo cookie is malformed') {
super(message)
}
}

/**
* JWT could not be decoded.
*/
Expand Down
12 changes: 8 additions & 4 deletions src/app/modules/sgid/sgid.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SgidClient } from '@opengovsg/sgid-client'
import { generatePkcePair, SgidClient } from '@opengovsg/sgid-client'
import fs from 'fs'
import Jwt from 'jsonwebtoken'
import { err, ok, Result, ResultAsync } from 'neverthrow'
Expand Down Expand Up @@ -74,14 +74,17 @@ export class SgidServiceClass {
* @param requestedAttributes - sgID attributes requested by this form
* @param encodedQuery base64 encoded queryId for frontend to retrieve stored query params (usually contains prefilled form information)
* for an extended period of time
* @returns The redirectUrl and the associated code verifier
*/
createRedirectUrl(
formId: string,
rememberMe: boolean,
requestedAttributes: InternalAttr[],
codeChallenge: string,
encodedQuery?: string,
): Result<string, SgidCreateRedirectUrlError> {
): Result<
{ redirectUrl: string; codeVerifier: string },
SgidCreateRedirectUrlError
> {
const state = encodedQuery
? `${formId},${rememberMe},${encodedQuery}`
: `${formId},${rememberMe}`
Expand All @@ -91,14 +94,15 @@ export class SgidServiceClass {
state,
}
const scopes = internalAttrListToScopes(requestedAttributes)
const { codeChallenge, codeVerifier } = generatePkcePair()
const result = this.client.authorizationUrl({
state,
scope: scopes,
nonce: null,
codeChallenge,
})
if (typeof result.url === 'string') {
return ok(result.url)
return ok({ redirectUrl: result.url, codeVerifier })
} else {
logger.error({
message: 'Error while creating redirect URL',
Expand Down

0 comments on commit ad06fe7

Please sign in to comment.