@opengovsg/mockpass 2.9.5 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,22 +18,18 @@ A mock SingPass/CorpPass/MyInfo server for dev purposes
18
18
  Configure your application to point to the following endpoints:
19
19
 
20
20
  SingPass:
21
- - http://localhost:5156/singpass/logininitial - SAML login redirect with optional page
22
- - http://localhost:5156/singpass/soap - receives SAML artifact and returns assertion
23
21
  - http://localhost:5156/singpass/authorize - OIDC login redirect with optional page
24
22
  - http://localhost:5156/singpass/token - receives OIDC authorization code and returns id_token
25
23
 
26
24
  CorpPass:
27
- - http://localhost:5156/corppass/logininitial
28
- - http://localhost:5156/corppass/soap
29
25
  - http://localhost:5156/corppass/authorize - OIDC login redirect with optional page
30
26
  - http://localhost:5156/corppass/token - receives OIDC authorization code and returns id_token
31
27
 
32
28
  MyInfo:
33
- - http://localhost:5156/myinfo/{v2,v3}/person-basic (exclusive to government systems)
34
- - http://localhost:5156/myinfo/{v2,v3}/authorise
35
- - http://localhost:5156/myinfo/{v2,v3}/token
36
- - http://localhost:5156/myinfo/{v2,v3}/person
29
+ - http://localhost:5156/myinfo/v3/person-basic (exclusive to government systems)
30
+ - http://localhost:5156/myinfo/v3/authorise
31
+ - http://localhost:5156/myinfo/v3/token
32
+ - http://localhost:5156/myinfo/v3/person
37
33
 
38
34
  sgID:
39
35
  - http://localhost:5156/sgid/v1/oauth/authorize
