@opengovsg/mockpass 4.0.12 → 4.1.0

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
@@ -159,6 +159,7 @@ Common configuration:
159
159
  | Configuration item | Explanation |
160
160
  |---|---|
161
161
  | Port number | **Overview:** What port number MockPass will listen for HTTP requests on. <br> **Default:** 5156. <br> **How to configure:** Set the env var `MOCKPASS_PORT` or `PORT` to some port number. |
162
+ | Stateless Mode | **Overview:** Enable for environments where the state of the process is not guaranteed, such as in serverless contexts. <br> **Default:** not set. <br> **How to configure:** Set the env var `MOCKPASS_STATELESS` to `true` or `false`. |
162
163
 
163
164
  Run MockPass:
164
165
 
package/app.js CHANGED
@@ -35,12 +35,15 @@ const cryptoConfig = {
35
35
  process.env.RESOLVE_ARTIFACT_REQUEST_SIGNED !== 'false',
36
36
  }
37
37
 
38
+ const isStateless = process.env.MOCKPASS_STATELESS === 'true'
39
+
38
40
  const options = {
39
41
  serviceProvider,
40
42
  showLoginPage: (req) =>
41
43
  (req.header('X-Show-Login-Page') || process.env.SHOW_LOGIN_PAGE) === 'true',
42
44
  encryptMyInfo: process.env.ENCRYPT_MYINFO === 'true',
43
45
  cryptoConfig,
46
+ isStateless,
44
47
  }
45
48
 
46
49
  const app = express()
@@ -50,7 +53,7 @@ configOIDC(app, options)
50
53
  configOIDCv2(app, options)
51
54
  configSGID(app, options)
52
55
 
53
- configMyInfo.consent(app)
56
+ configMyInfo.consent(app, options)
54
57
  configMyInfo.v3(app, options)
55
58
 
56
59
  app.enable('trust proxy')
package/lib/auth-code.js CHANGED
@@ -4,14 +4,24 @@ const crypto = require('crypto')
4
4
  const AUTH_CODE_TIMEOUT = 5 * 60 * 1000
5
5
  const profileAndNonceStore = new ExpiryMap(AUTH_CODE_TIMEOUT)
6
6
 
