@opengovsg/mockpass 3.0.3 → 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 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
- CorpPass:
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 { configOIDC, configMyInfo, configSGID } = require('./lib/express')
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)
@@ -1,5 +1,5 @@
1
1
  module.exports = {
2
- configOIDC: require('./oidc'),
2
+ ...require('./oidc'),
3
3
  configMyInfo: require('./myinfo'),
4
4
  configSGID: require('./sgid'),
5
5
  }
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ configOIDC: require('./spcp'),
3
+ configOIDCv2: require('./v2-ndi'),
4
+ }
@@ -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('../assertions')
9
- const { generateAuthCode, lookUpByAuthCode } = require('../auth-code')
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, '../../static/html/login-page.html'),
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, '../../static/certs/spcp-key.pem'),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/mockpass",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
4
4
  "description": "A mock SingPass/CorpPass server for dev purposes",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "dotenv": "^16.0.0",
42
42
  "expiry-map": "^2.0.0",
43
43
  "express": "^4.16.3",
44
- "jsonwebtoken": "^8.4.0",
44
+ "jsonwebtoken": "^9.0.0",
45
45
  "lodash": "^4.17.11",
46
46
  "morgan": "^1.9.1",
47
47
  "mustache": "^4.2.0",
@@ -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
+ }