Skip to content

Commit

Permalink
Merge pull request #473 from justynoh/feat/ndi-oidc
Browse files Browse the repository at this point in the history
feat: NDI OIDC for Singpass and Corppass OIDC for Corppass authentication
  • Loading branch information
LoneRifle committed Dec 29, 2022
2 parents 1338de2 + 2b60180 commit 8d3588a
Show file tree
Hide file tree
Showing 11 changed files with 485 additions and 43 deletions.
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,26 @@ A mock SingPass/CorpPass/MyInfo server for dev purposes

Configure your application to point to the following endpoints:

SingPass:
SingPass (v1 - Singpass OIDC):
- http://localhost:5156/singpass/authorize - OIDC login redirect with optional page
- http://localhost:5156/singpass/token - receives OIDC authorization code and returns id_token

CorpPass:
SingPass (v2 - NDI OIDC):
- http://localhost:5156/singpass/v2/authorize - OIDC login redirect with optional page
- http://localhost:5156/singpass/v2/token - receives OIDC authorization code and returns id_token
- http://localhost:5156/singpass/v2/.well-known/openid-configuration - OpenID discovery endpoint
- http://localhost:5156/singpass/v2/.well-known/keys - JWKS endpoint which exposes the auth provider's signing keys

CorpPass (v1 - Corppass OIDC):
- http://localhost:5156/corppass/authorize - OIDC login redirect with optional page
- http://localhost:5156/corppass/token - receives OIDC authorization code and returns id_token

CorpPass (v2 - Corppass OIDC):
- http://localhost:5156/corppass/v2/authorize - OIDC login redirect with optional page
- http://localhost:5156/corppass/v2/token - receives OIDC authorization code and returns id_token
- http://localhost:5156/corppass/v2/.well-known/openid-configuration - OpenID discovery endpoint
- http://localhost:5156/corppass/v2/.well-known/keys - JWKS endpoint which exposes the auth provider's signing keys

MyInfo:
- http://localhost:5156/myinfo/v3/person-basic (exclusive to government systems)
- http://localhost:5156/myinfo/v3/authorise
Expand All @@ -42,6 +54,11 @@ and with application certs at `static/certs/{key.pem|server.crt}`
Alternatively, provide the paths to your app cert as env vars
`SERVICE_PROVIDER_CERT_PATH` and `SERVICE_PROVIDER_PUB_KEY`

If you are integrating with Singpass NDI OIDC and/or Corppass v2 OIDC, you should
provide your well-known key endpoints as env vars `SP_RP_JWKS_ENDPOINT` and/or
`CP_RP_JWKS_ENDPOINT` respectively. Alternatively, provide your application with
the `oidc-v2-rp-*.json` JWKS.

```
$ npm install @opengovsg/mockpass
Expand Down
8 changes: 7 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ const morgan = require('morgan')
const path = require('path')
require('dotenv').config()

const { configOIDC, configMyInfo, configSGID } = require('./lib/express')
const {
configOIDC,
configOIDCv2,
configMyInfo,
configSGID,
} = require('./lib/express')

const PORT = process.env.MOCKPASS_PORT || process.env.PORT || 5156

Expand Down Expand Up @@ -44,6 +49,7 @@ const app = express()
app.use(morgan('combined'))

configOIDC(app, options)
configOIDCv2(app, options)
configSGID(app, options)

configMyInfo.consent(app)
Expand Down
2 changes: 1 addition & 1 deletion lib/express/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
configOIDC: require('./oidc'),
...require('./oidc'),
configMyInfo: require('./myinfo'),
configSGID: require('./sgid'),
}
4 changes: 4 additions & 0 deletions lib/express/oidc/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
configOIDC: require('./spcp'),
configOIDCv2: require('./v2-ndi'),
}
48 changes: 9 additions & 39 deletions lib/express/oidc.js → lib/express/oidc/spcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,25 @@ const jose = require('node-jose')
const path = require('path')
const ExpiryMap = require('expiry-map')

const assertions = require('../assertions')
const { generateAuthCode, lookUpByAuthCode } = require('../auth-code')
const assertions = require('../../assertions')
const { generateAuthCode, lookUpByAuthCode } = require('../../auth-code')
const {
buildAssertURL,
idGenerator,
customProfileFromHeaders,
} = require('./utils')

const LOGIN_TEMPLATE = fs.readFileSync(
path.resolve(__dirname, '../../static/html/login-page.html'),
path.resolve(__dirname, '../../../static/html/login-page.html'),
'utf8',
)
const REFRESH_TOKEN_TIMEOUT = 24 * 60 * 60 * 1000
const profileStore = new ExpiryMap(REFRESH_TOKEN_TIMEOUT)

const signingPem = fs.readFileSync(
path.resolve(__dirname, '../../static/certs/spcp-key.pem'),
path.resolve(__dirname, '../../../static/certs/spcp-key.pem'),
)

const buildAssertURL = (redirectURI, authCode, state) =>
`${redirectURI}?code=${encodeURIComponent(
authCode,
)}&state=${encodeURIComponent(state)}`

const idGenerator = {
singPass: ({ nric }) =>
assertions.myinfo.v3.personas[nric] ? `${nric} [MyInfo]` : nric,
corpPass: ({ nric, uen }) => `${nric} / UEN: ${uen}`,
}

const customProfileFromHeaders = {
singPass: (req) => {
const customNricHeader = req.header('X-Custom-NRIC')
const customUuidHeader = req.header('X-Custom-UUID')
if (!customNricHeader || !customUuidHeader) {
return false
}
return { nric: customNricHeader, uuid: customUuidHeader }
},
corpPass: (req) => {
const customNricHeader = req.header('X-Custom-NRIC')
const customUuidHeader = req.header('X-Custom-UUID')
const customUenHeader = req.header('X-Custom-UEN')
if (!customNricHeader || !customUuidHeader || !customUenHeader) {
return false
}
return {
nric: customNricHeader,
uuid: customUuidHeader,
uen: customUenHeader,
}
},
}

function config(app, { showLoginPage, serviceProvider }) {
for (const idp of ['singPass', 'corpPass']) {
const profiles = assertions.oidc[idp]
Expand Down
42 changes: 42 additions & 0 deletions lib/express/oidc/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const assertions = require('../../assertions')

const buildAssertURL = (redirectURI, authCode, state) =>
`${redirectURI}?code=${encodeURIComponent(
authCode,
)}&state=${encodeURIComponent(state)}`

const idGenerator = {
singPass: ({ nric }) =>
assertions.myinfo.v3.personas[nric] ? `${nric} [MyInfo]` : nric,
corpPass: ({ nric, uen }) => `${nric} / UEN: ${uen}`,
}

const customProfileFromHeaders = {
singPass: (req) => {
const customNricHeader = req.header('X-Custom-NRIC')
const customUuidHeader = req.header('X-Custom-UUID')
if (!customNricHeader || !customUuidHeader) {
return false
}
return { nric: customNricHeader, uuid: customUuidHeader }
},
corpPass: (req) => {
const customNricHeader = req.header('X-Custom-NRIC')
const customUuidHeader = req.header('X-Custom-UUID')
const customUenHeader = req.header('X-Custom-UEN')
if (!customNricHeader || !customUuidHeader || !customUenHeader) {
return false
}
return {
nric: customNricHeader,
uuid: customUuidHeader,
uen: customUenHeader,
}
},
}

module.exports = {
buildAssertURL,
idGenerator,
customProfileFromHeaders,
}
Loading

0 comments on commit 8d3588a

Please sign in to comment.