Skip to content

Commit

Permalink
feat: migrate corppass to OIDC (second attempt) (#4985)
Browse files Browse the repository at this point in the history
* feat: Revert "Revert "feat: migration corppass to oidc (#4248)" (#4586)"

* feat: allow foreign id in id token

* chore: fix test
  • Loading branch information
tshuli authored Oct 3, 2022
1 parent bd09d3c commit e47bd9c
Show file tree
Hide file tree
Showing 66 changed files with 7,233 additions and 2,596 deletions.
6 changes: 6 additions & 0 deletions .template-env
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,13 @@ FORMSG_SDK_MODE=
# SP_OIDC_RP_REDIRECT_URL=
# SP_OIDC_RP_JWKS_PUBLIC_PATH=
# SP_OIDC_RP_JWKS_SECRET_PATH=
# CP_OIDC_NDI_DISCOVERY_ENDPOINT=https://stg-id.corppass.gov.sg/.well-known/openid-configuration
# CP_OIDC_NDI_JWKS_ENDPOINT=https://stg-id.corppass.gov.sg/.well-known/keys
# CP_OIDC_RP_CLIENT_ID=
# CP_OIDC_RP_REDIRECT_URL=
# CP_OIDC_RP_JWKS_PUBLIC_PATH=
# CP_OIDC_RP_JWKS_SECRET_PATH=



# CP_FORMSG_KEY_PATH=./node_modules/@opengovsg/mockpass/static/certs/key.pem
Expand Down
11 changes: 8 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,14 @@ services:
- SP_OIDC_NDI_JWKS_ENDPOINT=https://stg-id.singpass.gov.sg/.well-known/keys
- SP_OIDC_RP_CLIENT_ID=rpClientId
- SP_OIDC_RP_REDIRECT_URL=https://staging.form.gov.sg/api/v3/singpass/login
- SP_OIDC_RP_JWKS_PUBLIC_PATH=./tests/certs/test_rp_public_jwks.json
- SP_OIDC_RP_JWKS_SECRET_PATH=./tests/certs/test_rp_secret_jwks.json
- CP_OIDC_RP_JWKS_PUBLIC_PATH=./tests/certs/test_rp_public_jwks.json
- SP_OIDC_RP_JWKS_PUBLIC_PATH=./tests/certs/test_sp_rp_public_jwks.json
- SP_OIDC_RP_JWKS_SECRET_PATH=./tests/certs/test_sp_rp_secret_jwks.json
- CP_OIDC_NDI_DISCOVERY_ENDPOINT=https://stg-id.corppass.gov.sg/.well-known/openid-configuration
- CP_OIDC_NDI_JWKS_ENDPOINT=https://stg-id.corppass.gov.sg/.well-known/keys
- CP_OIDC_RP_CLIENT_ID=rpClientId
- CP_OIDC_RP_REDIRECT_URL=https://staging.form.gov.sg/api/v3/corppass/login
- CP_OIDC_RP_JWKS_PUBLIC_PATH=./tests/certs/test_cp_rp_public_jwks.json
- CP_OIDC_RP_JWKS_SECRET_PATH=./tests/certs/test_cp_rp_secret_jwks.json
- CP_FORMSG_KEY_PATH=./node_modules/@opengovsg/mockpass/static/certs/key.pem
- CP_FORMSG_CERT_PATH=./node_modules/@opengovsg/mockpass/static/certs/server.crt
- CP_IDP_CERT_PATH=./node_modules/@opengovsg/mockpass/static/certs/spcp.crt
Expand Down
9 changes: 7 additions & 2 deletions docs/DEPLOYMENT_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,16 @@ Note that MyInfo is currently not supported for storage mode forms and enabling
| `SINGPASS_IDP_ID` | Partner ID of National Digital Identity Office for SingPass authentication. |
| `SP_OIDC_NDI_DISCOVERY_ENDPOINT` | NDI's Singpass OIDC Discovery Endpoint |
| `SP_OIDC_NDI_JWKS_ENDPOINT` | NDI's Singpass OIDC JWKS Endpoint |
| `SP_OIDC_RP_CLIENT_ID` | The Relying Party's Client ID as registered with NDI |
| `SP_OIDC_RP_REDIRECT_URL` | The Relying Party's Redirect URL |
| `SP_OIDC_RP_CLIENT_ID` | The Relying Party's Singpass Client ID as registered with NDI |
| `SP_OIDC_RP_REDIRECT_URL` | The Relying Party's Singpass Redirect URL |
| `SP_OIDC_RP_JWKS_PUBLIC_PATH` | Path to the Relying Party's Public Json Web Key Set used for Singpass-related communication with NDI. This will be hosted at /singpass/.well-known/jwks.json endpoint. |
| `SP_OIDC_RP_JWKS_SECRET_PATH` | Path to the Relying Party's Secret Json Web Key Set used for Singpass-related communication with NDI |
| `CP_OIDC_NDI_DISCOVERY_ENDPOINT` | NDI's Corppass OIDC Discovery Endpoint |
| `CP_OIDC_NDI_JWKS_ENDPOINT` | NDI's Corppass OIDC JWKS Endpoint |
| `CP_OIDC_RP_CLIENT_ID` | The Relying Party's Corppass Client ID as registered with NDI |
| `CP_OIDC_RP_REDIRECT_URL` | The Relying Party's Corppass Redirect URL |
| `CP_OIDC_RP_JWKS_PUBLIC_PATH` | Path to the Relying Party's Public Json Web Key Set used for Corppass-related communication with NDI. This will be hosted at api/v3/corppass/.well-known/jwks.json endpoint. |
| `CP_OIDC_RP_JWKS_SECRET_PATH` | Path to the Relying Party's Secret Json Web Key Set used for Corppass-related communication with NDI |
| `CORPPASS_ESRVC_ID` | e-service ID registered with National Digital Identity office for CorpPass authentication. |
| `CORPPASS_PARTNER_ENTITY_ID` | Partner ID registered with National Digital Identity Office for CorpPass authentication. |
| `CORPPASS_IDP_LOGIN_URL` | URL of CorpPass Login Page. |
Expand Down
39 changes: 37 additions & 2 deletions src/app/config/features/spcp-myinfo.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ type ISpcpConfig = {
spOidcRpRedirectUrl: string
spOidcRpJwksPublicPath: string
spOidcRpJwksSecretPath: string
cpOidcNdiDiscoveryEndpoint: string
cpOidcNdiJwksEndpoint: string
cpOidcRpClientId: string
cpOidcRpRedirectUrl: string
cpOidcRpJwksPublicPath: string
cpOidcRpJwksSecretPath: string
}

type IMyInfoConfig = {
Expand Down Expand Up @@ -235,13 +240,13 @@ const spcpMyInfoSchema: Schema<ISpcpMyInfo> = {
env: 'SP_OIDC_NDI_JWKS_ENDPOINT',
},
spOidcRpClientId: {
doc: "The Relying Party's Client ID as registered with NDI",
doc: "The Relying Party's Singpass Client ID as registered with NDI",
format: String,
default: null,
env: 'SP_OIDC_RP_CLIENT_ID',
},
spOidcRpRedirectUrl: {
doc: "The Relying Party's Redirect URL",
doc: "The Relying Party's Singpass Redirect URL",
format: String,
default: null,
env: 'SP_OIDC_RP_REDIRECT_URL',
Expand All @@ -258,12 +263,42 @@ const spcpMyInfoSchema: Schema<ISpcpMyInfo> = {
default: null,
env: 'SP_OIDC_RP_JWKS_SECRET_PATH',
},
cpOidcNdiDiscoveryEndpoint: {
doc: "NDI's Corppass OIDC Discovery Endpoint",
format: String,
default: null,
env: 'CP_OIDC_NDI_DISCOVERY_ENDPOINT',
},
cpOidcNdiJwksEndpoint: {
doc: "NDI's Corppass OIDC JWKS Endpoint",
format: String,
default: null,
env: 'CP_OIDC_NDI_JWKS_ENDPOINT',
},
cpOidcRpClientId: {
doc: "The Relying Party's Corppass Client ID as registered with NDI",
format: String,
default: null,
env: 'CP_OIDC_RP_CLIENT_ID',
},
cpOidcRpRedirectUrl: {
doc: "The Relying Party's Corppass Redirect URL",
format: String,
default: null,
env: 'CP_OIDC_RP_REDIRECT_URL',
},
cpOidcRpJwksPublicPath: {
doc: "Path to the Relying Party's Public Json Web Key Set used for Corppass-related communication with NDI. This will be hosted at api/v3/corppass/.well-known/jwks.json endpoint.",
format: String,
default: null,
env: 'CP_OIDC_RP_JWKS_PUBLIC_PATH',
},
cpOidcRpJwksSecretPath: {
doc: "Path to the Relying Party's Secret Json Web Key Set used for Cingpass-related communication with NDI",
format: String,
default: null,
env: 'CP_OIDC_RP_JWKS_SECRET_PATH',
},
}

export const spcpMyInfoConfig = convict(spcpMyInfoSchema)
Expand Down
2 changes: 1 addition & 1 deletion src/app/modules/examples/__tests__/examples.routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const app = setupApp('/examples', ExamplesRouter, {
setupWithAuth: true,
})

jest.mock('../../spcp/sp.oidc.client')
jest.mock('../../spcp/spcp.oidc.client')

describe('examples.routes', () => {
let request: Session
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jest.mock('nodemailer', () => ({
}))

// Avoid async refresh calls
jest.mock('src/app/modules/spcp/sp.oidc.client.ts')
jest.mock('src/app/modules/spcp/spcp.oidc.client.ts')

const UserModel = getUserModel(mongoose)
const FormModel = getFormModel(mongoose)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ import { MYINFO_COOKIE_NAME } from '../../../myinfo/myinfo.constants'
import { MyInfoCookieStateError } from '../../../myinfo/myinfo.errors'
import { MyInfoService } from '../../../myinfo/myinfo.service'
import { SGID_COOKIE_NAME } from '../../../sgid/sgid.constants'
import { SpOidcService } from '../../../spcp/sp.oidc.service'
import {
CreateRedirectUrlError,
FetchLoginPageError,
LoginPageValidationError,
MissingJwtError,
} from '../../../spcp/spcp.errors'
import { CpOidcServiceClass } from '../../../spcp/spcp.oidc.service/spcp.oidc.service.cp'
import { SpOidcServiceClass } from '../../../spcp/spcp.oidc.service/spcp.oidc.service.sp'
import { SpcpService } from '../../../spcp/spcp.service'
import { JwtName } from '../../../spcp/spcp.types'
import {
Expand All @@ -58,14 +59,15 @@ jest.mock('../public-form.service')
jest.mock('../../form.service')
jest.mock('../../../auth/auth.service')
jest.mock('../../../spcp/spcp.service')
jest.mock('../../../spcp/sp.oidc.service')
jest.mock('../../../spcp/spcp.oidc.service/spcp.oidc.service.sp')
jest.mock('../../../spcp/spcp.oidc.service/spcp.oidc.service.cp')
jest.mock('../../../myinfo/myinfo.service')

const MockFormService = mocked(FormService)
const MockPublicFormService = mocked(PublicFormService)
const MockAuthService = mocked(AuthService)
const MockSpcpService = mocked(SpcpService, true)
const MockSpOidcService = mocked(SpOidcService, true)

const MockMyInfoService = mocked(MyInfoService, true)

describe('public-form.controller', () => {
Expand Down Expand Up @@ -343,9 +345,10 @@ describe('public-form.controller', () => {
MockFormService.checkFormSubmissionLimitAndDeactivateForm.mockReturnValueOnce(
okAsync(MOCK_SP_AUTH_FORM),
)
MockSpOidcService.extractJwtPayloadFromRequest.mockReturnValueOnce(
okAsync(MOCK_SPCP_SESSION),
)

jest
.spyOn(SpOidcServiceClass.prototype, 'extractJwtPayloadFromRequest')
.mockReturnValueOnce(okAsync(MOCK_SPCP_SESSION))

// Act
await PublicFormController.handleGetPublicForm(
Expand Down Expand Up @@ -382,9 +385,9 @@ describe('public-form.controller', () => {
MockFormService.checkFormSubmissionLimitAndDeactivateForm.mockReturnValueOnce(
okAsync(MOCK_CP_AUTH_FORM),
)
MockSpcpService.extractJwtPayloadFromRequest.mockReturnValueOnce(
okAsync(MOCK_SPCP_SESSION),
)
jest
.spyOn(CpOidcServiceClass.prototype, 'extractJwtPayloadFromRequest')
.mockReturnValueOnce(okAsync(MOCK_SPCP_SESSION))
// Act
await PublicFormController.handleGetPublicForm(
MOCK_REQ,
Expand Down Expand Up @@ -686,9 +689,9 @@ describe('public-form.controller', () => {
MockFormService.checkFormSubmissionLimitAndDeactivateForm.mockReturnValueOnce(
okAsync(MOCK_SP_FORM),
)
MockSpOidcService.extractJwtPayloadFromRequest.mockReturnValueOnce(
errAsync(new MissingJwtError()),
)
jest
.spyOn(SpOidcServiceClass.prototype, 'extractJwtPayloadFromRequest')
.mockReturnValueOnce(errAsync(new MissingJwtError()))

// Act
// 2. GET the endpoint
Expand Down Expand Up @@ -718,9 +721,9 @@ describe('public-form.controller', () => {
MockFormService.checkFormSubmissionLimitAndDeactivateForm.mockReturnValueOnce(
okAsync(MOCK_CP_FORM),
)
MockSpcpService.extractJwtPayloadFromRequest.mockReturnValueOnce(
errAsync(new MissingJwtError()),
)
jest
.spyOn(CpOidcServiceClass.prototype, 'extractJwtPayloadFromRequest')
.mockReturnValueOnce(errAsync(new MissingJwtError()))

// Act
// 2. GET the endpoint
Expand Down Expand Up @@ -891,9 +894,9 @@ describe('public-form.controller', () => {

const mockRes = expressHandler.mockResponse()

MockSpOidcService.extractJwtPayloadFromRequest.mockReturnValueOnce(
okAsync(MOCK_SPCP_SESSION),
)
jest
.spyOn(SpOidcServiceClass.prototype, 'extractJwtPayloadFromRequest')
.mockReturnValueOnce(okAsync(MOCK_SPCP_SESSION))
MockFormService.checkIsIntranetFormAccess.mockReturnValueOnce(true)
MockAuthService.getFormIfPublic.mockReturnValueOnce(
okAsync(MOCK_SP_AUTH_FORM),
Expand Down Expand Up @@ -927,9 +930,9 @@ describe('public-form.controller', () => {
const mockRes = expressHandler.mockResponse()

MockFormService.checkIsIntranetFormAccess.mockReturnValueOnce(true)
MockSpcpService.extractJwtPayloadFromRequest.mockReturnValueOnce(
okAsync(MOCK_SPCP_SESSION),
)
jest
.spyOn(CpOidcServiceClass.prototype, 'extractJwtPayloadFromRequest')
.mockReturnValueOnce(okAsync(MOCK_SPCP_SESSION))
MockAuthService.getFormIfPublic.mockReturnValueOnce(
okAsync(MOCK_CP_AUTH_FORM),
)
Expand Down Expand Up @@ -1023,9 +1026,9 @@ describe('public-form.controller', () => {
MockFormService.retrieveFullFormById.mockReturnValueOnce(
okAsync(MOCK_FORM),
)
MockSpOidcService.createRedirectUrl.mockResolvedValueOnce(
ok(MOCK_REDIRECT_URL),
)
jest
.spyOn(SpOidcServiceClass.prototype, 'createRedirectUrl')
.mockResolvedValueOnce(ok(MOCK_REDIRECT_URL))

// Act
await PublicFormController._handleFormAuthRedirect(
Expand Down Expand Up @@ -1056,9 +1059,9 @@ describe('public-form.controller', () => {
MockFormService.retrieveFullFormById.mockReturnValueOnce(
okAsync(MOCK_FORM),
)
MockSpOidcService.createRedirectUrl.mockResolvedValueOnce(
ok(MOCK_REDIRECT_URL),
)
jest
.spyOn(SpOidcServiceClass.prototype, 'createRedirectUrl')
.mockResolvedValueOnce(ok(MOCK_REDIRECT_URL))

// Act
await PublicFormController._handleFormAuthRedirect(
Expand Down Expand Up @@ -1092,9 +1095,9 @@ describe('public-form.controller', () => {
MockFormService.retrieveFullFormById.mockReturnValueOnce(
okAsync(MOCK_FORM),
)
MockSpOidcService.createRedirectUrl.mockResolvedValueOnce(
ok(MOCK_REDIRECT_URL),
)
jest
.spyOn(SpOidcServiceClass.prototype, 'createRedirectUrl')
.mockResolvedValueOnce(ok(MOCK_REDIRECT_URL))

// Act
await PublicFormController._handleFormAuthRedirect(
Expand All @@ -1121,9 +1124,9 @@ describe('public-form.controller', () => {
MockFormService.retrieveFullFormById.mockReturnValueOnce(
okAsync(MOCK_FORM),
)
MockSpcpService.createRedirectUrl.mockReturnValueOnce(
ok(MOCK_REDIRECT_URL),
)
jest
.spyOn(CpOidcServiceClass.prototype, 'createRedirectUrl')
.mockReturnValueOnce(okAsync(MOCK_REDIRECT_URL))

// Act
await PublicFormController._handleFormAuthRedirect(
Expand Down Expand Up @@ -1307,9 +1310,9 @@ describe('public-form.controller', () => {
MockFormService.retrieveFullFormById.mockReturnValueOnce(
okAsync(MOCK_FORM),
)
MockSpOidcService.createRedirectUrl.mockResolvedValue(
err(new CreateRedirectUrlError()),
)
jest
.spyOn(SpOidcServiceClass.prototype, 'createRedirectUrl')
.mockResolvedValue(err(new CreateRedirectUrlError()))

// Act
await PublicFormController._handleFormAuthRedirect(
Expand All @@ -1336,9 +1339,9 @@ describe('public-form.controller', () => {
MockFormService.retrieveFullFormById.mockReturnValueOnce(
okAsync(MOCK_FORM),
)
MockSpcpService.createRedirectUrl.mockReturnValueOnce(
err(new CreateRedirectUrlError()),
)
jest
.spyOn(CpOidcServiceClass.prototype, 'createRedirectUrl')
.mockReturnValueOnce(errAsync(new CreateRedirectUrlError()))

// Act
await PublicFormController._handleFormAuthRedirect(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import SPCPAuthClient from '@opengovsg/spcp-auth-client'
import { ObjectId } from 'bson-ext'
import { errAsync } from 'neverthrow'
import supertest, { Session } from 'supertest-session'
Expand All @@ -13,7 +12,7 @@ import dbHandler from 'tests/unit/backend/helpers/jest-db'

import { FormAuthType, FormStatus } from '../../../../../../shared/types'
import * as AuthService from '../../../auth/auth.service'
import { SpOidcClient } from '../../../spcp/sp.oidc.client'
import { CpOidcClient, SpOidcClient } from '../../../spcp/spcp.oidc.client'
import { PublicFormRouter } from '../public-form.routes'

jest.mock('@opengovsg/myinfo-gov-client', () => ({
Expand All @@ -29,10 +28,10 @@ jest.mock('@opengovsg/myinfo-gov-client', () => ({
.MyInfoAttribute,
}))

jest.mock('../../../spcp/sp.oidc.client')
jest.mock('../../../spcp/spcp.oidc.client')

jest.mock('@opengovsg/spcp-auth-client')
const MockSpcpAuthClient = mocked(SPCPAuthClient, true)
const MockCpOidcClient = mocked(CpOidcClient, true)

const app = setupApp('/', PublicFormRouter, {
setupWithAuth: false,
Expand All @@ -41,7 +40,7 @@ const app = setupApp('/', PublicFormRouter, {
describe('public-form.routes', () => {
let request: Session

const mockCpClient = mocked(MockSpcpAuthClient.mock.instances[1], true)
const mockCpClient = mocked(MockCpOidcClient.mock.instances[0], true)

beforeAll(async () => await dbHandler.connect())
beforeEach(async () => {
Expand Down Expand Up @@ -127,15 +126,13 @@ describe('public-form.routes', () => {
})
it('should return 200 with public form when form has FormAuthType.CP and valid formId', async () => {
// Arrange
mockCpClient.verifyJWT.mockImplementationOnce((_jwt, cb) =>
cb(null, {
userName: MOCK_COOKIE_PAYLOAD.userName,
userInfo: 'MyCorpPassUEN',
iat: 100000000,
exp: 1000000000,
rememberMe: false,
}),
)
mockCpClient.verifyJwt.mockResolvedValueOnce({
userName: MOCK_COOKIE_PAYLOAD.userName,
userInfo: 'MyCorpPassUEN',
iat: 100000000,
exp: 1000000000,
rememberMe: false,
})
const { form } = await dbHandler.insertEmailForm({
formOptions: {
esrvcId: 'mockEsrvcId',
Expand Down
Loading

0 comments on commit e47bd9c

Please sign in to comment.