@opengovsg/mockpass 2.8.1 → 2.9.2

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
@@ -56,11 +56,13 @@ $ export CORPPASS_ASSERT_ENDPOINT=http://localhost:5000/corppass/assert
56
56
 
57
57
  # All values shown here are defaults
58
58
  $ export MOCKPASS_PORT=5156
59
- $ export MOCKPASS_NRIC=S8979373D
60
- $ export MOCKPASS_UEN=123456789A
61
59
 
62
60
  $ export SHOW_LOGIN_PAGE=true # Optional, defaults to `false`; can be overridden per request using `X-Show-Login-Page` HTTP header
63
61
 
62
+ # Configure which profile to return when login page is disabled
63
+ # Can be overridden per request using `X-Custom-NRIC`/`X-Custom-UUID`/`X-Custom-UEN` HTTP headers
64
+ $ export MOCKPASS_NRIC=S8979373D # Optional, defaults to first profile
65
+
64
66
  # Disable signing/encryption (Optional, by default `true`)
65
67
  $ export SIGN_ASSERTION=false
66
68
  $ export ENCRYPT_ASSERTION=false
package/index.js CHANGED
@@ -61,9 +61,8 @@ const options = {
61
61
  assertEndpoint: process.env.CORPPASS_ASSERT_ENDPOINT,
62
62
  },
63
63
  },
64
- showLoginPage: (req) => {
65
- return process.env.SHOW_LOGIN_PAGE === 'true' || req.header('X-Show-Login-Page') === 'true'
66
- },
64
+ showLoginPage: (req) =>
65
+ (req.header('X-Show-Login-Page') || process.env.SHOW_LOGIN_PAGE) === 'true',
67
66
  encryptMyInfo: process.env.ENCRYPT_MYINFO === 'true',
68
67
  cryptoConfig,
69
68
  }
package/lib/assertions.js CHANGED
@@ -19,17 +19,6 @@ const signingPem = fs.readFileSync(
19
19
  path.resolve(__dirname, '../static/certs/spcp-key.pem'),
20
20
  )
21
21
 
22
- const createRefreshToken = (uuid) => {
23
- const prefixSize = (`${uuid}`.length + 1) / 2
24
- const padding = Number.isInteger(prefixSize) ? '/' : '/f'
25
-
26
- return (
27
- uuid +
28
- padding +
29
- crypto.randomBytes(20 - Math.floor(prefixSize)).toString('hex')
30
- )
31
- }
32
-
33
22
  const hashToken = (token) => {
34
23
  const fullHash = crypto.createHash('sha256')
35
24
  fullHash.update(token, 'utf8')
@@ -38,8 +27,8 @@ const hashToken = (token) => {
38
27
  if (Buffer.isEncoding('base64url')) {
39
28
  return digestBuffer.toString('base64url')
40
29
  } else {
41
- const fromBase64 = (base64) =>
42
- base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
30
+ const fromBase64 = (base64String) =>
31
+ base64String.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
43
32
  return fromBase64(digestBuffer.toString('base64'))
44
33
  }
45
34
  }
@@ -51,42 +40,42 @@ const myinfo = {
51
40
 
52
41
  const saml = {
53
42
  singPass: [
54
- 'S8979373D',
55
- 'S8116474F',
56
- 'S8723211E',
57
- 'S5062854Z',
58
- 'T0066846F',
59
- 'F9477325W',
60
- 'S3000024B',
61
- 'S6005040F',
62
- 'S6005041D',
63
- 'S6005042B',
64
- 'S6005043J',
65
- 'S6005044I',
66
- 'S6005045G',
67
- 'S6005046E',
68
- 'S6005047C',
69
- 'S6005064C',
70
- 'S6005065A',
71
- 'S6005066Z',
72
- 'S6005037F',
73
- 'S6005038D',
74
- 'S6005039B',
75
- 'G1612357P',
76
- 'G1612358M',
77
- 'F1612359P',
78
- 'F1612360U',
79
- 'F1612361R',
80
- 'F1612362P',
81
- 'F1612363M',
82
- 'F1612364K',
83
- 'F1612365W',
84
- 'F1612366T',
85
- 'F1612367Q',
86
- 'F1612358R',
87
- 'F1612354N',
88
- 'F1612357U',
89
- ...Object.keys(myinfo.v2.personas),
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 })),
90
79
  ],
