@opengovsg/mockpass 3.0.4 → 3.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 +19 -2
- package/index.js +7 -1
- package/lib/express/index.js +1 -1
- package/lib/express/oidc/index.js +4 -0
- package/lib/express/{oidc.js → oidc/spcp.js} +9 -39
- package/lib/express/oidc/utils.js +42 -0
- package/lib/express/oidc/v2-ndi.js +332 -0
- package/package.json +1 -1
- package/static/certs/oidc-v2-asp-public.json +12 -0
- package/static/certs/oidc-v2-asp-secret.json +13 -0
- package/static/certs/oidc-v2-rp-public.json +22 -0
- package/static/certs/oidc-v2-rp-secret.json +24 -0
package/README.md
CHANGED
|
@@ -17,14 +17,26 @@ A mock SingPass/CorpPass/MyInfo server for dev purposes
|
|
|
17
17
|
|
|
18
18
|
Configure your application to point to the following endpoints:
|
|
19
19
|
|
|
20
|
-
SingPass:
|
|
20
|
+
SingPass (v1 - Singpass OIDC):
|
|
21
21
|
- http://localhost:5156/singpass/authorize - OIDC login redirect with optional page
|
|
22
22
|
- http://localhost:5156/singpass/token - receives OIDC authorization code and returns id_token
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
SingPass (v2 - NDI OIDC):
|
|
25
|
+
- http://localhost:5156/singpass/v2/authorize - OIDC login redirect with optional page
|
|
26
|
+
- http://localhost:5156/singpass/v2/token - receives OIDC authorization code and returns id_token
|
|
27
|
+
- http://localhost:5156/singpass/v2/.well-known/openid-configuration - OpenID discovery endpoint
|
|
28
|
+
- http://localhost:5156/singpass/v2/.well-known/keys - JWKS endpoint which exposes the auth provider's signing keys
|
|
29
|
+
|
|
30
|
+
CorpPass (v1 - Corppass OIDC):
|
|
25
31
|
- http://localhost:5156/corppass/authorize - OIDC login redirect with optional page
|
|
26
32
|
- http://localhost:5156/corppass/token - receives OIDC authorization code and returns id_token
|
|
27
33
|
|
|
34
|
+
CorpPass (v2 - Corppass OIDC):
|
|
35
|
+
- http://localhost:5156/corppass/v2/authorize - OIDC login redirect with optional page
|
|
36
|
+
- http://localhost:5156/corppass/v2/token - receives OIDC authorization code and returns id_token
|
|
37
|
+
- http://localhost:5156/corppass/v2/.well-known/openid-configuration - OpenID discovery endpoint
|
|
38
|
+
- http://localhost:5156/corppass/v2/.well-known/keys - JWKS endpoint which exposes the auth provider's signing keys
|
|
39
|
+
|
|
28
40
|
MyInfo:
|
|
29
41
|
- http://localhost:5156/myinfo/v3/person-basic (exclusive to government systems)
|
|
30
42
|
- http://localhost:5156/myinfo/v3/authorise
|
|
@@ -42,6 +54,11 @@ and with application certs at `static/certs/{key.pem|server.crt}`
|
|
|
42
54
|
Alternatively, provide the paths to your app cert as env vars
|
|
43
55
|
`SERVICE_PROVIDER_CERT_PATH` and `SERVICE_PROVIDER_PUB_KEY`
|
|
44
56
|
|
|
57
|
+
If you are integrating with Singpass NDI OIDC and/or Corppass v2 OIDC, you should
|
|
58
|
+
provide your well-known key endpoints as env vars `SP_RP_JWKS_ENDPOINT` and/or
|
|
59
|
+
`CP_RP_JWKS_ENDPOINT` respectively. Alternatively, provide your application with
|
|
60
|
+
the `oidc-v2-rp-*.json` JWKS.
|
|
61
|
+
|
|
45
62
|
```
|
|
46
63
|
$ npm install @opengovsg/mockpass
|
|
47
64
|
|
package/index.js
CHANGED
|
@@ -5,7 +5,12 @@ const morgan = require('morgan')
|
|
|
5
5
|
const path = require('path')
|
|
6
6
|
require('dotenv').config()
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
configOIDC,
|
|
10
|
+
configOIDCv2,
|
|
11
|
+
configMyInfo,
|
|
12
|
+
configSGID,
|
|
13
|
+
} = require('./lib/express')
|
|
9
14
|
|
|
10
15
|
const PORT = process.env.MOCKPASS_PORT || process.env.PORT || 5156
|
|
11
16
|
|
|
@@ -44,6 +49,7 @@ const app = express()
|
|
|
44
49
|
app.use(morgan('combined'))
|
|
45
50
|
|
|
46
51
|
configOIDC(app, options)
|
|
52
|
+
configOIDCv2(app, options)
|
|
47
53
|
configSGID(app, options)
|
|
48
54
|
|
|
49
55
|
configMyInfo.consent(app)
|
package/lib/express/index.js
CHANGED
|
@@ -5,55 +5,25 @@ const jose = require('node-jose')
|
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const ExpiryMap = require('expiry-map')
|
|
7
7
|
|
|
8
|
-
const assertions = require('
|
|
9
|
-
const { generateAuthCode, lookUpByAuthCode } = require('
|
|
8
|
+
const assertions = require('../../assertions')
|
|
9
|
+
const { generateAuthCode, lookUpByAuthCode } = require('../../auth-code')
|
|
10
|
+
const {
|
|
11
|
+
buildAssertURL,
|
|
12
|
+
idGenerator,
|
|
13
|
+
customProfileFromHeaders,
|
|
14
|
+
} = require('./utils')
|
|
10
15
|
|
|
11
16
|
const LOGIN_TEMPLATE = fs.readFileSync(
|
|
12
|
-
path.resolve(__dirname, '
|
|
17
|
+
path.resolve(__dirname, '../../../static/html/login-page.html'),
|
|
13
18
|
'utf8',
|
|
14
19
|
)
|
|
15
20
|
const REFRESH_TOKEN_TIMEOUT = 24 * 60 * 60 * 1000
|
|
16
21
|
const profileStore = new ExpiryMap(REFRESH_TOKEN_TIMEOUT)
|
|
17
22
|
|
|
18
23
|
const signingPem = fs.readFileSync(
|
|
19
|
-
path.resolve(__dirname, '
|
|
24
|
+
path.resolve(__dirname, '../../../static/certs/spcp-key.pem'),
|
|
20
25
|
)
|
|
21
26
|
|
|
22
|
-
const buildAssertURL = (redirectURI, authCode, state) =>
|
|
23
|
-
`${redirectURI}?code=${encodeURIComponent(
|
|
24
|
-
authCode,
|
|
25
|
-
)}&state=${encodeURIComponent(state)}`
|
|
26
|
-
|
|
27
|
-
const idGenerator = {
|
|
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
|
-
},
|
|
55
|
-
}
|
|
56
|
-
|
|
57
27
|
function config(app, { showLoginPage, serviceProvider }) {
|
|
58
28
|
for (const idp of ['singPass', 'corpPass']) {
|
|
59
29
|
const profiles = assertions.oidc[idp]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const assertions = require('../../assertions')
|
|
2
|
+
|
|
3
|
+
const buildAssertURL = (redirectURI, authCode, state) =>
|
|
4
|
+
`${redirectURI}?code=${encodeURIComponent(
|
|
5
|
+
authCode,
|
|
6
|
+
)}&state=${encodeURIComponent(state)}`
|
|
7
|
+
|
|
8
|
+
const idGenerator = {
|
|
9
|
+
singPass: ({ nric }) =>
|
|
10
|
+
assertions.myinfo.v3.personas[nric] ? `${nric} [MyInfo]` : nric,
|
|
11
|
+
corpPass: ({ nric, uen }) => `${nric} / UEN: ${uen}`,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const customProfileFromHeaders = {
|
|
15
|
+
singPass: (req) => {
|
|
16
|
+
const customNricHeader = req.header('X-Custom-NRIC')
|
|
17
|
+
const customUuidHeader = req.header('X-Custom-UUID')
|
|
18
|
+
if (!customNricHeader || !customUuidHeader) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
return { nric: customNricHeader, uuid: customUuidHeader }
|
|
22
|
+
},
|
|
23
|
+
corpPass: (req) => {
|
|
24
|
+
const customNricHeader = req.header('X-Custom-NRIC')
|
|
25
|
+
const customUuidHeader = req.header('X-Custom-UUID')
|
|
26
|
+
const customUenHeader = req.header('X-Custom-UEN')
|
|
27
|
+
if (!customNricHeader || !customUuidHeader || !customUenHeader) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
nric: customNricHeader,
|
|
32
|
+
uuid: customUuidHeader,
|
|
33
|
+
uen: customUenHeader,
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
buildAssertURL,
|
|
40
|
+
idGenerator,
|
|
41
|
+
customProfileFromHeaders,
|
|
42
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// This file implements NDI OIDC for Singpass authentication and Corppass OIDC
|
|
2
|
+
// for Corppass authentication.
|
|
3
|
+
|
|
4
|
+
const express = require('express')
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const { render } = require('mustache')
|
|
7
|
+
const jose = require('node-jose')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
const assertions = require('../../assertions')
|
|
11
|
+
const { generateAuthCode, lookUpByAuthCode } = require('../../auth-code')
|
|
12
|
+
const {
|
|
13
|
+
buildAssertURL,
|
|
14
|
+
idGenerator,
|
|
15
|
+
customProfileFromHeaders,
|
|
16
|
+
} = require('./utils')
|
|
17
|
+
|
|
18
|
+
const LOGIN_TEMPLATE = fs.readFileSync(
|
|
19
|
+
path.resolve(__dirname, '../../../static/html/login-page.html'),
|
|
20
|
+
'utf8',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const aspPublic = fs.readFileSync(
|
|
24
|
+
path.resolve(__dirname, '../../../static/certs/oidc-v2-asp-public.json'),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const aspSecret = fs.readFileSync(
|
|
28
|
+
path.resolve(__dirname, '../../../static/certs/oidc-v2-asp-secret.json'),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const rpPublic = fs.readFileSync(
|
|
32
|
+
path.resolve(__dirname, '../../../static/certs/oidc-v2-rp-public.json'),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
function config(app, { showLoginPage }) {
|
|
36
|
+
for (const idp of ['singPass', 'corpPass']) {
|
|
37
|
+
const profiles = assertions.oidc[idp]
|
|
38
|
+
const defaultProfile =
|
|
39
|
+
profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0]
|
|
40
|
+
|
|
41
|
+
app.get(`/${idp.toLowerCase()}/v2/authorize`, (req, res) => {
|
|
42
|
+
const {
|
|
43
|
+
scope,
|
|
44
|
+
response_type,
|
|
45
|
+
client_id,
|
|
46
|
+
redirect_uri: redirectURI,
|
|
47
|
+
state,
|
|
48
|
+
nonce,
|
|
49
|
+
} = req.query
|
|
50
|
+
|
|
51
|
+
if (scope !== 'openid') {
|
|
52
|
+
return res.status(400).send(`Unknown scope ${scope}`)
|
|
53
|
+
}
|
|
54
|
+
if (response_type !== 'code') {
|
|
55
|
+
return res.status(400).send(`Unknown response_type ${response_type}`)
|
|
56
|
+
}
|
|
57
|
+
if (!client_id) {
|
|
58
|
+
return res.status(400).send('Missing client_id')
|
|
59
|
+
}
|
|
60
|
+
if (!redirectURI) {
|
|
61
|
+
return res.status(400).send('Missing redirect_uri')
|
|
62
|
+
}
|
|
63
|
+
if (!nonce) {
|
|
64
|
+
return res.status(400).send('Missing nonce')
|
|
65
|
+
}
|
|
66
|
+
if (!state) {
|
|
67
|
+
return res.status(400).send('Missing state')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Identical to OIDC v1
|
|
71
|
+
if (showLoginPage(req)) {
|
|
72
|
+
const values = profiles.map((profile) => {
|
|
73
|
+
const authCode = generateAuthCode({ profile, nonce })
|
|
74
|
+
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
75
|
+
const id = idGenerator[idp](profile)
|
|
76
|
+
return { id, assertURL }
|
|
77
|
+
})
|
|
78
|
+
const response = render(LOGIN_TEMPLATE, {
|
|
79
|
+
values,
|
|
80
|
+
customProfileConfig: {
|
|
81
|
+
endpoint: `/${idp.toLowerCase()}/v2/authorize/custom-profile`,
|
|
82
|
+
showUuid: true,
|
|
83
|
+
showUen: idp === 'corpPass',
|
|
84
|
+
redirectURI,
|
|
85
|
+
state,
|
|
86
|
+
nonce,
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
res.send(response)
|
|
90
|
+
} else {
|
|
91
|
+
const profile = customProfileFromHeaders[idp](req) || defaultProfile
|
|
92
|
+
const authCode = generateAuthCode({ profile, nonce })
|
|
93
|
+
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
94
|
+
console.warn(
|
|
95
|
+
`Redirecting login from ${req.query.client_id} to ${redirectURI}`,
|
|
96
|
+
)
|
|
97
|
+
res.redirect(assertURL)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
app.get(`/${idp.toLowerCase()}/v2/authorize/custom-profile`, (req, res) => {
|
|
102
|
+
const { nric, uuid, uen, redirectURI, state, nonce } = req.query
|
|
103
|
+
|
|
104
|
+
const profile = { nric, uuid }
|
|
105
|
+
if (idp === 'corpPass') {
|
|
106
|
+
profile.name = `Name of ${nric}`
|
|
107
|
+
profile.isSingPassHolder = false
|
|
108
|
+
profile.uen = uen
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const authCode = generateAuthCode({ profile, nonce })
|
|
112
|
+
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
113
|
+
res.redirect(assertURL)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
app.post(
|
|
117
|
+
`/${idp.toLowerCase()}/v2/token`,
|
|
118
|
+
express.urlencoded({ extended: false }),
|
|
119
|
+
async (req, res) => {
|
|
120
|
+
const {
|
|
121
|
+
client_id,
|
|
122
|
+
redirect_uri: redirectURI,
|
|
123
|
+
grant_type,
|
|
124
|
+
code: authCode,
|
|
125
|
+
client_assertion_type,
|
|
126
|
+
client_assertion: clientAssertion,
|
|
127
|
+
} = req.body
|
|
128
|
+
|
|
129
|
+
// Only SP requires client_id
|
|
130
|
+
if (idp === 'singPass' && !client_id) {
|
|
131
|
+
return res.status(400).send('Missing client_id')
|
|
132
|
+
}
|
|
133
|
+
if (!redirectURI) {
|
|
134
|
+
return res.status(400).send('Missing redirect_uri')
|
|
135
|
+
}
|
|
136
|
+
if (grant_type !== 'authorization_code') {
|
|
137
|
+
return res.status(400).send(`Unknown grant_type ${grant_type}`)
|
|
138
|
+
}
|
|
139
|
+
if (!authCode) {
|
|
140
|
+
return res.status(400).send('Missing code')
|
|
141
|
+
}
|
|
142
|
+
if (
|
|
143
|
+
client_assertion_type !==
|
|
144
|
+
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
|
|
145
|
+
) {
|
|
146
|
+
return res
|
|
147
|
+
.status(400)
|
|
148
|
+
.send(`Unknown client_assertion_type ${client_assertion_type}`)
|
|
149
|
+
}
|
|
150
|
+
if (!clientAssertion) {
|
|
151
|
+
return res.status(400).send('Missing client_assertion')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Step 0: Get the RP keyset
|
|
155
|
+
const rpJwksEndpoint =
|
|
156
|
+
idp === 'singPass'
|
|
157
|
+
? process.env.SP_RP_JWKS_ENDPOINT
|
|
158
|
+
: process.env.CP_RP_JWKS_ENDPOINT
|
|
159
|
+
|
|
160
|
+
let rpKeysetString
|
|
161
|
+
|
|
162
|
+
if (rpJwksEndpoint) {
|
|
163
|
+
try {
|
|
164
|
+
rpKeysetString = await fetch(rpJwksEndpoint, {
|
|
165
|
+
method: 'GET',
|
|
166
|
+
}).then((response) => response.text())
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return res
|
|
169
|
+
.status(400)
|
|
170
|
+
.send(
|
|
171
|
+
`Failed to fetch RP JWKS from specified endpoint: ${e.message}`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// If the endpoint is not defined, default to the sample keyset we provided.
|
|
176
|
+
rpKeysetString = rpPublic
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let rpKeysetJson
|
|
180
|
+
try {
|
|
181
|
+
rpKeysetJson = JSON.parse(rpKeysetString)
|
|
182
|
+
} catch (e) {
|
|
183
|
+
return res.status(400).send(`Unable to parse RP keyset: ${e.message}`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const rpKeyset = await jose.JWK.asKeyStore(rpKeysetJson)
|
|
187
|
+
|
|
188
|
+
// Step 0.5: Verify client assertion with RP signing key
|
|
189
|
+
let clientAssertionVerified
|
|
190
|
+
try {
|
|
191
|
+
clientAssertionVerified = await jose.JWS.createVerify(
|
|
192
|
+
rpKeyset,
|
|
193
|
+
).verify(clientAssertion)
|
|
194
|
+
} catch (e) {
|
|
195
|
+
return res
|
|
196
|
+
.status(400)
|
|
197
|
+
.send(`Unable to verify client_assertion: ${e.message}`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let clientAssertionClaims
|
|
201
|
+
try {
|
|
202
|
+
clientAssertionClaims = JSON.parse(clientAssertionVerified.payload)
|
|
203
|
+
} catch (e) {
|
|
204
|
+
return res
|
|
205
|
+
.status(400)
|
|
206
|
+
.send(`Unable to parse client_assertion: ${e.message}`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (idp === 'singPass') {
|
|
210
|
+
if (clientAssertionClaims['sub'] !== client_id) {
|
|
211
|
+
return res
|
|
212
|
+
.status(400)
|
|
213
|
+
.send('Incorrect sub in client_assertion claims')
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// Since client_id is not given for corpPass, sub claim is required in
|
|
217
|
+
// order to get aud for id_token.
|
|
218
|
+
if (!clientAssertionClaims['sub']) {
|
|
219
|
+
return res
|
|
220
|
+
.status(400)
|
|
221
|
+
.send('Missing sub in client_assertion claims')
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// According to OIDC spec, asp must check the aud claim.
|
|
226
|
+
const iss = `${req.protocol}://${req.get(
|
|
227
|
+
'host',
|
|
228
|
+
)}/${idp.toLowerCase()}/v2`
|
|
229
|
+
|
|
230
|
+
if (clientAssertionClaims['aud'] !== iss) {
|
|
231
|
+
return res
|
|
232
|
+
.status(400)
|
|
233
|
+
.send('Incorrect aud in client_assertion claims')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Step 1: Obtain profile for which the auth code requested data for
|
|
237
|
+
const { profile, nonce } = lookUpByAuthCode(authCode)
|
|
238
|
+
|
|
239
|
+
// Step 2: Get ID token
|
|
240
|
+
const aud = clientAssertionClaims['sub']
|
|
241
|
+
console.warn(
|
|
242
|
+
`Received auth code ${authCode} from ${aud} and ${redirectURI}`,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const { idTokenClaims, accessToken } = await assertions.oidc.create[
|
|
246
|
+
idp
|
|
247
|
+
](profile, iss, aud, nonce)
|
|
248
|
+
|
|
249
|
+
// Step 3: Sign ID token with ASP signing key
|
|
250
|
+
const signingKey = await jose.JWK.asKeyStore(
|
|
251
|
+
JSON.parse(aspSecret),
|
|
252
|
+
).then((keystore) => keystore.get({ use: 'sig' }))
|
|
253
|
+
|
|
254
|
+
const signedIdToken = await jose.JWS.createSign(
|
|
255
|
+
{ format: 'compact' },
|
|
256
|
+
signingKey,
|
|
257
|
+
)
|
|
258
|
+
.update(JSON.stringify(idTokenClaims))
|
|
259
|
+
.final()
|
|
260
|
+
|
|
261
|
+
// Step 4: Encrypt ID token with RP encryption key
|
|
262
|
+
// We're using the first encryption key we find, although NDI actually
|
|
263
|
+
// has its own selection criteria.
|
|
264
|
+
const encryptionKey = rpKeyset.get({ use: 'enc' })
|
|
265
|
+
|
|
266
|
+
const idToken = await jose.JWE.createEncrypt(
|
|
267
|
+
{ format: 'compact', fields: { cty: 'JWT' } },
|
|
268
|
+
encryptionKey,
|
|
269
|
+
)
|
|
270
|
+
.update(signedIdToken)
|
|
271
|
+
.final()
|
|
272
|
+
|
|
273
|
+
// Step 5: Send token
|
|
274
|
+
res.status(200).send({
|
|
275
|
+
access_token: accessToken,
|
|
276
|
+
token_type: 'Bearer',
|
|
277
|
+
id_token: idToken,
|
|
278
|
+
...(idp === 'corpPass'
|
|
279
|
+
? { scope: 'openid', expires_in: 10 * 60 }
|
|
280
|
+
: {}),
|
|
281
|
+
})
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
app.get(
|
|
286
|
+
`/${idp.toLowerCase()}/v2/.well-known/openid-configuration`,
|
|
287
|
+
(req, res) => {
|
|
288
|
+
const baseUrl = `${req.protocol}://${req.get(
|
|
289
|
+
'host',
|
|
290
|
+
)}/${idp.toLowerCase()}/v2`
|
|
291
|
+
|
|
292
|
+
// Note: does not support backchannel auth
|
|
293
|
+
const data = {
|
|
294
|
+
issuer: baseUrl,
|
|
295
|
+
authorization_endpoint: `${baseUrl}/authorize`,
|
|
296
|
+
jwks_uri: `${baseUrl}/.well-known/keys`,
|
|
297
|
+
response_types_supported: ['code'],
|
|
298
|
+
scopes_supported: ['openid'],
|
|
299
|
+
subject_types_supported: ['public'],
|
|
300
|
+
claims_supported: ['nonce', 'aud', 'iss', 'sub', 'exp', 'iat'],
|
|
301
|
+
grant_types_supported: ['authorization_code'],
|
|
302
|
+
token_endpoint: `${baseUrl}/token`,
|
|
303
|
+
token_endpoint_auth_methods_supported: ['private_key_jwt'],
|
|
304
|
+
token_endpoint_auth_signing_alg_values_supported: ['ES512'], // omits ES256 and ES384 (allowed in SP)
|
|
305
|
+
id_token_signing_alg_values_supported: ['ES256'],
|
|
306
|
+
id_token_encryption_alg_values_supported: ['ECDH-ES+A256KW'], // omits ECDH-ES+A192KW, ECDH-ES+A128KW and RSA-OAEP-256 (allowed in SP)
|
|
307
|
+
id_token_encryption_enc_values_supported: ['A256CBC-HS512'],
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (idp === 'corpPass') {
|
|
311
|
+
data['claims_supported'].push([
|
|
312
|
+
'userInfo',
|
|
313
|
+
'entityInfo',
|
|
314
|
+
'rt_hash',
|
|
315
|
+
'at_hash',
|
|
316
|
+
'amr',
|
|
317
|
+
])
|
|
318
|
+
// Omit authorization-info_endpoint for CP
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
res.status(200).send(data)
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
app.get(`/${idp.toLowerCase()}/v2/.well-known/keys`, (req, res) => {
|
|
326
|
+
res.status(200).send(JSON.parse(aspPublic))
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
return app
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = config
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"keys": [
|
|
3
|
+
{
|
|
4
|
+
"kty": "EC",
|
|
5
|
+
"use": "sig",
|
|
6
|
+
"crv": "P-521",
|
|
7
|
+
"kid": "sig-1655709297",
|
|
8
|
+
"x": "AWuSHLkeP89DOkPaTs6MUDTFX1oL_Nr2rsJxCUyWL9x4LDEwtGXxWmw5-KhJSKauwJL2fAiNribZa2E0EZ-A4DzL",
|
|
9
|
+
"y": "AHoghl5OGyp7Vejt2sqYW7z2G_gTGBDR9q-ylLjnERpKd7-kHybLEutkwp5tmkhhlOysCcXE7vpTcnwxeQPa3zN0"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"keys": [
|
|
3
|
+
{
|
|
4
|
+
"kty": "EC",
|
|
5
|
+
"d": "ATdzXBC0WOU74xmdFfeVWfm2ybggXGeWGCMpYlpqzhW5cdrTrlj7UbTmKYlPJe70F5UD-wG2TK6tUoNiVpfEKmky",
|
|
6
|
+
"use": "sig",
|
|
7
|
+
"crv": "P-521",
|
|
8
|
+
"kid": "sig-1655709297",
|
|
9
|
+
"x": "AWuSHLkeP89DOkPaTs6MUDTFX1oL_Nr2rsJxCUyWL9x4LDEwtGXxWmw5-KhJSKauwJL2fAiNribZa2E0EZ-A4DzL",
|
|
10
|
+
"y": "AHoghl5OGyp7Vejt2sqYW7z2G_gTGBDR9q-ylLjnERpKd7-kHybLEutkwp5tmkhhlOysCcXE7vpTcnwxeQPa3zN0"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"keys": [
|
|
3
|
+
{
|
|
4
|
+
"kty": "EC",
|
|
5
|
+
"use": "sig",
|
|
6
|
+
"crv": "P-521",
|
|
7
|
+
"kid": "sig-2022-06-04T09:22:28Z",
|
|
8
|
+
"x": "AAj_CAKL9NmP6agPCMto6_LiYQqko3o3ZWTtBg75bA__Z8yKEv_CwHzaibkVLnJ9XKWxCQeyEk9ROLhJoJuZxnsI",
|
|
9
|
+
"y": "AZeoe0v-EwqD3oo1V5lxUAmC80qHt-ybqOsl1mYKPgE_ctGcD4hj8tVhmD0Of6ARuKVTxNWej-X82hEW_7Aa-XpR",
|
|
10
|
+
"alg": "ES512"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"kty": "EC",
|
|
14
|
+
"use": "enc",
|
|
15
|
+
"crv": "P-521",
|
|
16
|
+
"kid": "enc-2022-06-04T13:46:15Z",
|
|
17
|
+
"x": "AB-16HyJwnlSZbQtqhFskADqFrm6rgX9XeaV8FgynX61750GCRbYjoueDosSNt-qzK5QNHskdQw0QZ700YF2JIlb",
|
|
18
|
+
"y": "AZwYlSBSdV-CxGRMz6ovTvWxKJ6e44gaZHf-YfbJV7w9VdAJb3OuzbHNGRuzNDjEa8eH-paLDaAB84ezrEm1SRHq",
|
|
19
|
+
"alg": "ECDH-ES+A256KW"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"keys": [
|
|
3
|
+
{
|
|
4
|
+
"kty": "EC",
|
|
5
|
+
"d": "AFOzlND2sq43ykty-VZXw-IEIOyHkBsNXUU77o5yEYcktpoMe9Dl3jsaXwzRK6wtDJH_uoz4IG1Uj4J_WyH5O3GS",
|
|
6
|
+
"use": "sig",
|
|
7
|
+
"crv": "P-521",
|
|
8
|
+
"kid": "sig-2022-06-04T09:22:28Z",
|
|
9
|
+
"x": "AAj_CAKL9NmP6agPCMto6_LiYQqko3o3ZWTtBg75bA__Z8yKEv_CwHzaibkVLnJ9XKWxCQeyEk9ROLhJoJuZxnsI",
|
|
10
|
+
"y": "AZeoe0v-EwqD3oo1V5lxUAmC80qHt-ybqOsl1mYKPgE_ctGcD4hj8tVhmD0Of6ARuKVTxNWej-X82hEW_7Aa-XpR",
|
|
11
|
+
"alg": "ES512"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"kty": "EC",
|
|
15
|
+
"d": "AP7xECOnlKW-FuLpe1h3ULZoqFzScFrbyAEQTFFG49j5HRHl0k13-6_6nWnwJ9Y8sTrGOWH4GszmDBBZGGvESJQr",
|
|
16
|
+
"use": "enc",
|
|
17
|
+
"crv": "P-521",
|
|
18
|
+
"kid": "enc-2022-06-04T13:46:15Z",
|
|
19
|
+
"x": "AB-16HyJwnlSZbQtqhFskADqFrm6rgX9XeaV8FgynX61750GCRbYjoueDosSNt-qzK5QNHskdQw0QZ700YF2JIlb",
|
|
20
|
+
"y": "AZwYlSBSdV-CxGRMz6ovTvWxKJ6e44gaZHf-YfbJV7w9VdAJb3OuzbHNGRuzNDjEa8eH-paLDaAB84ezrEm1SRHq",
|
|
21
|
+
"alg": "ECDH-ES+A256KW"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|