@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 +1 -0
- package/app.js +4 -1
- package/lib/auth-code.js +14 -4
- package/lib/express/myinfo/consent.js +2 -2
- package/lib/express/oidc/spcp.js +20 -8
- package/lib/express/oidc/v2-ndi.js +5 -5
- package/lib/express/sgid.js +15 -5
- package/package.json +1 -1
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 = (
|
|
8
|
-
|
|
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
|
|
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
|
package/lib/express/oidc/spcp.js
CHANGED
|
@@ -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 =
|
|
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 {
|
|
103
|
-
|
|
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']
|
package/lib/express/sgid.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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]
|