91
80
  corpPass: [
92
81
  { nric: 'S8979373D', uen: '123456789A' },
@@ -100,7 +89,7 @@ const saml = {
100
89
  ],
101
90
  create: {
102
91
  singPass: (
103
- nric,
92
+ { nric },
104
93
  issuer,
105
94
  recipient,
106
95
  inResponseTo,
@@ -116,7 +105,7 @@ const saml = {
116
105
  audience,
117
106
  }),
118
107
  corpPass: (
119
- source,
108
+ { nric, uen },
120
109
  issuer,
121
110
  recipient,
122
111
  inResponseTo,
@@ -124,8 +113,8 @@ const saml = {
124
113
  ) =>
125
114
  render(TEMPLATE, {
126
115
  issueInstant: moment.utc().format(),
127
- name: source.uen,
128
- value: base64.encode(render(corpPassTemplate, source)),
116
+ name: uen,
117
+ value: base64.encode(render(corpPassTemplate, { nric, uen })),
129
118
  recipient,
130
119
  issuer,
131
120
  inResponseTo,
@@ -136,88 +125,99 @@ const saml = {
136
125
 
137
126
  const oidc = {
138
127
  singPass: [
139
- 'S8979373D',
140
- 'S8116474F',
141
- 'S8723211E',
142
- 'S5062854Z',
143
- 'T0066846F',
144
- 'F9477325W',
145
- 'S3000024B',
146
- 'S6005040F',
147
- 'S6005041D',
148
- 'S6005042B',
149
- 'S6005043J',
150
- 'S6005044I',
151
- 'S6005045G',
152
- 'S6005046E',
153
- 'S6005047C',
154
- 'S6005064C',
155
- 'S6005065A',
156
- 'S6005066Z',
157
- 'S6005037F',
158
- 'S6005038D',
159
- 'S6005039B',
160
- 'G1612357P',
161
- 'G1612358M',
162
- 'F1612359P',
163
- 'F1612360U',
164
- 'F1612361R',
165
- 'F1612362P',
166
- 'F1612363M',
167
- 'F1612364K',
168
- 'F1612365W',
169
- 'F1612366T',
170
- 'F1612367Q',
171
- 'F1612358R',
172
- 'F1612354N',
173
- 'F1612357U',
174
- ...Object.keys(myinfo.v3.personas),
128
+ { nric: 'S8979373D', uuid: 'a9865837-7bd7-46ac-bef4-42a76a946424' },
129
+ { nric: 'S8116474F', uuid: 'f4b70aea-d639-4b79-b8d9-8ace5875f6b1' },
130
+ { nric: 'S8723211E', uuid: '178478de-fed7-4c03-a75e-e68c44d0d5f0' },
131
+ { nric: 'S5062854Z', uuid: '1bd2e743-8681-4079-a557-6a66a8d16386' },
132
+ { nric: 'T0066846F', uuid: '14f7ee8f-9e64-4170-a529-e55ca7578e2b' },
133
+ { nric: 'F9477325W', uuid: '2135fe5c-d07b-49d3-b960-aabb0ff2e05a' },
134
+ { nric: 'S3000024B', uuid: 'b5630beb-e3ee-4a31-aec5-534cdc087fd8' },
135
+ { nric: 'S6005040F', uuid: '6c6745d9-e6c5-40ee-8c96-5d737ddbc5e4' },
136
+ { nric: 'S6005041D', uuid: 'bd3fd1e0-c807-4b07-bbe4-b567cab54b8c' },
137
+ { nric: 'S6005042B', uuid: '2dd788c0-d11f-4d5b-99af-b89d2389b474' },
138
+ { nric: 'S6005043J', uuid: 'eb196477-36b3-4c0f-ae5e-2172e2f6a6d8' },
139
+ { nric: 'S6005044I', uuid: '843ebc6b-1de1-4d46-b1dd-9ad4aeac3a27' },
140
+ { nric: 'S6005045G', uuid: 'caafaedc-f369-498a-9e35-27e9cb7f0de2' },
141
+ { nric: 'S6005046E', uuid: 'f9b37d06-de3f-4c4f-8331-37a3b2ee6cb4' },
142
+ { nric: 'S6005047C', uuid: '57620e0f-fdf9-4f3e-a8f6-f6088e151395' },
143
+ { nric: 'S6005064C', uuid: '80952b2f-3455-4b59-b50f-39afbc418271' },
144
+ { nric: 'S6005065A', uuid: '3af48e26-69a1-43e3-b5f2-303098ef3210' },
145
+ { nric: 'S6005066Z', uuid: '8b2f8213-2fe9-493a-ac95-0b55e319e689' },
146
+ { nric: 'S6005037F', uuid: 'ae3d1d8c-6d14-449e-8ed1-9ce3d5e67607' },
147
+ { nric: 'S6005038D', uuid: '23d3bb45-a324-46d6-b0d9-2e94194ed9ae' },
148
+ { nric: 'S6005039B', uuid: '9ac807a2-5217-417a-a8d1-d7018b002b3f' },
149
+ { nric: 'G1612357P', uuid: 'eb125a02-3137-486f-9262-eab3e0c57a5f' },
150
+ { nric: 'G1612358M', uuid: 'd821900c-663d-4552-a753-a2e1cf8d124f' },
151
+ { nric: 'F1612359P', uuid: '08df8d35-600c-45fd-a812-b37a27b7856a' },
152
+ { nric: 'F1612360U', uuid: '1e90b698-23af-4acb-9fb4-eb5a80f444b6' },
153
+ { nric: 'F1612361R', uuid: 'bc134ee1-f104-4b26-9839-32047fecb963' },
154
+ { nric: 'F1612362P', uuid: '285e8366-f3bd-48b4-8153-b47260fc9f56' },
155
+ { nric: 'F1612363M', uuid: '379bc106-d3db-492c-a38e-fd27642ef47f' },
156
+ { nric: 'F1612364K', uuid: '108fa3ff-c85c-461e-ba1f-8edef62b68e2' },
157
+ { nric: 'F1612365W', uuid: '1275ae4e-02d2-4b09-9573-36ac610ede89' },
158
+ { nric: 'F1612366T', uuid: '23c6a3a4-d9d8-445f-a588-9d91831980a6' },
159
+ { nric: 'F1612367Q', uuid: '0c400961-eb00-425a-8df4-6656b0b9245a' },
160
+ { nric: 'F1612358R', uuid: '45669f5c-e9ac-43c6-bcd2-9c3757f1fa1c' },
161
+ { nric: 'F1612354N', uuid: 'c38ddb2d-9e5d-45c2-bb70-8ccb54fc8320' },
162
+ { nric: 'F1612357U', uuid: 'f904a2b1-4b61-47e2-bdad-e2d606325e20' },
163
+ ...Object.keys(myinfo.v3.personas).map((nric) => ({
164
+ nric,
165
+ uuid: myinfo.v3.personas[nric].uuid.value,
166
+ })),
175
167
  ],
176
168
  corpPass: [
177
169
  {
178
170
  nric: 'S8979373D',
171
+ uuid: 'a9865837-7bd7-46ac-bef4-42a76a946424',
179
172
  name: 'Name of S8979373D',
180
173
  isSingPassHolder: true,
181
174
  uen: '123456789A',
182
175
  },
183
176
  {
184
177
  nric: 'S8116474F',
178
+ uuid: 'f4b70aea-d639-4b79-b8d9-8ace5875f6b1',
185
179
  name: 'Name of S8116474F',
186
180
  isSingPassHolder: true,
187
181
  uen: '123456789A',
188
182
  },
189
183
  {
190
184
  nric: 'S8723211E',
185
+ uuid: '178478de-fed7-4c03-a75e-e68c44d0d5f0',
191
186
  name: 'Name of S8723211E',
192
187
  isSingPassHolder: true,
193
188
  uen: '123456789A',
194
189
  },
195
190
  {
196
191
  nric: 'S5062854Z',
192
+ uuid: '1bd2e743-8681-4079-a557-6a66a8d16386',
197
193
  name: 'Name of S5062854Z',
198
194
  isSingPassHolder: true,
199
195
  uen: '123456789B',
200
196
  },
201
197
  {
202
198
  nric: 'T0066846F',
199
+ uuid: '14f7ee8f-9e64-4170-a529-e55ca7578e2b',
203
200
  name: 'Name of T0066846F',
204
201
  isSingPassHolder: true,
205
202
  uen: '123456789B',
206
203
  },
207
204
  {
208
205
  nric: 'F9477325W',
206
+ uuid: '2135fe5c-d07b-49d3-b960-aabb0ff2e05a',
209
207
  name: 'Name of F9477325W',
210
208
  isSingPassHolder: false,
211
209
  uen: '123456789B',
212
210
  },
213
211
  {
214
212
  nric: 'S3000024B',
213
+ uuid: 'b5630beb-e3ee-4a31-aec5-534cdc087fd8',
215
214
  name: 'Name of S3000024B',
216
215
  isSingPassHolder: true,
217
216
  uen: '123456789C',
218
217
  },
219
218
  {
220
219
  nric: 'S6005040F',
220
+ uuid: '6c6745d9-e6c5-40ee-8c96-5d737ddbc5e4',
221
221
  name: 'Name of S6005040F',
222
222
  isSingPassHolder: true,
223
223
  uen: '123456789C',
@@ -225,17 +225,17 @@ const oidc = {
225
225
  ],
226
226
  create: {
227
227
  singPass: (
228
- uuid,
228
+ { nric, uuid },
229
229
  iss,
230
230
  aud,
231
231
  nonce,
232
232
  accessToken = crypto.randomBytes(15).toString('hex'),
233
233
  ) => {
234
- const nric = oidc.singPass[uuid]
235
234
  const sub = `s=${nric},u=${uuid}`
236
235
 
237
- const refreshToken = createRefreshToken(uuid)
238
236
  const accessTokenHash = hashToken(accessToken)
237
+
238
+ const refreshToken = crypto.randomBytes(20).toString('hex')
239
239
  const refreshTokenHash = hashToken(refreshToken)
240
240
 
241
241
  return {
@@ -254,7 +254,12 @@ const oidc = {
254
254
  },
255
255
  }
256
256
  },
257
- corpPass: async (uuid, iss, aud, nonce) => {
257
+ corpPass: async (
258
+ { nric, uuid, name, isSingPassHolder },
259
+ iss,
260
+ aud,
261
+ nonce,
262
+ ) => {
258
263
  const baseClaims = {
259
264
  iat: Math.floor(Date.now() / 1000),
260
265
  exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
@@ -262,8 +267,7 @@ const oidc = {
262
267
  aud,
263
268
  }
264
269
 
265
- const profile = oidc.corpPass[uuid]
266
- const sub = `s=${profile.nric},u=${uuid},c=SG`
270
+ const sub = `s=${nric},u=${uuid},c=SG`
267
271
 
268
272
  const accessTokenClaims = {
269
273
  ...baseClaims,
@@ -284,20 +288,23 @@ const oidc = {
284
288
 
285
289
  const accessTokenHash = hashToken(accessToken)
286
290
 
291
+ const refreshToken = crypto.randomBytes(20).toString('hex')
292
+ const refreshTokenHash = hashToken(refreshToken)
293
+
287
294
  return {
288
- refreshToken: 'refresh',
289
295
  accessToken,
296
+ refreshToken,
290
297
  idTokenClaims: {
291
298
  ...baseClaims,
292
- rt_hash: '',
299
+ rt_hash: refreshTokenHash,
293
300
  at_hash: accessTokenHash,
294
301
  amr: ['pwd'],
295
302
  sub,
296
303
  ...(nonce ? { nonce } : {}),
297
304
  userInfo: {
298
305
  CPAccType: 'User',
299
- CPUID_FullName: profile.name,
300
- ISSPHOLDER: profile.isSingPassHolder ? 'YES' : 'NO',
306
+ CPUID_FullName: name,
307
+ ISSPHOLDER: isSingPassHolder ? 'YES' : 'NO',
301
308
  },
302
309
  },
303
310
  }
@@ -305,15 +312,8 @@ const oidc = {
305
312
  },
306
313
  }
307
314
 
308
- const singPassNric = process.env.MOCKPASS_NRIC || saml.singPass[0]
309
- const corpPassNric = process.env.MOCKPASS_NRIC || saml.corpPass[0].nric
310
- const uen = process.env.MOCKPASS_UEN || saml.corpPass[0].uen
311
-
312
315
  module.exports = {
313
316
  saml,
314
317
  oidc,
315
318
  myinfo,
316
- singPassNric,
317
- corpPassNric,
318
- uen,
319
319
  }
@@ -0,0 +1,17 @@
1
+ const ExpiryMap = require('expiry-map')
2
+ const crypto = require('crypto')
3
+
4
+ const AUTH_CODE_TIMEOUT = 5 * 60 * 1000
5
+ const profileAndNonceStore = new ExpiryMap(AUTH_CODE_TIMEOUT)
6
+
7
+ const generateAuthCode = ({ profile, nonce }) => {
8
+ const authCode = crypto.randomBytes(45).toString('base64')
9
+ profileAndNonceStore.set(authCode, { profile, nonce })
10
+ return authCode
11
+ }
12
+
13
+ const lookUpByAuthCode = (authCode) => {
14
+ return profileAndNonceStore.get(authCode)
15
+ }
16
+
17
+ module.exports = { generateAuthCode, lookUpByAuthCode }
@@ -8,6 +8,8 @@ const qs = require('querystring')
8
8
  const { v1: uuid } = require('uuid')
9
9
 
10
10
  const assertions = require('../../assertions')
11
+ const { lookUpByAuthCode } = require('../../auth-code')
12
+ const { lookUpBySamlArtifact } = require('../../saml-artifact')
11
13
 
12
14
  const MYINFO_ASSERT_ENDPOINT = '/consent/myinfo-com'
13
15
  const AUTHORIZE_ENDPOINT = '/consent/oauth2/authorize'
@@ -47,41 +49,33 @@ const authorizeViaSAML = authorize(
47
49
  )
48
50
 
49
51
  const authorizeViaOIDC = authorize(
50
- (relayState) =>
51
- `/singpass/authorize?client_id=MYINFO-CONSENTPLATFORM&redirect_uri=${MYINFO_ASSERT_ENDPOINT}&state=${relayState}`,
52
+ (state) =>
53
+ `/singpass/authorize?client_id=MYINFO-CONSENTPLATFORM&redirect_uri=${MYINFO_ASSERT_ENDPOINT}&state=${state}`,
52
54
  )
53
55
 
54
56
  function config(app) {
55
57
  app.get(MYINFO_ASSERT_ENDPOINT, (req, res) => {
56
58
  const rawArtifact = req.query.SAMLart || req.query.code
57
59
  const artifact = rawArtifact.replace(/ /g, '+')
58
- const artifactBuffer = Buffer.from(artifact, 'base64')
59
- const artifactMessage = artifactBuffer.toString('utf8', 24)
60
- let index = artifactBuffer.readInt8(artifactBuffer.length - 1)
61
-
62
60
  const state = req.query.RelayState || req.query.state
63
- let id
64
- if (artifactMessage.startsWith('customNric:')) {
65
- id = artifactMessage.slice('customNric:'.length)
66
- } else {
67
- const assertionType = req.query.code ? 'oidc' : 'saml'
68
61
 
69
- // use env NRIC when SHOW_LOGIN_PAGE is false
70
- if (index === -1) {
71
- index = assertions[assertionType].singPass.indexOf(
72
- assertions.singPassNric,
73
- )
74
- }
75
- id = assertions[assertionType].singPass[index]
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'
76
69
  }
77
- const persona = assertions.myinfo[req.query.code ? 'v3' : 'v2'].personas[id]
70
+ const { nric: id } = profile
71
+
72
+ const persona = assertions.myinfo[myinfoVersion].personas[id]
78
73
  if (!persona) {
79
74
  res.status(404).send({
80
75
  message: 'Cannot find MyInfo Persona',
81
76
  artifact,
82
- index,
77
+ myinfoVersion,
83
78
  id,
84
- persona,
85
79
  })
86
80
  } else {
87
81
  res.cookie('connect.sid', id)
@@ -6,51 +6,85 @@ const path = require('path')
6
6
  const ExpiryMap = require('expiry-map')
7
7
 
8
8
  const assertions = require('../assertions')
9
- const { samlArtifact } = require('../saml-artifact')
9
+ const { generateAuthCode, lookUpByAuthCode } = require('../auth-code')
10
10
 
11
11
  const LOGIN_TEMPLATE = fs.readFileSync(
12
12
  path.resolve(__dirname, '../../static/html/login-page.html'),
13
13
  'utf8',
14
14
  )
15
- const NONCE_TIMEOUT = 5 * 60 * 1000
16
- const nonceStore = new ExpiryMap(NONCE_TIMEOUT)
15
+ const REFRESH_TOKEN_TIMEOUT = 24 * 60 * 60 * 1000
16
+ const profileStore = new ExpiryMap(REFRESH_TOKEN_TIMEOUT)
17
17
 
18
18
  const signingPem = fs.readFileSync(
19
19
  path.resolve(__dirname, '../../static/certs/spcp-key.pem'),
20
20
  )
21
21
 
22
+ const buildAssertURL = (redirectURI, authCode, state) =>
23
+ `${redirectURI}?code=${encodeURIComponent(
24
+ authCode,
25
+ )}&state=${encodeURIComponent(state)}`
26
+
22
27
  const idGenerator = {
23
- singPass: (rawId) =>
24
- assertions.myinfo.v3.personas[rawId] ? `${rawId} [MyInfo]` : rawId,
25
- corpPass: (rawId) => `${rawId.nric} / UEN: ${rawId.uen}`,
28
+ singPass: ({ nric }) =>
29
+ assertions.myinfo.v3.personas[nric] ? `${nric} [MyInfo]` : nric,
30
+ corpPass: ({ nric, uen }) => `${nric} / UEN: ${uen}`,
31
+ }
32
+
33
+ const customProfileFromHeaders = {
34
+ singPass: (req) => {
35
+ const customNricHeader = req.header('X-Custom-NRIC')
36
+ const customUuidHeader = req.header('X-Custom-UUID')
37
+ if (!customNricHeader || !customUuidHeader) {
38
+ return false
39
+ }
40
+ return { nric: customNricHeader, uuid: customUuidHeader }
41
+ },
42
+ corpPass: (req) => {
43
+ const customNricHeader = req.header('X-Custom-NRIC')
44
+ const customUuidHeader = req.header('X-Custom-UUID')
45
+ const customUenHeader = req.header('X-Custom-UEN')
46
+ if (!customNricHeader || !customUuidHeader || !customUenHeader) {
47
+ return false
48
+ }
49
+ return {
50
+ nric: customNricHeader,
51
+ uuid: customUuidHeader,
52
+ uen: customUenHeader,
53
+ }
54
+ },
26
55
  }
27
56
 
28
- function config(app, { showLoginPage, idpConfig, serviceProvider }) {
57
+ function config(app, { showLoginPage, serviceProvider }) {
29
58
  for (const idp of ['singPass', 'corpPass']) {
59
+ const profiles = assertions.oidc[idp]
60
+ const defaultProfile =
61
+ profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0]
62
+
30
63
  app.get(`/${idp.toLowerCase()}/authorize`, (req, res) => {
31
- const redirectURI = req.query.redirect_uri
32
- const state = encodeURIComponent(req.query.state)
64
+ const { redirect_uri: redirectURI, state, nonce } = req.query
33
65
  if (showLoginPage(req)) {
34
- const oidc = assertions.oidc[idp]
35
- const values = oidc.map((rawId, index) => {
36
- const code = encodeURIComponent(
37
- samlArtifact(idpConfig[idp].id, index),
38
- )
39
- if (req.query.nonce) {
40
- nonceStore.set(code, req.query.nonce)
41
- }
42
- const assertURL = `${redirectURI}?code=${code}&state=${state}`
43
- const id = idGenerator[idp](rawId)
66
+ const values = profiles.map((profile) => {
67
+ const authCode = generateAuthCode({ profile, nonce })
68
+ const assertURL = buildAssertURL(redirectURI, authCode, state)
69
+ const id = idGenerator[idp](profile)
44
70
  return { id, assertURL }
45
71
  })
46
- const response = render(LOGIN_TEMPLATE, { values })
72
+ const response = render(LOGIN_TEMPLATE, {
73
+ values,
74
+ customProfileConfig: {
75
+ endpoint: `/${idp.toLowerCase()}/authorize/custom-profile`,
76
+ showUuid: true,
77
+ showUen: idp === 'corpPass',
78
+ redirectURI,
79
+ state,
80
+ nonce,
81
+ },
82
+ })
47
83
  res.send(response)
48
84
  } else {
49
- const code = encodeURIComponent(samlArtifact(idpConfig[idp].id))
50
- if (req.query.nonce) {
51
- nonceStore.set(code, req.query.nonce)
52
- }
53
- const assertURL = `${redirectURI}?code=${code}&state=${state}`
85
+ const profile = customProfileFromHeaders[idp](req) || defaultProfile
86
+ const authCode = generateAuthCode({ profile, nonce })
87
+ const assertURL = buildAssertURL(redirectURI, authCode, state)
54
88
  console.warn(
55
89
  `Redirecting login from ${req.query.client_id} to ${redirectURI}`,
56
90
  )
@@ -58,45 +92,47 @@ function config(app, { showLoginPage, idpConfig, serviceProvider }) {
58
92
  }
59
93
  })
60
94
 
95
+ app.get(`/${idp.toLowerCase()}/authorize/custom-profile`, (req, res) => {
96
+ const { nric, uuid, uen, redirectURI, state, nonce } = req.query
97
+
98
+ const profile = { nric, uuid }
99
+ if (idp === 'corpPass') {
100
+ profile.name = `Name of ${nric}`
101
+ profile.isSingPassHolder = false
102
+ profile.uen = uen
103
+ }
104
+
105
+ const authCode = generateAuthCode({ profile, nonce })
106
+ const assertURL = buildAssertURL(redirectURI, authCode, state)
107
+ res.redirect(assertURL)
108
+ })
109
+
61
110
  app.post(
62
111
  `/${idp.toLowerCase()}/token`,
63
112
  express.urlencoded({ extended: false }),
64
113
  async (req, res) => {
65
114
  const { client_id: aud, grant_type: grant } = req.body
66
- let nonce, uuid
115
+ let profile, nonce
67
116
 
68
117
  if (grant === 'refresh_token') {
69
- const { refresh_token: refreshToken } = req.body
70
- console.warn(`Refreshing tokens with ${refreshToken}`)
118
+ const { refresh_token: suppliedRefreshToken } = req.body
119
+ console.warn(`Refreshing tokens with ${suppliedRefreshToken}`)
71
120
 
72
- uuid = refreshToken.split('/')[0]
121
+ profile = profileStore.get(suppliedRefreshToken)
73
122
  } else {
74
- const { code: artifact } = req.body
123
+ const { code: authCode } = req.body
75
124
  console.warn(
76
- `Received artifact ${artifact} from ${aud} and ${req.body.redirect_uri}`,
125
+ `Received auth code ${authCode} from ${aud} and ${req.body.redirect_uri}`,
77
126
  )
78
- const artifactBuffer = Buffer.from(artifact, 'base64')
79
- uuid = artifactBuffer.readInt8(artifactBuffer.length - 1)
80
- nonce = nonceStore.get(encodeURIComponent(artifact))
127
+ ;({ profile, nonce } = lookUpByAuthCode(authCode))
81
128
  }
82
129
 
83
- // use env NRIC when SHOW_LOGIN_PAGE is false
84
- if (uuid === -1) {
85
- uuid =
86
- idp === 'singPass'
87
- ? assertions.oidc.singPass.indexOf(assertions.singPassNric)
88
- : assertions.oidc.corpPass.findIndex(
89
- (c) => c.nric === assertions.corpPassNric,
90
- )
91
- }
130
+ const iss = `${req.protocol}://${req.get('host')}`
92
131
 
93
132
  const { idTokenClaims, accessToken, refreshToken } =
94
- await assertions.oidc.create[idp](
95
- uuid,
96
- `${req.protocol}://${req.get('host')}`,
97
- aud,
98
- nonce,
99
- )
133
+ await assertions.oidc.create[idp](profile, iss, aud, nonce)
134
+
135
+ profileStore.set(refreshToken, profile)
100
136
 
101
137
  const signingKey = await jose.JWK.asKey(signingPem, 'pem')
102
138
  const signedIdToken = await jose.JWS.createSign(
@@ -8,7 +8,10 @@ const moment = require('moment')
8
8
 
9
9
  const assertions = require('../assertions')
10
10
  const crypto = require('../crypto')
11
- const { samlArtifact, hashPartnerId } = require('../saml-artifact')
11
+ const {
12
+ generateSamlArtifact,
13
+ lookUpBySamlArtifact,
14
+ } = require('../saml-artifact')
12
15
 
13
16
  const domParser = new DOMParser()
14
17
  const dom = (xmlString) => domParser.parseFromString(xmlString)
@@ -24,10 +27,36 @@ const LOGIN_TEMPLATE = fs.readFileSync(
24
27
 
25
28
  const MYINFO_ASSERT_ENDPOINT = '/consent/myinfo-com'
26
29
 
30
+ const buildAssertURL = (assertEndpoint, samlArt, relayState) => {
31
+ let assertURL = `${assertEndpoint}?SAMLart=${encodeURIComponent(samlArt)}`
32
+ if (relayState !== undefined) {
33
+ assertURL += `&RelayState=${encodeURIComponent(relayState)}`
34
+ }
35
+ return assertURL
36
+ }
37
+
27
38
  const idGenerator = {
28
- singPass: (rawId) =>
29
- assertions.myinfo.v2.personas[rawId] ? `${rawId} [MyInfo]` : rawId,
30
- corpPass: (rawId) => `${rawId.nric} / UEN: ${rawId.uen}`,
39
+ singPass: ({ nric }) =>
40
+ assertions.myinfo.v2.personas[nric] ? `${nric} [MyInfo]` : nric,
41
+ corpPass: ({ nric, uen }) => `${nric} / UEN: ${uen}`,
42
+ }
43
+
44
+ const customProfileFromHeaders = {
45
+ singPass: (req) => {
46
+ const customNricHeader = req.header('X-Custom-NRIC')
47
+ if (!customNricHeader) {
48
+ return false
49
+ }
50
+ return { nric: customNricHeader }
51
+ },
52
+ corpPass: (req) => {
53
+ const customNricHeader = req.header('X-Custom-NRIC')
54
+ const customUenHeader = req.header('X-Custom-UEN')
55
+ if (!customNricHeader || !customUenHeader) {
56
+ return false
57
+ }
58
+ return { nric: customNricHeader, uen: customUenHeader }
59
+ },
31
60
  }
32
61
 
33
62
  function config(
@@ -38,38 +67,41 @@ function config(
38
67
  crypto(serviceProvider)
39
68
 
40
69
  for (const idp of ['singPass', 'corpPass']) {
70
+ const partnerId = idpConfig[idp].id
71
+ const partnerAssertEndpoint = idpConfig[idp].assertEndpoint
72
+
73
+ const profiles = assertions.saml[idp]
74
+ const defaultProfile =
75
+ profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0]
76
+
41
77
  app.get(`/${idp.toLowerCase()}/logininitial`, (req, res) => {
42
78
  const assertEndpoint =
43
79
  req.query.esrvcID === 'MYINFO-CONSENTPLATFORM' && idp === 'singPass'
44
80
  ? MYINFO_ASSERT_ENDPOINT
45
- : idpConfig[idp].assertEndpoint || req.query.PartnerId
81
+ : partnerAssertEndpoint || req.query.PartnerId
46
82
  const relayState = req.query.Target
47
- const partnerId = idpConfig[idp].id
48
83
  if (showLoginPage(req)) {
49
- const saml = assertions.saml[idp]
50
- const values = saml.map((rawId, index) => {
51
- const samlArt = encodeURIComponent(samlArtifact(partnerId, index))
52
- let assertURL = `${assertEndpoint}?SAMLart=${samlArt}`
53
- if (relayState !== undefined) {
54
- assertURL += `&RelayState=${encodeURIComponent(relayState)}`
55
- }
56
- const id = idGenerator[idp](rawId)
84
+ const values = profiles.map((profile) => {
85
+ const samlArt = generateSamlArtifact(partnerId, profile)
86
+ const assertURL = buildAssertURL(assertEndpoint, samlArt, relayState)
87
+ const id = idGenerator[idp](profile)
57
88
  return { id, assertURL }
58
89
  })
59
- const hashedPartnerId = hashPartnerId(partnerId)
60
90
  const response = render(LOGIN_TEMPLATE, {
61
91
  values,
62
- assertEndpoint,
63
- relayState,
64
- hashedPartnerId,
92
+ customProfileConfig: {
93
+ endpoint: `/${idp.toLowerCase()}/logininitial/custom-profile`,
94
+ showUuid: false,
95
+ showUen: idp === 'corpPass',
96
+ assertEndpoint,
97
+ relayState,
98
+ },
65
99
  })
66
100
  res.send(response)
67
101
  } else {
68
- const samlArt = encodeURIComponent(samlArtifact(partnerId))
69
- let assertURL = `${assertEndpoint}?SAMLart=${samlArt}`
70
- if (relayState !== undefined) {
71
- assertURL += `&RelayState=${encodeURIComponent(relayState)}`
72
- }
102
+ const profile = customProfileFromHeaders[idp](req) || defaultProfile
103
+ const samlArt = generateSamlArtifact(partnerId, profile)
104
+ const assertURL = buildAssertURL(assertEndpoint, samlArt, relayState)
73
105
  console.warn(
74
106
  `Redirecting login from ${req.query.PartnerId} to ${assertURL}`,
75
107
  )
@@ -77,6 +109,19 @@ function config(
77
109
  }
78
110
  })
79
111
 
112
+ app.get(`/${idp.toLowerCase()}/logininitial/custom-profile`, (req, res) => {
113
+ const { nric, uen, assertEndpoint, relayState } = req.query
114
+
115
+ const profile = { nric }
116
+ if (idp === 'corpPass') {
117
+ profile.uen = uen
118
+ }
119
+
120
+ const samlArt = generateSamlArtifact(partnerId, profile)
121
+ const assertURL = buildAssertURL(assertEndpoint, samlArt, relayState)
122
+ res.redirect(assertURL)
123
+ })
124
+
80
125
  app.post(
81
126
  `/${idp.toLowerCase()}/soap`,
82
127
  express.text({ type: 'text/xml' }),
@@ -102,28 +147,7 @@ function config(
102
147
  // Handle encoded base64 Artifact
103
148
  // Take the template and plug in the typical SingPass/CorpPass response
104
149
  // Sign and encrypt the assertion
105
- const samlArtifactBuffer = Buffer.from(samlArtifact, 'base64')
106
- const samlArtifactMessage = samlArtifactBuffer.toString('utf8', 24)
107
-
108
- let nric
109
- if (samlArtifactMessage.startsWith('customNric:')) {
110
- nric = samlArtifactMessage.slice('customNric:'.length)
111
- } else {
112
- let index = samlArtifactBuffer.readInt8(
113
- samlArtifactBuffer.length - 1,
114
- )
115
- // use env NRIC when SHOW_LOGIN_PAGE is false
116
- if (index === -1) {
117
- index =
118
- idp === 'singPass'
119
- ? assertions.saml.singPass.indexOf(assertions.singPassNric)
120
- : assertions.saml.corpPass.findIndex(
121
- (c) => c.nric === assertions.corpPassNric,
122
- )
123
- }
124
-
125
- nric = assertions.saml[idp][index]
126
- }
150
+ const profile = lookUpBySamlArtifact(samlArtifact)
127
151
 
128
152
  const samlArtifactResolveId = xpath.select(
129
153
  "string(//*[local-name(.)='ArtifactResolve']/@ID)",
@@ -131,9 +155,9 @@ function config(
131
155
  )
132
156
 
133
157
  let result = assertions.saml.create[idp](
134
- nric,
135
- idpConfig[idp].id,
136
- idpConfig[idp].assertEndpoint,
158
+ profile,
159
+ partnerId,
160
+ partnerAssertEndpoint,
137
161
  samlArtifactResolveId,
138
162
  )
139
163
 
@@ -148,8 +172,8 @@ function config(
148
172
  let response = render(TEMPLATE, {
149
173
  assertion,
150
174
  issueInstant: moment.utc().format(),
151
- issuer: idpConfig[idp].id,
152
- destination: idpConfig[idp].assertEndpoint,
175
+ issuer: partnerId,
176
+ destination: partnerAssertEndpoint,
153
177
  inResponseTo: samlArtifactResolveId,
154
178
  })
155
179
  if (cryptoConfig.signResponse) {
@@ -3,17 +3,14 @@ const fs = require('fs')
3
3
  const { render } = require('mustache')
4
4
  const jose = require('node-jose')
5
5
  const path = require('path')
6
- const ExpiryMap = require('expiry-map')
7
6
 
8
7
  const assertions = require('../assertions')
9
- const { samlArtifact } = require('../saml-artifact')
8
+ const { generateAuthCode, lookUpByAuthCode } = require('../auth-code')
10
9
 
11
10
  const LOGIN_TEMPLATE = fs.readFileSync(
12
11
  path.resolve(__dirname, '../../static/html/login-page.html'),
13
12
  'utf8',
14
13
  )
15
- const NONCE_TIMEOUT = 5 * 60 * 1000
16
- const nonceStore = new ExpiryMap(NONCE_TIMEOUT)
17
14
 
18
15
  const PATH_PREFIX = '/sgid/v1/oauth'
19
16
 
@@ -22,38 +19,37 @@ const signingPem = fs.readFileSync(
22
19
  )
23
20
 
24
21
  const idGenerator = {
25
- singPass: (rawId) =>
26
- assertions.myinfo.v3.personas[rawId] ? `${rawId} [MyInfo]` : rawId,
22
+ singPass: ({ nric }) =>
23
+ assertions.myinfo.v3.personas[nric] ? `${nric} [MyInfo]` : nric,
27
24
  }
28
25
 
29
- function config(app, { showLoginPage, idpConfig, serviceProvider }) {
26
+ const buildAssertURL = (redirectURI, authCode, state) =>
27
+ `${redirectURI}?code=${encodeURIComponent(
28
+ authCode,
29
+ )}&state=${encodeURIComponent(state)}`
30
+
31
+ function config(app, { showLoginPage, serviceProvider }) {
32
+ const profiles = assertions.oidc.singPass
33
+ const defaultProfile =
34
+ profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0]
35
+
30
36
  app.get(`${PATH_PREFIX}/authorize`, (req, res) => {
31
- const redirectURI = req.query.redirect_uri
32
- const state = encodeURIComponent(req.query.state)
37
+ const { redirect_uri: redirectURI, state, nonce } = req.query
33
38
  if (showLoginPage(req)) {
34
- const oidc = assertions.oidc.singPass
35
- const values = oidc
36
- .filter((rawId) => assertions.myinfo.v3.personas[rawId])
37
- .map((rawId) => {
38
- const index = oidc.indexOf(rawId)
39
- const code = encodeURIComponent(
40
- samlArtifact(idpConfig.singPass.id, index),
41
- )
42
- if (req.query.nonce) {
43
- nonceStore.set(code, req.query.nonce)
44
- }
45
- const assertURL = `${redirectURI}?code=${code}&state=${state}`
46
- const id = idGenerator.singPass(rawId)
39
+ const values = profiles
40
+ .filter((profile) => assertions.myinfo.v3.personas[profile.nric])
41
+ .map((profile) => {
42
+ const authCode = generateAuthCode({ profile, nonce })
43
+ const assertURL = buildAssertURL(redirectURI, authCode, state)
44
+ const id = idGenerator.singPass(profile)
47
45
  return { id, assertURL }
48
46
  })
49
47
  const response = render(LOGIN_TEMPLATE, { values })
50
48
  res.send(response)
51
49
  } else {
52
- const code = encodeURIComponent(samlArtifact(idpConfig.singPass.id))
53
- if (req.query.nonce) {
54
- nonceStore.set(code, req.query.nonce)
55
- }
56
- const assertURL = `${redirectURI}?code=${code}&state=${state}`
50
+ const profile = defaultProfile
51
+ const authCode = generateAuthCode({ profile, nonce })
52
+ const assertURL = buildAssertURL(redirectURI, authCode, state)
57
53
  console.warn(
58
54
  `Redirecting login from ${req.query.client_id} to ${assertURL}`,
59
55
  )
@@ -67,31 +63,24 @@ function config(app, { showLoginPage, idpConfig, serviceProvider }) {
67
63
  express.urlencoded({ extended: true }),
68
64
  async (req, res) => {
69
65
  console.log(req.body)
70
- const { client_id: aud, code: artifact } = req.body
71
- let uuid
66
+ const { client_id: aud, code: authCode } = req.body
72
67
 
73
68
  console.warn(
74
- `Received artifact ${artifact} from ${aud} and ${req.body.redirect_uri}`,
69
+ `Received auth code ${authCode} from ${aud} and ${req.body.redirect_uri}`,
75
70
  )
76
71
  try {
77
- const artifactBuffer = Buffer.from(artifact, 'base64')
78
- uuid = artifactBuffer.readInt8(artifactBuffer.length - 1)
79
- const nonce = nonceStore.get(encodeURIComponent(artifact))
80
-
81
- // use env NRIC when SHOW_LOGIN_PAGE is false
82
- if (uuid === -1) {
83
- uuid = assertions.oidc.singPass.indexOf(assertions.singPassNric)
84
- }
85
-
86
- const accessToken = `${uuid}`
87
- const { idTokenClaims, refreshToken } =
88
- await assertions.oidc.create.singPass(
89
- uuid,
90
- `${req.protocol}://${req.get('host')}`,
91
- aud,
92
- nonce,
93
- accessToken,
94
- )
72
+ const { profile, nonce } = lookUpByAuthCode(authCode)
73
+
74
+ const accessToken = profile.uuid
75
+ const iss = `${req.protocol}://${req.get('host')}`
76
+
77
+ const { idTokenClaims, refreshToken } = assertions.oidc.create.singPass(
78
+ profile,
79
+ iss,
80
+ aud,
81
+ nonce,
82
+ accessToken,
83
+ )
95
84
  // Change sub from `s=${nric},u=${uuid}`
96
85
  // to `u=${uuid}` to be consistent with userinfo sub
97
86
  idTokenClaims.sub = idTokenClaims.sub.split(',')[1]
@@ -123,7 +112,7 @@ function config(app, { showLoginPage, idpConfig, serviceProvider }) {
123
112
  const uuid = (
124
113
  req.headers.authorization || req.headers.Authorization
125
114
  ).replace('Bearer ', '')
126
- const nric = assertions.oidc.singPass[uuid]
115
+ const nric = assertions.oidc.singPass.find((p) => p.uuid === uuid).nric
127
116
  const persona = assertions.myinfo.v3.personas[nric]
128
117
  const name = persona.name.value
129
118
  const dateOfBirth = persona.dob.value
@@ -1,5 +1,9 @@
1
+ const ExpiryMap = require('expiry-map')
1
2
  const crypto = require('crypto')
2
3
 
4
+ const SAML_ARTIFACT_TIMEOUT = 5 * 60 * 1000
5
+ const profileStore = new ExpiryMap(SAML_ARTIFACT_TIMEOUT)
6
+
3
7
  /**
4
8
  * Construct a SingPass/CorpPass SAML artifact, a base64
5
9
  * encoding of a byte sequence consisting of the following:
@@ -8,25 +12,28 @@ const crypto = require('crypto')
8
12
  * - a 20-byte sha1 hash of the partner id, and;
9
13
  * - a 20-byte random sequence that is effectively the message id
10
14
  * @param {string} partnerId - the partner id
11
- * @param {number} [index] - represents the nth identity to use. Defaults to -1
15
+ * @param {string} profile - the profile (identity) to store
12
16
  * @return {string} the SAML artifact, a base64 string
13
17
  * containing the type code, the endpoint index,
14
- * the hash of the partner id, followed by 20 random bytes
18
+ * the hash of the partner id, followed by 20 random bytes;
19
+ * this can be used to look up the stored profile (identity)
15
20
  */
16
- function samlArtifact(partnerId, index) {
17
- const hashedPartnerId = hashPartnerId(partnerId)
18
- const randomBytes = crypto.randomBytes(19).toString('hex')
19
- const indexBuffer = Buffer.alloc(1)
20
- indexBuffer.writeInt8(index || index === 0 ? index : -1)
21
- const indexString = indexBuffer.toString('hex')
22
- return Buffer.from(
23
- `00040000${hashedPartnerId}${randomBytes}${indexString}`,
21
+ function generateSamlArtifact(partnerId, profile) {
22
+ const hashedPartnerId = crypto
23
+ .createHash('sha1')
24
+ .update(partnerId, 'utf8')
25
+ .digest('hex')
26
+ const randomBytes = crypto.randomBytes(20).toString('hex')
27
+ const samlArtifact = Buffer.from(
28
+ `00040000${hashedPartnerId}${randomBytes}`,
24
29
  'hex',
25
30
  ).toString('base64')
31
+ profileStore.set(samlArtifact, profile)
32
+ return samlArtifact
26
33
  }
27
34
 
28
- function hashPartnerId(partnerId) {
29
- return crypto.createHash('sha1').update(partnerId, 'utf8').digest('hex')
35
+ function lookUpBySamlArtifact(samlArtifact) {
36
+ return profileStore.get(samlArtifact)
30
37
  }
31
38
 
32
- module.exports = { samlArtifact, hashPartnerId }
39
+ module.exports = { generateSamlArtifact, lookUpBySamlArtifact }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/mockpass",
3
- "version": "2.8.1",
3
+ "version": "2.9.2",
4
4
  "description": "A mock SingPass/CorpPass server for dev purposes",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -768,31 +768,6 @@ function invalidLoginAction(errorMessage, captchaVal) {
768
768
  }
769
769
  }
770
770
 
771
- function hexEncode(str) {
772
- var result = '';
773
- for (var i = 0; i < str.length; i++) {
774
- result += str.charCodeAt(i).toString(16);
775
- }
776
- return result;
777
- }
778
-
779
- function hexToBase64(hexString) {
780
- return btoa(hexString.match(/\w{2}/g).map(function(a) {
781
- return String.fromCharCode(parseInt(a, 16));
782
- }).join(''));
783
- }
784
-
785
- function generateSamlArtFromCustomNric() {
786
- var customNric = document.getElementById('customNric').value;
787
- if (customNric.length !== 9) {
788
- return false;
789
- }
790
- var hashedPartnerId = document.getElementById('hashedPartnerId').value;
791
- var artifactDataHex = '00040000' + hashedPartnerId + hexEncode('customNric:' + customNric);
792
- document.getElementById('customNricSamlArt').value = hexToBase64(artifactDataHex);
793
- return true;
794
- }
795
-
796
771
  /*******************************************************************************
797
772
  * WOGAA RELATED METHODS STARTS
798
773
  ******************************************************************************/
@@ -176,33 +176,48 @@
176
176
  </div>
177
177
  </div>
178
178
  </div>
179
- <div>
180
-
181
- <input type="hidden" name="CSRFToken" value="null" />
182
- </div>
179
+ <div>
180
+ <input type="hidden" name="CSRFToken" value="null" />
181
+ </div>
183
182
  </form>
184
- <form action="{{assertEndpoint}}" method="get" onsubmit="return generateSamlArtFromCustomNric()">
183
+ {{#customProfileConfig}}
184
+ <form action="{{endpoint}}" class="innate-form" method="get">
185
+ <br>
186
+ <h6>or with your own user</h6>
187
+ <br>
188
+
189
+ <label for="nric">NRIC</label>
190
+ <input type="text" name="nric" id="nric" value="S1234567A" minlength="9" maxlength="9">
191
+
192
+ {{#showUuid}}
193
+ <label for="uuid">UUID</label>
194
+ <input type="text" name="uuid" id="uuid" value="ef39a074-b64d-4990-a937-6f80772e2bb8" minlength="36" maxlength="36">
195
+ {{/showUuid}}
196
+
197
+ {{#showUen}}
198
+ <label for="uen">UEN</label>
199
+ <input type="text" name="uen" id="uen" value="123456789D" minlength="9" maxlength="10">
200
+ {{/showUen}}
201
+
202
+ {{#assertEndpoint}}<input type="hidden" name="assertEndpoint" value="{{ assertEndpoint }}" />{{/assertEndpoint}}
203
+ {{#redirectURI}}<input type="hidden" name="redirectURI" value="{{ redirectURI }}" />{{/redirectURI}}
204
+ {{#relayState}}<input type="hidden" name="relayState" value="{{ relayState }}" />{{/relayState}}
205
+ {{#state}}<input type="hidden" name="state" value="{{ state }}" />{{/state}}
206
+ {{#nonce}}<input type="hidden" name="nonce" value="{{ nonce }}" />{{/nonce}}
207
+
208
+ <button autofocus="" type="submit">Login</button>
185
209
  <br>
186
- {{#assertEndpoint}}
187
- <h6>or with your own user</h6>
188
- <br>
189
- <input type="hidden" name="RelayState" value="{{ relayState }}" />
190
- <input type="hidden" id="hashedPartnerId" value="{{ hashedPartnerId }}" />
191
- <input minlength="9" maxlength="9" id="customNric" placeholder="NRIC" value="S1234567A" style="width: 100%; border: 2px solid #ccc; border-radius: 5px; background: white; color: rgb(42, 45, 51); text-align: left;">
192
- <input type="hidden" id="customNricSamlArt" name="SAMLart" />
193
- <button autofocus="" type="submit">Login</button>
194
- <br>
195
- <br>
196
- {{/assertEndpoint}}
197
- <div class="login__footer">
198
- <div class="login-note">
199
- Forgot <a aria-label="Forgot MockPass ID">MockPass ID</a> or <a aria-label="Forgot password">password</a>?
200
- </div>
201
- <div class="sp-reglink">
202
- Don't have an account? <a aria-label="Register now">Register now</a>
203
- </div>
204
- </div>
205
210
  </form>
211
+ {{/customProfileConfig}}
212
+ <br>
213
+ <div class="login__footer">
214
+ <div class="login-note">
215
+ Forgot <a aria-label="Forgot MockPass ID">MockPass ID</a> or <a aria-label="Forgot password">password</a>?
216
+ </div>
217
+ <div class="sp-reglink">
218
+ Don't have an account? <a aria-label="Register now">Register now</a>
219
+ </div>
220
+ </div>
206
221
  </div>
207
222
  <div class="clearfix"></div>
208
223
  </div>
@@ -268,4 +283,4 @@
268
283
  </div>
269
284
  </div>
270
285
  </body>
271
- </html>
286
+ </html>
@@ -9765,6 +9765,12 @@
9765
9765
  "value": "97399245"
9766
9766
  }
9767
9767
  },
9768
+ "uuid": {
9769
+ "lastupdated": "2020-04-16",
9770
+ "source": "1",
9771
+ "classification": "C",
9772
+ "value": "b6cb9308-733b-42b3-ad38-9b5afc890206"
9773
+ },
9768
9774
  "mailadd": {
9769
9775
  "country": {
9770
9776
  "code": "SG",
@@ -27681,7 +27687,7 @@
27681
27687
  "lastupdated": "2020-04-16",
27682
27688
  "source": "1",
27683
27689
  "classification": "C",
27684
- "value": ""
27690
+ "value": "632d6cd6-7125-4fcc-b21b-e33aedf05806"
27685
27691
  },
27686
27692
  "mailadd": {
27687
27693
  "country": {