7
- const generateAuthCode = ({ profile, scopes, nonce }) => {
8
- const authCode = crypto.randomBytes(45).toString('base64')
7
+ const generateAuthCode = (
8
+ { profile, scopes, nonce },
9
+ { isStateless = false },
10
+ ) => {
11
+ const authCode = isStateless
12
+ ? Buffer.from(JSON.stringify({ profile, scopes, nonce })).toString(
13
+ 'base64url',
14
+ )
15
+ : crypto.randomBytes(45).toString('base64')
16
+
9
17
  profileAndNonceStore.set(authCode, { profile, scopes, nonce })
10
18
  return authCode
11
19
  }
12
20
 
13
- const lookUpByAuthCode = (authCode) => {
14
- return profileAndNonceStore.get(authCode)
21
+ const lookUpByAuthCode = (authCode, { isStateless = false }) => {
22
+ return isStateless
23
+ ? JSON.parse(Buffer.from(authCode, 'base64url').toString('utf-8'))
24
+ : profileAndNonceStore.get(authCode)
15
25
  }
16
26
 
17
27
  module.exports = { generateAuthCode, lookUpByAuthCode }
@@ -47,13 +47,13 @@ const authorizeViaOIDC = authorize(
47
47
  `/singpass/authorize?client_id=MYINFO-CONSENTPLATFORM&redirect_uri=${MYINFO_ASSERT_ENDPOINT}&state=${state}`,
48
48
  )
49
49
 
50
- function config(app) {
50
+ function config(app, { isStateless }) {
51
51
  app.get(MYINFO_ASSERT_ENDPOINT, (req, res) => {
52
52
  const rawArtifact = req.query.SAMLart || req.query.code
53
53
  const artifact = rawArtifact.replace(/ /g, '+')
54
54
  const state = req.query.RelayState || req.query.state
55
55
 
56
- const profile = lookUpByAuthCode(artifact).profile
56
+ const profile = lookUpByAuthCode(artifact, { isStateless }).profile
57
57
  const myinfoVersion = 'v3'
58
58
 
59
59
  const { nric: id } = profile
@@ -24,7 +24,7 @@ const signingPem = fs.readFileSync(
24
24
  path.resolve(__dirname, '../../../static/certs/spcp-key.pem'),
25
25
  )
26
26
 
27
- function config(app, { showLoginPage, serviceProvider }) {
27
+ function config(app, { showLoginPage, serviceProvider, isStateless }) {
28
28
  for (const idp of ['singPass', 'corpPass']) {
29
29
  const profiles = assertions.oidc[idp]
30
30
  const defaultProfile =
@@ -34,7 +34,7 @@ function config(app, { showLoginPage, serviceProvider }) {
34
34
  const { redirect_uri: redirectURI, state, nonce } = req.query
35
35
  if (showLoginPage(req)) {
36
36
  const values = profiles.map((profile) => {
37
- const authCode = generateAuthCode({ profile, nonce })
37
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
38
38
  const assertURL = buildAssertURL(redirectURI, authCode, state)
39
39
  const id = idGenerator[idp](profile)
40
40
  return { id, assertURL }
@@ -53,7 +53,7 @@ function config(app, { showLoginPage, serviceProvider }) {
53
53
  res.send(response)
54
54
  } else {
55
55
  const profile = customProfileFromHeaders[idp](req) || defaultProfile
56
- const authCode = generateAuthCode({ profile, nonce })
56
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
57
57
  const assertURL = buildAssertURL(redirectURI, authCode, state)
58
58
  console.warn(
59
59
  `Redirecting login from ${req.query.client_id} to ${redirectURI}`,
@@ -72,7 +72,7 @@ function config(app, { showLoginPage, serviceProvider }) {
72
72
  profile.uen = uen
73
73
  }
74
74
 
75
- const authCode = generateAuthCode({ profile, nonce })
75
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
76
76
  const assertURL = buildAssertURL(redirectURI, authCode, state)
77
77
  res.redirect(assertURL)
78
78
  })
@@ -88,20 +88,32 @@ function config(app, { showLoginPage, serviceProvider }) {
88
88
  const { refresh_token: suppliedRefreshToken } = req.body
89
89
  console.warn(`Refreshing tokens with ${suppliedRefreshToken}`)
90
90
 
91
- profile = profileStore.get(suppliedRefreshToken)
91
+ profile = isStateless
92
+ ? JSON.parse(
93
+ Buffer.from(suppliedRefreshToken, 'base64url').toString(
94
+ 'utf-8',
95
+ ),
96
+ )
97
+ : profileStore.get(suppliedRefreshToken)
92
98
  } else {
93
99
  const { code: authCode } = req.body
94
100
  console.warn(
95
101
  `Received auth code ${authCode} from ${aud} and ${req.body.redirect_uri}`,
96
102
  )
97
- ;({ profile, nonce } = lookUpByAuthCode(authCode))
103
+ ;({ profile, nonce } = lookUpByAuthCode(authCode, { isStateless }))
98
104
  }
99
105
 
100
106
  const iss = `${req.protocol}://${req.get('host')}`
101
107
 
102
- const { idTokenClaims, accessToken, refreshToken } =
103
- await assertions.oidc.create[idp](profile, iss, aud, nonce)
108
+ const {
109
+ idTokenClaims,
110
+ accessToken,
111
+ refreshToken: generatedRefreshToken,
112
+ } = await assertions.oidc.create[idp](profile, iss, aud, nonce)
104
113
 
114
+ const refreshToken = isStateless
115
+ ? Buffer.from(JSON.stringify(profile)).toString('base64url')
116
+ : generatedRefreshToken
105
117
  profileStore.set(refreshToken, profile)
106
118
 
107
119
  const signingKey = await jose.JWK.asKey(signingPem, 'pem')
@@ -140,7 +140,7 @@ function findEncryptionKey(jwks, algs) {
140
140
  }
141
141
  }
142
142
 
143
- function config(app, { showLoginPage }) {
143
+ function config(app, { showLoginPage, isStateless }) {
144
144
  for (const idp of ['singPass', 'corpPass']) {
145
145
  const profiles = assertions.oidc[idp]
146
146
  const defaultProfile =
@@ -196,7 +196,7 @@ function config(app, { showLoginPage }) {
196
196
  // Identical to OIDC v1
197
197
  if (showLoginPage(req)) {
198
198
  const values = profiles.map((profile) => {
199
- const authCode = generateAuthCode({ profile, nonce })
199
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
200
200
  const assertURL = buildAssertURL(redirectURI, authCode, state)
201
201
  const id = idGenerator[idp](profile)
202
202
  return { id, assertURL }
@@ -215,7 +215,7 @@ function config(app, { showLoginPage }) {
215
215
  res.send(response)
216
216
  } else {
217
217
  const profile = customProfileFromHeaders[idp](req) || defaultProfile
218
- const authCode = generateAuthCode({ profile, nonce })
218
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
219
219
  const assertURL = buildAssertURL(redirectURI, authCode, state)
220
220
  console.warn(
221
221
  `Redirecting login from ${req.query.client_id} to ${redirectURI}`,
@@ -234,7 +234,7 @@ function config(app, { showLoginPage }) {
234
234
  profile.uen = uen
235
235
  }
236
236
 
237
- const authCode = generateAuthCode({ profile, nonce })
237
+ const authCode = generateAuthCode({ profile, nonce }, { isStateless })
238
238
  const assertURL = buildAssertURL(redirectURI, authCode, state)
239
239
  res.redirect(assertURL)
240
240
  })
@@ -434,7 +434,7 @@ function config(app, { showLoginPage }) {
434
434
  }
435
435
 
436
436
  // Step 1: Obtain profile for which the auth code requested data for
437
- const { profile, nonce } = lookUpByAuthCode(authCode)
437
+ const { profile, nonce } = lookUpByAuthCode(authCode, { isStateless })
438
438
 
439
439
  // Step 2: Get ID token
440
440
  const aud = clientAssertionClaims['sub']
@@ -30,7 +30,7 @@ const buildAssertURL = (redirectURI, authCode, state) =>
30
30
  authCode,
31
31
  )}&state=${encodeURIComponent(state)}`
32
32
 
33
- function config(app, { showLoginPage, serviceProvider }) {
33
+ function config(app, { showLoginPage, serviceProvider, isStateless }) {
34
34
  const profiles = assertions.oidc.singPass
35
35
  const defaultProfile =
36
36
  profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0]
@@ -43,7 +43,10 @@ function config(app, { showLoginPage, serviceProvider }) {
43
43
  const values = profiles
44
44
  .filter((profile) => assertions.myinfo.v3.personas[profile.nric])
45
45
  .map((profile) => {
46
- const authCode = generateAuthCode({ profile, scopes, nonce })
46
+ const authCode = generateAuthCode(
47
+ { profile, scopes, nonce },
48
+ { isStateless },
49
+ )
47
50
  const assertURL = buildAssertURL(redirectURI, authCode, state)
48
51
  const id = idGenerator.singPass(profile)
49
52
  return { id, assertURL }
@@ -52,7 +55,10 @@ function config(app, { showLoginPage, serviceProvider }) {
52
55
  res.send(response)
53
56
  } else {
54
57
  const profile = defaultProfile
55
- const authCode = generateAuthCode({ profile, scopes, nonce })
58
+ const authCode = generateAuthCode(
59
+ { profile, scopes, nonce },
60
+ { isStateless },
61
+ )
56
62
  const assertURL = buildAssertURL(redirectURI, authCode, state)
57
63
  console.info(
58
64
  `Redirecting login from ${req.query.client_id} to ${assertURL}`,
@@ -74,7 +80,9 @@ function config(app, { showLoginPage, serviceProvider }) {
74
80
  )
75
81
 
76
82
  try {
77
- const { profile, scopes, nonce } = lookUpByAuthCode(authCode)
83
+ const { profile, scopes, nonce } = lookUpByAuthCode(authCode, {
84
+ isStateless,
85
+ })
78
86
  console.info(
79
87
  `Profile ${JSON.stringify(profile)} with token scope ${scopes}`,
80
88
  )
@@ -120,7 +128,9 @@ function config(app, { showLoginPage, serviceProvider }) {
120
128
  req.headers.authorization || req.headers.Authorization
121
129
  ).replace('Bearer ', '')
122
130
  // eslint-disable-next-line no-unused-vars
123
- const { profile, scopes, unused } = lookUpByAuthCode(authCode)
131
+ const { profile, scopes, unused } = lookUpByAuthCode(authCode, {
132
+ isStateless,
133
+ })
124
134
  const uuid = profile.uuid
125
135
  const nric = assertions.oidc.singPass.find((p) => p.uuid === uuid).nric
126
136
  const persona = assertions.myinfo.v3.personas[nric]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/mockpass",
3
- "version": "4.0.12",
3
+ "version": "4.1.0",
4
4
  "description": "A mock SingPass/CorpPass server for dev purposes",
5
5
  "main": "app.js",
6
6
  "bin": {