Skip to content

Commit

Permalink
refactor: reorg assertions, spcp -> saml, etc
Browse files Browse the repository at this point in the history
- Allow oidc to replicate its endpoints for CorpPass
- Ensure that id generation is looked up, not determined by ternary
- Fix myinfo endpoints to use the correct assertions and authz depending
  on v2 or v3
- Rename spcp -> saml
  • Loading branch information
LoneRifle committed Sep 1, 2020
1 parent d448572 commit 7f29add
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 140 deletions.
4 changes: 2 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const morgan = require('morgan')
const path = require('path')
require('dotenv').config()

const { configSPCP, configOIDC, configMyInfo } = require('./lib/express')
const { configSAML, configOIDC, configMyInfo } = require('./lib/express')

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

Expand Down Expand Up @@ -64,7 +64,7 @@ const options = {
const app = express()
app.use(morgan('combined'))

configSPCP(app, options)
configSAML(app, options)
configOIDC(app, options)

configMyInfo.consent(app)
Expand Down
85 changes: 39 additions & 46 deletions lib/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const readFrom = (p) => fs.readFileSync(path.resolve(__dirname, p), 'utf8')
const TEMPLATE = readFrom('../static/saml/unsigned-assertion.xml')
const corpPassTemplate = readFrom('../static/saml/corppass.xml')

const defaultAudience =
process.env.SERVICE_PROVIDER_ENTITY_ID ||
'http://sp.example.com/demo1/metadata.php'

const myinfo = {
v2: JSON.parse(readFrom('../static/myinfo/v2.json')),
v3: JSON.parse(readFrom('../static/myinfo/v3.json')),
Expand Down Expand Up @@ -63,6 +67,40 @@ const saml = {
{ NRIC: 'S3000024B', UEN: '123456789C' },
{ NRIC: 'S6005040F', UEN: '123456789C' },
],
create: {
singPass: (
nric,
issuer,
recipient,
inResponseTo,
audience = defaultAudience,
) =>
render(TEMPLATE, {
name: 'UserName',
value: nric,
issueInstant: moment.utc().format(),
recipient,
issuer,
inResponseTo,
audience,
}),
corpPass: (
source,
issuer,
recipient,
inResponseTo,
audience = defaultAudience,
) =>
render(TEMPLATE, {
issueInstant: moment.utc().format(),
name: source.UEN,
value: base64.encode(render(corpPassTemplate, source)),
recipient,
issuer,
inResponseTo,
audience,
}),
},
}

const oidc = {
Expand Down Expand Up @@ -116,56 +154,11 @@ const oidc = {
],
}

const singPassNric = process.env.MOCKPASS_NRIC || saml.singPass[0]
const corpPassNric = process.env.MOCKPASS_NRIC || saml.corpPass[0].NRIC
const uen = process.env.MOCKPASS_UEN || saml.corpPass[0].UEN

const defaultAudience =
process.env.SERVICE_PROVIDER_ENTITY_ID ||
'http://sp.example.com/demo1/metadata.php'

const makeCorpPass = (
source = { NRIC: corpPassNric, UEN: uen },
issuer,
recipient,
inResponseTo,
audience = defaultAudience,
) =>
render(TEMPLATE, {
issueInstant: moment.utc().format(),
name: source.UEN,
value: base64.encode(render(corpPassTemplate, source)),
recipient,
issuer,
inResponseTo,
audience,
})

const singPassNric = process.env.MOCKPASS_NRIC || saml.singPass[0]

const makeSingPass = (
nric = singPassNric,
issuer,
recipient,
inResponseTo,
audience = defaultAudience,
) =>
render(TEMPLATE, {
name: 'UserName',
value: nric,
issueInstant: moment.utc().format(),
recipient,
issuer,
inResponseTo,
audience,
})

module.exports = {
singPass: {
create: makeSingPass,
},
corpPass: {
create: makeCorpPass,
},
saml,
oidc,
myinfo,
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 = {
configSPCP: require('./spcp'),
configSAML: require('./saml'),
configOIDC: require('./oidc'),
configMyInfo: require('./myinfo'),
}
11 changes: 8 additions & 3 deletions lib/express/myinfo/consent.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,17 @@ function config(app) {
const artifact = rawArtifact.replace(/ /g, '+')
const artifactBuffer = Buffer.from(artifact, 'base64')
let index = artifactBuffer.readInt8(artifactBuffer.length - 1)

const assertionType = req.query.code ? 'oidc' : 'saml'

// use env NRIC when SHOW_LOGIN_PAGE is false
if (index === -1) {
index = assertions.saml.singPass.indexOf(assertions.singPassNric)
index = assertions[assertionType].singPass.indexOf(
assertions.singPassNric,
)
}
const id = assertions.saml.singPass[index]
const persona = assertions.myinfo.v3.personas[id]
const id = assertions[assertionType].singPass[index]
const persona = assertions.myinfo[req.query.code ? 'v3' : 'v2'].personas[id]
if (!persona) {
res.status(404).send({
message: 'Cannot find MyInfo Persona',
Expand Down
5 changes: 4 additions & 1 deletion lib/express/myinfo/controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,10 @@ module.exports = (version, myInfoSignature) => (
}
})

app.get(`/myinfo/${version}/authorise`, consent.authorizeViaOIDC)
app.get(
`/myinfo/${version}/authorise`,
version === 'v3' ? consent.authorizeViaOIDC : consent.authorizeViaSAML,
)

app.post(
`/myinfo/${version}/token`,
Expand Down
154 changes: 76 additions & 78 deletions lib/express/oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,97 +15,95 @@ const LOGIN_TEMPLATE = fs.readFileSync(
const NONCE_TIMEOUT = 5 * 60 * 1000
const nonceStore = new ExpiryMap(NONCE_TIMEOUT)

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

function config(app, { showLoginPage, idpConfig, serviceProvider }) {
app.get('/singpass/authorize', (req, res) => {
const redirectURI = req.query.redirect_uri
const state = encodeURIComponent(req.query.state)
if (showLoginPage) {
const oidc = assertions.oidc.singPass
const generateIdFrom = (rawId) =>
assertions.myinfo.v3.personas[rawId] ? `${rawId} [MyInfo]` : rawId
const values = oidc.map((rawId, index) => {
const code = encodeURIComponent(
samlArtifact(idpConfig.singPass.id, index),
)
for (const idp of ['singPass', 'corpPass']) {
app.get(`/${idp.toLowerCase()}/authorize`, (req, res) => {
const redirectURI = req.query.redirect_uri
const state = encodeURIComponent(req.query.state)
if (showLoginPage) {
const oidc = assertions.oidc[idp]
const values = oidc.map((rawId, index) => {
const code = encodeURIComponent(
samlArtifact(idpConfig[idp].id, index),
)
if (req.query.nonce) {
nonceStore.set(code, req.query.nonce)
}
const assertURL = `${redirectURI}?code=${code}&state=${state}`
const id = idGenerator[idp](rawId)
return { id, assertURL }
})
const response = render(LOGIN_TEMPLATE, values)
res.send(response)
} else {
const code = encodeURIComponent(samlArtifact(idpConfig[idp].id))
if (req.query.nonce) {
nonceStore.set(code, req.query.nonce)
}
const assertURL = `${redirectURI}?code=${code}&state=${state}`
const id = generateIdFrom(rawId)
return { id, assertURL }
})
const response = render(LOGIN_TEMPLATE, values)
res.send(response)
} else {
const code = encodeURIComponent(samlArtifact(idpConfig.singPass.id))
if (req.query.nonce) {
nonceStore.set(code, req.query.nonce)
console.warn(
`Redirecting login from ${req.query.client_id} to ${redirectURI}`,
)
res.redirect(assertURL)
}
const assertURL = `${redirectURI}?code=${code}&state=${state}`
console.warn(
`Redirecting login from ${req.query.client_id} to ${redirectURI}`,
)
res.redirect(assertURL)
}
})
})

app.post(
'/singpass/token',
bodyParser.urlencoded({ extended: false }),
async (req, res) => {
const { client_id: aud, code: artifact } = req.body
console.warn(
`Received artifact ${artifact} from ${aud} and ${req.body.redirect_uri}`,
)
app.post(
`/${idp.toLowerCase()}/token`,
bodyParser.urlencoded({ extended: false }),
async (req, res) => {
const { client_id: aud, code: artifact } = req.body
console.warn(
`Received artifact ${artifact} from ${aud} and ${req.body.redirect_uri}`,
)

const artifactBuffer = Buffer.from(artifact, 'base64')
const uuid = artifactBuffer.readInt8(artifactBuffer.length - 1)
const nric = assertions.oidc.singPass[uuid]
const sub = `s=${nric},u=${uuid}`
const artifactBuffer = Buffer.from(artifact, 'base64')

const nonce = nonceStore.get(encodeURIComponent(artifact))
const uuid = artifactBuffer.readInt8(artifactBuffer.length - 1)
const nonce = nonceStore.get(encodeURIComponent(artifact))

const payload = {
rt_hash: '',
at_hash: '',
iat: Date.now(),
exp: Date.now() + 24 * 60 * 60 * 1000,
iss: `${req.protocol}://${req.get('host')}`,
amr: ['pwd'],
aud,
sub,
...(nonce ? { nonce } : {}),
}

const signingPem = fs.readFileSync(
path.resolve(__dirname, '../../static/certs/spcp-key.pem'),
)
const signingKey = await jose.JWK.asKey(signingPem, 'pem')
const signedPayload = await jose.JWS.createSign(
{ format: 'compact' },
signingKey,
)
.update(JSON.stringify(payload))
.final()
const { idTokenPayload, accessToken } = assertions.oidc[idp](
uuid,
`${req.protocol}://${req.get('host')}`,
aud,
nonce,
)

const encryptionKey = await jose.JWK.asKey(serviceProvider.cert, 'pem')
const idToken = await jose.JWE.createEncrypt(
{ format: 'compact', fields: { cty: 'JWT' } },
encryptionKey,
)
.update(signedPayload)
.final()
const signingPem = fs.readFileSync(
path.resolve(__dirname, '../../static/certs/spcp-key.pem'),
)
const signingKey = await jose.JWK.asKey(signingPem, 'pem')
const signedIdToken = await jose.JWS.createSign(
{ format: 'compact' },
signingKey,
)
.update(JSON.stringify(idTokenPayload))
.final()

res.send({
access_token: 'access',
refresh_token: 'refresh',
scope: 'openid',
token_type: 'bearer',
id_token: idToken,
})
},
)
const encryptionKey = await jose.JWK.asKey(serviceProvider.cert, 'pem')
const idToken = await jose.JWE.createEncrypt(
{ format: 'compact', fields: { cty: 'JWT' } },
encryptionKey,
)
.update(signedIdToken)
.final()

res.send({
access_token: accessToken,
refresh_token: 'refresh',
scope: 'openid',
token_type: 'bearer',
id_token: idToken,
})
},
)
}
return app
}

Expand Down
17 changes: 8 additions & 9 deletions lib/express/spcp.js → lib/express/saml.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const LOGIN_TEMPLATE = fs.readFileSync(

const MYINFO_ASSERT_ENDPOINT = '/consent/myinfo-com'

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

function config(
app,
{ showLoginPage, serviceProvider, idpConfig, cryptoConfig },
Expand All @@ -41,13 +47,6 @@ function config(
const relayState = req.query.Target
if (showLoginPage) {
const saml = assertions.saml[idp]
const generateIdFrom =
idp === 'corpPass'
? (rawId) => `${rawId.NRIC} / UEN: ${rawId.UEN}`
: (rawId) =>
assertions.myinfo.v2.personas[rawId]
? `${rawId} [MyInfo]`
: rawId
const values = saml.map((rawId, index) => {
const samlArt = encodeURIComponent(
samlArtifact(idpConfig[idp].id, index),
Expand All @@ -56,7 +55,7 @@ function config(
if (relayState !== undefined) {
assertURL += `&RelayState=${encodeURIComponent(relayState)}`
}
const id = generateIdFrom(rawId)
const id = idGenerator[idp](rawId)
return { id, assertURL }
})
const response = render(LOGIN_TEMPLATE, values)
Expand Down Expand Up @@ -108,7 +107,7 @@ function config(
xml,
)

let result = assertions[idp].create(
let result = assertions.saml.create[idp](
assertions.saml[idp][index],
idpConfig[idp].id,
idpConfig[idp].assertEndpoint,
Expand Down

0 comments on commit 7f29add

Please sign in to comment.