@@ -49,11 +45,6 @@ Alternatively, provide the paths to your app cert as env vars
49
45
  ```
50
46
  $ npm install @opengovsg/mockpass
51
47
 
52
- # Some familiarity with SAML Artifact Binding is assumed
53
- # Optional: Configure where MockPass should send SAML artifact to, default endpoint will be `PartnerId` in request query parameter.
54
- $ export SINGPASS_ASSERT_ENDPOINT=http://localhost:5000/singpass/assert
55
- $ export CORPPASS_ASSERT_ENDPOINT=http://localhost:5000/corppass/assert
56
-
57
48
  # All values shown here are defaults
58
49
  $ export MOCKPASS_PORT=5156
59
50
 
@@ -69,7 +60,7 @@ $ export ENCRYPT_ASSERTION=false
69
60
  $ export SIGN_RESPONSE=false
70
61
  $ export RESOLVE_ARTIFACT_REQUEST_SIGNED=false
71
62
 
72
- # Encrypt payloads returned by /myinfo/*/{person, person-basic},
63
+ # Encrypt payloads returned by /myinfo/v3/{person, person-basic},
73
64
  # equivalent to MyInfo Auth Level L2 (testing and production)
74
65
  $ export ENCRYPT_MYINFO=false
75
66
 
@@ -89,7 +80,7 @@ who then need to connect to the staging servers hosted by SingPass/CorpPass,
89
80
  which may not always be available (eg, down for maintenance, or no Internet).
90
81
 
91
82
  MockPass tries to solves this by providing an extremely lightweight implementation
92
- of a SAML 2.0 Identity Provider that returns mock SingPass and CorpPass assertions.
83
+ of an OIDC Provider that returns mock SingPass and CorpPass assertions.
93
84
  It optionally provides a mock login page that (badly) mimics the SingPass/CorpPass
94
85
  login experience.
95
86
 
package/index.js CHANGED
@@ -5,25 +5,10 @@ const morgan = require('morgan')
5
5
  const path = require('path')
6
6
  require('dotenv').config()
7
7
 
8
- const {
9
- configSAML,
10
- configOIDC,
11
- configMyInfo,
12
- configSGID,
13
- } = require('./lib/express')
8
+ const { configOIDC, configMyInfo, configSGID } = require('./lib/express')
14
9
 
15
10
  const PORT = process.env.MOCKPASS_PORT || process.env.PORT || 5156
16
11
 
17
- if (
18
- !process.env.SINGPASS_ASSERT_ENDPOINT &&
19
- !process.env.CORPPASS_ASSERT_ENDPOINT
20
- ) {
21
- console.warn(
22
- 'SINGPASS_ASSERT_ENDPOINT or CORPPASS_ASSERT_ENDPOINT is not set. ' +
23
- 'Value of `PartnerId` request query parameter in redirect URL will be used.',
24
- )
25
- }
26
-
27
12
  const serviceProvider = {
28
13
  cert: fs.readFileSync(
29
14
  path.resolve(
@@ -49,18 +34,6 @@ const cryptoConfig = {
49
34
 
50
35
  const options = {
51
36
  serviceProvider,
52
- idpConfig: {
53
- singPass: {
54
- id:
55
- process.env.SINGPASS_IDP_ID || 'http://localhost:5156/singpass/saml20',
56
- assertEndpoint: process.env.SINGPASS_ASSERT_ENDPOINT,
57
- },
58
- corpPass: {
59
- id:
60
- process.env.CORPPASS_IDP_ID || 'http://localhost:5156/corppass/saml20',
61
- assertEndpoint: process.env.CORPPASS_ASSERT_ENDPOINT,
62
- },
63
- },
64
37
  showLoginPage: (req) =>
65
38
  (req.header('X-Show-Login-Page') || process.env.SHOW_LOGIN_PAGE) === 'true',
66
39
  encryptMyInfo: process.env.ENCRYPT_MYINFO === 'true',
@@ -70,12 +43,10 @@ const options = {
70
43
  const app = express()
71
44
  app.use(morgan('combined'))
72
45
 
73
- configSAML(app, options)
74
46
  configOIDC(app, options)
75
47
  configSGID(app, options)
76
48
 
77
49
  configMyInfo.consent(app)
78
- configMyInfo.v2(app, options)
79
50
  configMyInfo.v3(app, options)
80
51
 
81
52
  app.enable('trust proxy')
package/lib/assertions.js CHANGED
@@ -1,20 +1,10 @@
1
- const base64 = require('base-64')
2
1
  const crypto = require('crypto')
3
2
  const fs = require('fs')
4
- const { render } = require('mustache')
5
- const moment = require('moment')
6
3
  const jose = require('node-jose')
7
4
  const path = require('path')
8
5
 
9
6
  const readFrom = (p) => fs.readFileSync(path.resolve(__dirname, p), 'utf8')
10
7
 
11
- const TEMPLATE = readFrom('../static/saml/unsigned-assertion.xml')
12
- const corpPassTemplate = readFrom('../static/saml/corppass.xml')
13
-
14
- const defaultAudience =
15
- process.env.SERVICE_PROVIDER_ENTITY_ID ||
16
- 'http://sp.example.com/demo1/metadata.php'
17
-
18
8
  const signingPem = fs.readFileSync(
19
9
  path.resolve(__dirname, '../static/certs/spcp-key.pem'),
20
10
  )
@@ -34,95 +24,9 @@ const hashToken = (token) => {
34
24
  }
35
25
 
36
26
  const myinfo = {
37
- v2: JSON.parse(readFrom('../static/myinfo/v2.json')),
38
27
  v3: JSON.parse(readFrom('../static/myinfo/v3.json')),
39
28
  }
40
29
 
41
- const saml = {
42
- singPass: [
43
- { nric: 'S8979373D' },
44
- { nric: 'S8116474F' },
45
- { nric: 'S8723211E' },
46
- { nric: 'S5062854Z' },
47
- { nric: 'T0066846F' },
48
- { nric: 'F9477325W' },
49
- { nric: 'S3000024B' },
50
- { nric: 'S6005040F' },
51
- { nric: 'S6005041D' },
52
- { nric: 'S6005042B' },
53
- { nric: 'S6005043J' },
54
- { nric: 'S6005044I' },
55
- { nric: 'S6005045G' },
56
- { nric: 'S6005046E' },
57
- { nric: 'S6005047C' },
58
- { nric: 'S6005064C' },
59
- { nric: 'S6005065A' },
60
- { nric: 'S6005066Z' },
61
- { nric: 'S6005037F' },
62
- { nric: 'S6005038D' },
63
- { nric: 'S6005039B' },
64
- { nric: 'G1612357P' },
65
- { nric: 'G1612358M' },
66
- { nric: 'F1612359P' },
67
- { nric: 'F1612360U' },
68
- { nric: 'F1612361R' },
69
- { nric: 'F1612362P' },
70
- { nric: 'F1612363M' },
71
- { nric: 'F1612364K' },
72
- { nric: 'F1612365W' },
73
- { nric: 'F1612366T' },
74
- { nric: 'F1612367Q' },
75
- { nric: 'F1612358R' },
76
- { nric: 'F1612354N' },
77
- { nric: 'F1612357U' },
78
- ...Object.keys(myinfo.v2.personas).map((nric) => ({ nric })),
79
- ],
80
- corpPass: [
81
- { nric: 'S8979373D', uen: '123456789A' },
82
- { nric: 'S8116474F', uen: '123456789A' },
83
- { nric: 'S8723211E', uen: '123456789A' },
84
- { nric: 'S5062854Z', uen: '123456789B' },
85
- { nric: 'T0066846F', uen: '123456789B' },
86
- { nric: 'F9477325W', uen: '123456789B' },
87
- { nric: 'S3000024B', uen: '123456789C' },
88
- { nric: 'S6005040F', uen: '123456789C' },
89
- ],
90
- create: {
91
- singPass: (
92
- { nric },
93
- issuer,
94
- recipient,
95
- inResponseTo,
96
- audience = defaultAudience,
97
- ) =>
98
- render(TEMPLATE, {
99
- name: 'UserName',
100
- value: nric,
101
- issueInstant: moment.utc().format(),
102
- recipient,
103
- issuer,
104
- inResponseTo,
105
- audience,
106
- }),
107
- corpPass: (
108
- { nric, uen },
109
- issuer,
110
- recipient,
111
- inResponseTo,
112
- audience = defaultAudience,
113
- ) =>
114
- render(TEMPLATE, {
115
- issueInstant: moment.utc().format(),
116
- name: uen,
117
- value: base64.encode(render(corpPassTemplate, { nric, uen })),
118
- recipient,
119
- issuer,
120
- inResponseTo,
121
- audience,
122
- }),
123
- },
124
- }
125
-
126
30
  const oidc = {
127
31
  singPass: [
128
32
  { nric: 'S8979373D', uuid: 'a9865837-7bd7-46ac-bef4-42a76a946424' },
@@ -321,7 +225,6 @@ const oidc = {
321
225
  }
322
226
 
323
227
  module.exports = {
324
- saml,
325
228
  oidc,
326
229
  myinfo,
327
230
  }
@@ -1,86 +1,5 @@
1
1
  const _ = require('lodash')
2
2
 
3
- const apex = function apex(authHeader, req, context = {}) {
4
- const authHeaderFieldPairs = _(authHeader)
5
- .replace(/"/g, '')
6
- .replace(/apex_l2_eg_/g, '')
7
- .split(',')
8
- .map((v) => v.replace('=', '~').split('~'))
9
-
10
- const authHeaderFields = _(authHeaderFieldPairs)
11
- .fromPairs()
12
- .mapKeys((v, k) => _.camelCase(k))
13
- .value()
14
-
15
- const url = `${req.protocol}://${req.get('host')}${req.baseUrl}${req.path}`
16
-
17
- const { clientSecret, redirectURI } = context
18
-
19
- const {
20
- method: httpMethod,
21
- query: { attributes, singpassEserviceId },
22
- } = req
23
-
24
- const { code } = req.body || {}
25
-
26
- const {
27
- signature,
28
- appId,
29
- appId: clientId,
30
- nonce,
31
- timestamp,
32
- } = authHeaderFields
33
-
34
- const baseString = req.path.endsWith('/token')
35
- ? httpMethod.toUpperCase() +
36
- // url string replacement was dictated by MyInfo docs - no explanation
37
- // was provided for why this is necessary
38
- '&' +
39
- url.replace('.api.gov.sg', '.e.api.gov.sg') +
40
- '&apex_l2_eg_app_id=' +
41
- appId +
42
- '&apex_l2_eg_nonce=' +
43
- nonce +
44
- '&apex_l2_eg_signature_method=SHA256withRSA' +
45
- '&apex_l2_eg_timestamp=' +
46
- timestamp +
47
- '&apex_l2_eg_version=1.0' +
48
- '&client_id=' +
49
- clientId +
50
- '&client_secret=' +
51
- clientSecret +
52
- '&code=' +
53
- code +
54
- '&grant_type=authorization_code' +
55
- '&redirect_uri=' +
56
- redirectURI
57
- : httpMethod.toUpperCase() +
58
- // url string replacement was dictated by MyInfo docs - no explanation
59
- // was provided for why this is necessary
60
- '&' +
61
- url.replace('.api.gov.sg', '.e.api.gov.sg') +
62
- '&apex_l2_eg_app_id=' +
63
- appId +
64
- '&apex_l2_eg_nonce=' +
65
- nonce +
66
- '&apex_l2_eg_signature_method=SHA256withRSA' +
67
- '&apex_l2_eg_timestamp=' +
68
- timestamp +
69
- '&apex_l2_eg_version=1.0' +
70
- '&attributes=' +
71
- attributes +
72
- '&client_id=' +
73
- clientId +
74
- (req.path.includes('/person-basic')
75
- ? '&singpassEserviceId=' + singpassEserviceId
76
- : '')
77
-
78
- return {
79
- signature,
80
- baseString,
81
- }
82
- }
83
-
84
3
  const pki = function pki(authHeader, req, context = {}) {
85
4
  const authHeaderFieldPairs = _(authHeader)
86
5
  .replace(/"/g, '')
@@ -150,4 +69,4 @@ const pki = function pki(authHeader, req, context = {}) {
150
69
  }
151
70
  }
152
71
 
153
- module.exports = { pki, apex }
72
+ module.exports = { pki }
@@ -1,5 +1,4 @@
1
1
  module.exports = {
2
- configSAML: require('./saml'),
3
2
  configOIDC: require('./oidc'),
4
3
  configMyInfo: require('./myinfo'),
5
4
  configSGID: require('./sgid'),
@@ -9,7 +9,6 @@ const { v1: uuid } = require('uuid')
9
9
 
10
10
  const assertions = require('../../assertions')
11
11
  const { lookUpByAuthCode } = require('../../auth-code')
12
- const { lookUpBySamlArtifact } = require('../../saml-artifact')
13
12
 
14
13
  const MYINFO_ASSERT_ENDPOINT = '/consent/myinfo-com'
15
14
  const AUTHORIZE_ENDPOINT = '/consent/oauth2/authorize'
@@ -43,11 +42,6 @@ const authorize = (redirectTo) => (req, res) => {
43
42
  res.redirect(redirectTo(relayState))
44
43
  }
45
44
 
46
- const authorizeViaSAML = authorize(
47
- (relayState) =>
48
- `/singpass/logininitial?esrvcID=MYINFO-CONSENTPLATFORM&PartnerId=${MYINFO_ASSERT_ENDPOINT}&Target=${relayState}`,
49
- )
50
-
51
45
  const authorizeViaOIDC = authorize(
52
46
  (state) =>
53
47
  `/singpass/authorize?client_id=MYINFO-CONSENTPLATFORM&redirect_uri=${MYINFO_ASSERT_ENDPOINT}&state=${state}`,
@@ -59,14 +53,9 @@ function config(app) {
59
53
  const artifact = rawArtifact.replace(/ /g, '+')
60
54
  const state = req.query.RelayState || req.query.state
61
55
 
62
- let profile, myinfoVersion
63
- if (req.query.code) {
64
- profile = lookUpByAuthCode(artifact).profile
65
- myinfoVersion = 'v3'
66
- } else {
67
- profile = lookUpBySamlArtifact(artifact)
68
- myinfoVersion = 'v2'
69
- }
56
+ const profile = lookUpByAuthCode(artifact).profile
57
+ const myinfoVersion = 'v3'
58
+
70
59
  const { nric: id } = profile
71
60
 
72
61
  const persona = assertions.myinfo[myinfoVersion].personas[id]
@@ -147,7 +136,6 @@ function config(app) {
147
136
  }
148
137
 
149
138
  module.exports = {
150
- authorizeViaSAML,
151
139
  authorizeViaOIDC,
152
140
  authorizations,
153
141
  config,
@@ -31,12 +31,9 @@ module.exports =
31
31
  }
32
32
 
33
33
  const encryptPersona = async (persona) => {
34
- const signedPersona =
35
- version === 'v3'
36
- ? jwt.sign(persona, MOCKPASS_PRIVATE_KEY, {
37
- algorithm: 'RS256',
38
- })
39
- : persona
34
+ const signedPersona = jwt.sign(persona, MOCKPASS_PRIVATE_KEY, {
35
+ algorithm: 'RS256',
36
+ })
40
37
  const serviceCertAsKey = await jose.JWK.asKey(serviceProvider.cert, 'pem')
41
38
  const encryptedAndSignedPersona = await jose.JWE.createEncrypt(
42
39
  { format: 'compact' },
@@ -126,10 +123,7 @@ module.exports =
126
123
  }
127
124
  })
128
125
 
129
- app.get(
130
- `/myinfo/${version}/authorise`,
131
- version === 'v3' ? consent.authorizeViaOIDC : consent.authorizeViaSAML,
132
- )
126
+ app.get(`/myinfo/${version}/authorise`, consent.authorizeViaOIDC)
133
127
 
134
128
  app.post(
135
129
  `/myinfo/${version}/token`,
@@ -1,10 +1,9 @@
1
1
  const { config: consent } = require('./consent')
2
2
  const controllers = require('./controllers')
3
3
 
4
- const { pki, apex } = require('../../crypto/myinfo-signature')
4
+ const { pki } = require('../../crypto/myinfo-signature')
5
5
 
6
6
  module.exports = {
7
7
  consent,
8
- v2: controllers('v2', apex),
9
8
  v3: controllers('v3', pki),
10
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/mockpass",
3
- "version": "2.9.5",
3
+ "version": "3.0.1",
4
4
  "description": "A mock SingPass/CorpPass server for dev purposes",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -44,13 +44,10 @@
44
44
  "express": "^4.16.3",
45
45
  "jsonwebtoken": "^8.4.0",
46
46
  "lodash": "^4.17.11",
47
- "moment": "^2.24.0",
48
47
  "morgan": "^1.9.1",
49
48
  "mustache": "^4.2.0",
50
49
  "node-jose": "^2.0.0",
51
50
  "uuid": "^9.0.0",
52
- "xml-crypto": "^3.0.0",
53
- "xml-encryption": "^3.0.0",
54
51
  "xpath": "0.0.32"
55
52
  },
56
53
  "devDependencies": {
@@ -1,61 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
- const { SignedXml } = require('xml-crypto')
4
- const { encrypt } = require('xml-encryption')
5
- const xpath = require('xpath')
6
-
7
- module.exports = (serviceProvider) => {
8
- // NOTE - the typo in keyEncryptionAlgorighm is deliberate
9
- const ENCRYPT_OPTIONS = {
10
- rsa_pub: serviceProvider.pubKey,
11
- pem: serviceProvider.cert,
12
- encryptionAlgorithm: 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
13
- keyEncryptionAlgorithm: 'http://www.w3.org/2001/04/xmlenc#rsa-1_5',
14
- }
15
-
16
- return {
17
- verifySignature(xml) {
18
- const [signature] =
19
- xpath.select("//*[local-name(.)='Signature']", xml) || []
20
- const [artifactResolvePayload] =
21
- xpath.select("//*[local-name(.)='ArtifactResolve']", xml) || []
22
- const verifier = new SignedXml()
23
- verifier.keyInfoProvider = { getKey: () => ENCRYPT_OPTIONS.pem }
24
- verifier.loadSignature(signature.toString())
25
- return verifier.checkSignature(artifactResolvePayload.toString())
26
- },
27
-
28
- sign(payload, reference) {
29
- const sig = new SignedXml()
30
- const transforms = [
31
- 'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
32
- 'http://www.w3.org/2001/10/xml-exc-c14n#',
33
- ]
34
- const digestAlgorithm = 'http://www.w3.org/2001/04/xmlenc#sha256'
35
- sig.addReference(reference, transforms, digestAlgorithm)
36
-
37
- sig.signingKey = fs.readFileSync(
38
- path.resolve(__dirname, '../../static/certs/spcp-key.pem'),
39
- )
40
- sig.signatureAlgorithm =
41
- 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
42
- const options = {
43
- prefix: 'ds',
44
- location: { reference, action: 'prepend' },
45
- }
46
- sig.computeSignature(payload, options)
47
- return sig.getSignedXml()
48
- },
49
-
50
- promiseToEncryptAssertion: (assertion) =>
51
- new Promise((resolve, reject) => {
52
- encrypt(assertion, ENCRYPT_OPTIONS, (err, data) =>
53
- err
54
- ? reject(err)
55
- : resolve(
56
- `<saml:EncryptedAssertion>${data}</saml:EncryptedAssertion>`,
57
- ),
58
- )
59
- }),
60
- }
61
- }