@andrewheberle/serverless-ssh-ca 0.23.0 → 0.24.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 +119 -0
- package/dist/api/operations.d.ts +2 -1
- package/dist/api/operations.d.ts.map +1 -1
- package/dist/api/operations.js +103 -14
- package/dist/api/operations.js.map +1 -1
- package/dist/api/schema.d.ts +1 -0
- package/dist/api/schema.d.ts.map +1 -1
- package/dist/api/schema.js +8 -1
- package/dist/api/schema.js.map +1 -1
- package/dist/api/v3/index.d.ts.map +1 -1
- package/dist/api/v3/index.js +1 -1
- package/dist/api/v3/index.js.map +1 -1
- package/dist/api/v3/schema.d.ts.map +1 -1
- package/dist/api/v3/schema.js +11 -7
- package/dist/api/v3/schema.js.map +1 -1
- package/dist/api/v4/index.d.ts.map +1 -1
- package/dist/api/v4/index.js +1 -1
- package/dist/api/v4/index.js.map +1 -1
- package/dist/api/v4/schema.d.ts.map +1 -1
- package/dist/api/v4/schema.js +11 -7
- package/dist/api/v4/schema.js.map +1 -1
- package/dist/db/index.d.ts +51 -4
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +133 -33
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations/0003_krl_revision.d.ts +3 -0
- package/dist/db/migrations/0003_krl_revision.d.ts.map +1 -0
- package/dist/db/migrations/0003_krl_revision.js +14 -0
- package/dist/db/migrations/0003_krl_revision.js.map +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +2 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/types.d.ts +2 -0
- package/dist/db/types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/krl.d.ts +2 -1
- package/dist/krl.d.ts.map +1 -1
- package/dist/krl.js +3 -2
- package/dist/krl.js.map +1 -1
- package/dist/node/cache.d.ts +21 -0
- package/dist/node/cache.d.ts.map +1 -0
- package/dist/node/cache.js +49 -0
- package/dist/node/cache.js.map +1 -0
- package/dist/node/env.d.ts +6 -3
- package/dist/node/env.d.ts.map +1 -1
- package/dist/node/env.js +4 -3
- package/dist/node/env.js.map +1 -1
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -0
- package/dist/node/index.js.map +1 -1
- package/dist/router.d.ts +25 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +38 -1
- package/dist/router.js.map +1 -1
- package/dist/ssh/keys.d.ts +12 -0
- package/dist/ssh/keys.d.ts.map +1 -1
- package/dist/ssh/keys.js +28 -0
- package/dist/ssh/keys.js.map +1 -1
- package/dist/types.d.ts +53 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +68 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +166 -13
- package/dist/utils.js.map +1 -1
- package/dist/verify.d.ts +17 -5
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +47 -9
- package/dist/verify.js.map +1 -1
- package/openapi.json +402 -2
- package/package.json +1 -1
- package/src/api/operations.ts +115 -12
- package/src/api/schema.ts +9 -0
- package/src/api/v3/index.ts +1 -0
- package/src/api/v3/schema.ts +11 -4
- package/src/api/v4/index.ts +1 -0
- package/src/api/v4/schema.ts +11 -5
- package/src/db/index.ts +162 -37
- package/src/db/migrations/0003_krl_revision.ts +15 -0
- package/src/db/migrations/index.ts +2 -0
- package/src/db/types.ts +2 -0
- package/src/index.ts +6 -4
- package/src/krl.ts +3 -2
- package/src/node/cache.ts +62 -0
- package/src/node/env.ts +6 -5
- package/src/node/index.ts +1 -0
- package/src/router.ts +64 -2
- package/src/ssh/keys.ts +33 -0
- package/src/types.ts +49 -3
- package/src/utils.ts +188 -14
- package/src/verify.ts +54 -14
package/src/utils.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { SshCaBindings } from "./types.js"
|
|
2
2
|
import { JWKInvalid, JWKSInvalid, JWSInvalid, JWSSignatureVerificationFailed, JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "jose/errors"
|
|
3
3
|
import { CertificateParseError, SshCertificate } from "./ssh/certificate.js"
|
|
4
|
-
import { KeyParseError, parsePrivateKey, PublicKey, UnsupportedKeyError, type PrivateKey } from "./ssh/keys.js"
|
|
4
|
+
import { KeyParseError, MinimumRsaKeyBits, parsePrivateKey, PublicKey, rsaKeyBits, UnsupportedKeyError, type PrivateKey } from "./ssh/keys.js"
|
|
5
5
|
import z from "zod"
|
|
6
|
-
import { verifyJWT } from "./verify.js"
|
|
6
|
+
import { MissingAudienceError, verifyJWT } from "./verify.js"
|
|
7
7
|
import { CertificateRequestJWTPayload } from "./types.js"
|
|
8
8
|
import { RenewalProofOfPossession, ProofOfPossession, PossessionParseError } from "./proof.js"
|
|
9
9
|
import type { isRevoked as IsRevokedFn } from "./db/index.js"
|
|
@@ -56,6 +56,74 @@ type AccessToken = {
|
|
|
56
56
|
sub: string
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Thrown when a claim of a verified token is missing or is not of the
|
|
61
|
+
* expected type.
|
|
62
|
+
*/
|
|
63
|
+
export class TokenClaimError extends Error {
|
|
64
|
+
constructor(message: string) {
|
|
65
|
+
super(message)
|
|
66
|
+
this.name = "TokenClaimError"
|
|
67
|
+
|
|
68
|
+
Object.setPrototypeOf(this, TokenClaimError.prototype)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SubjectClaimSchema = z.string().min(1)
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns the subject of a verified token, which is compared between the
|
|
76
|
+
* access and identity tokens so must always be present.
|
|
77
|
+
*
|
|
78
|
+
* @param payload The payload of a verified token
|
|
79
|
+
* @returns The `sub` claim of the token
|
|
80
|
+
* @throws {@link TokenClaimError} when the `sub` claim is missing, empty or not a string
|
|
81
|
+
*/
|
|
82
|
+
export const tokenSubject = (payload: { sub?: unknown }): string => {
|
|
83
|
+
const parsed = SubjectClaimSchema.safeParse(payload.sub)
|
|
84
|
+
if (!parsed.success) {
|
|
85
|
+
throw new TokenClaimError("JWT was verified but was missing required sub claim")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return parsed.data
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The claims of an access token that identify the user's email address.
|
|
93
|
+
*
|
|
94
|
+
* `email_verified` is optional as not every IdP includes it, but when it is
|
|
95
|
+
* included it must be true, as an IdP may otherwise issue a token for an
|
|
96
|
+
* address the user has not shown they control.
|
|
97
|
+
*/
|
|
98
|
+
const EmailClaimsSchema = z.object({
|
|
99
|
+
email: z.email(),
|
|
100
|
+
email_verified: z.boolean().optional(),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns the email address of a verified access token, or why it cannot be
|
|
105
|
+
* used.
|
|
106
|
+
*
|
|
107
|
+
* @param payload The payload of a verified access token
|
|
108
|
+
* @returns The email address, or an error message if the claims are not acceptable
|
|
109
|
+
*/
|
|
110
|
+
export const tokenEmail = (payload: Record<string, unknown>): { ok: true, email: string } | { ok: false, error: string } => {
|
|
111
|
+
if (payload.email === undefined) {
|
|
112
|
+
return { ok: false, error: "JWT was verified but was missing required email claim" }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parsed = EmailClaimsSchema.safeParse(payload)
|
|
116
|
+
if (!parsed.success) {
|
|
117
|
+
return { ok: false, error: "JWT was verified but the email or email_verified claim was not valid" }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (parsed.data.email_verified === false) {
|
|
121
|
+
return { ok: false, error: "JWT was verified but the email address has not been verified by the IdP" }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { ok: true, email: parsed.data.email }
|
|
125
|
+
}
|
|
126
|
+
|
|
59
127
|
export const transformAuthorizationHeader = async (env: SshCaBindings, val: string, ctx: z.RefinementCtx): Promise<AccessToken | never> => {
|
|
60
128
|
if (!val.startsWith("Bearer ") || val.length <= 7) {
|
|
61
129
|
return fatalIssue(ctx, "authorization header must be a Bearer token", val)
|
|
@@ -69,19 +137,28 @@ export const transformAuthorizationHeader = async (env: SshCaBindings, val: stri
|
|
|
69
137
|
}
|
|
70
138
|
|
|
71
139
|
try {
|
|
72
|
-
// verify jwt
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
140
|
+
// verify jwt, which must be issued for this CA
|
|
141
|
+
const aud = split(env.JWT_ACCESS_TOKEN_AUD).length > 0 ? env.JWT_ACCESS_TOKEN_AUD : env.JWT_AUD
|
|
142
|
+
const { payload } = await verifyJWT(env, jwt, aud)
|
|
143
|
+
|
|
144
|
+
const email = tokenEmail(payload)
|
|
145
|
+
if (!email.ok) {
|
|
146
|
+
l.warn(email.error, "in", "transformAuthorizationHeader")
|
|
147
|
+
return fatalIssue(ctx, email.error, val)
|
|
77
148
|
}
|
|
78
149
|
|
|
79
150
|
return {
|
|
80
|
-
email:
|
|
81
|
-
sub: payload
|
|
151
|
+
email: email.email,
|
|
152
|
+
sub: tokenSubject(payload),
|
|
82
153
|
}
|
|
83
154
|
} catch (err) {
|
|
84
155
|
switch (true) {
|
|
156
|
+
case (err instanceof MissingAudienceError):
|
|
157
|
+
// a misconfigured CA is a server error, not a validation failure
|
|
158
|
+
l.error(err.message, "in", "transformAuthorizationHeader")
|
|
159
|
+
throw new InternalServerErrorException(err.message)
|
|
160
|
+
case (err instanceof TokenClaimError):
|
|
161
|
+
return fatalIssue(ctx, err.message, val)
|
|
85
162
|
case (err instanceof JWSInvalid):
|
|
86
163
|
return fatalIssue(ctx, "the access token was invalid", val)
|
|
87
164
|
case (err instanceof JWTInvalid):
|
|
@@ -145,6 +222,36 @@ export const transformPublicKey = (val: string | Buffer<ArrayBufferLike>, ctx: z
|
|
|
145
222
|
}
|
|
146
223
|
}
|
|
147
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Returns why a public key cannot be issued a certificate, or undefined if it
|
|
227
|
+
* can be.
|
|
228
|
+
*
|
|
229
|
+
* RSA keys must be at least {@link MinimumRsaKeyBits} bits, as smaller keys
|
|
230
|
+
* can be factored and are refused by current OpenSSH releases.
|
|
231
|
+
*
|
|
232
|
+
* @param key The public key a certificate would be issued for
|
|
233
|
+
* @returns The reason the key cannot be used, or undefined if it can be
|
|
234
|
+
*/
|
|
235
|
+
export const subjectKeyIssue = (key: PublicKey): string | undefined => {
|
|
236
|
+
const bits = rsaKeyBits(key)
|
|
237
|
+
if (bits !== undefined && bits < MinimumRsaKeyBits) {
|
|
238
|
+
return `RSA keys must be at least ${MinimumRsaKeyBits} bits, not ${bits}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return undefined
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Refines a public key so only one allowed by {@link subjectKeyIssue} is
|
|
246
|
+
* accepted for a certificate request.
|
|
247
|
+
*/
|
|
248
|
+
export const refineSubjectKey = (val: PublicKey, ctx: z.RefinementCtx): void => {
|
|
249
|
+
const issue = subjectKeyIssue(val)
|
|
250
|
+
if (issue !== undefined) {
|
|
251
|
+
fatalIssue(ctx, issue, val.toString())
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
148
255
|
/**
|
|
149
256
|
* Thrown by {@link identityPrincipals} when the principals claim of an
|
|
150
257
|
* identity token is not a string or an array of strings.
|
|
@@ -210,13 +317,13 @@ export const parseIdentity = async (env: SshCaBindings, jwt: string | undefined,
|
|
|
210
317
|
throw new Error("missing identity token")
|
|
211
318
|
}
|
|
212
319
|
|
|
213
|
-
const
|
|
214
|
-
const { payload } = await verifyJWT(env, jwt, { aud: aud.length === 0 ? undefined : aud })
|
|
320
|
+
const { payload } = await verifyJWT(env, jwt, env.JWT_AUD)
|
|
215
321
|
|
|
322
|
+
const sub = tokenSubject(payload)
|
|
216
323
|
const principals = identityPrincipals(env, payload, claim)
|
|
217
324
|
|
|
218
325
|
return {
|
|
219
|
-
sub:
|
|
326
|
+
sub: sub,
|
|
220
327
|
principals: principals,
|
|
221
328
|
}
|
|
222
329
|
}
|
|
@@ -227,9 +334,14 @@ export const transformIdentityToken = async (env: SshCaBindings, val: string, ct
|
|
|
227
334
|
|
|
228
335
|
return identity
|
|
229
336
|
} catch (err) {
|
|
337
|
+
// a misconfigured CA is a server error, not a validation failure
|
|
338
|
+
if (err instanceof MissingAudienceError) {
|
|
339
|
+
logger(env).error(err.message, "in", "transformIdentityToken")
|
|
340
|
+
throw new InternalServerErrorException(err.message)
|
|
341
|
+
}
|
|
230
342
|
ctx.issues.push({
|
|
231
343
|
code: "custom",
|
|
232
|
-
message: err instanceof PrincipalsClaimError ? err.message : "problem parsing identity token",
|
|
344
|
+
message: err instanceof PrincipalsClaimError || err instanceof TokenClaimError ? err.message : "problem parsing identity token",
|
|
233
345
|
input: val
|
|
234
346
|
})
|
|
235
347
|
return z.NEVER
|
|
@@ -356,6 +468,51 @@ export const refineHostCertificateRequest = async (env: SshCaBindings, val: Pars
|
|
|
356
468
|
}
|
|
357
469
|
}
|
|
358
470
|
|
|
471
|
+
// characters that OpenSSH treats as pattern syntax when matching host
|
|
472
|
+
// certificate principals, or that separate entries in a pattern list
|
|
473
|
+
const HostPrincipalPatternChars = /[*?!,]/
|
|
474
|
+
// whitespace and control characters, which no host name contains
|
|
475
|
+
const HostPrincipalInvalidChars = /[\s\p{Cc}]/u
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Returns why a principal cannot be included on a host certificate, or
|
|
479
|
+
* undefined if it can be.
|
|
480
|
+
*
|
|
481
|
+
* OpenSSH matches the principals of a host certificate as patterns, so a
|
|
482
|
+
* principal such as `*` would make the certificate valid for every host that
|
|
483
|
+
* trusts the CA. Principals are otherwise not restricted, as hosts may be
|
|
484
|
+
* reached by IP address, bare host name or fully qualified domain name.
|
|
485
|
+
*
|
|
486
|
+
* @param principal The principal to check
|
|
487
|
+
* @returns The reason the principal cannot be used, or undefined if it can be
|
|
488
|
+
*/
|
|
489
|
+
export const hostPrincipalIssue = (principal: string): string | undefined => {
|
|
490
|
+
if (principal === "") {
|
|
491
|
+
return "host certificate principals must not be empty"
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (HostPrincipalPatternChars.test(principal)) {
|
|
495
|
+
return `host certificate principal ${JSON.stringify(principal)} must not contain wildcard or pattern characters (*, ?, ! or ,)`
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (HostPrincipalInvalidChars.test(principal)) {
|
|
499
|
+
return `host certificate principal ${JSON.stringify(principal)} must not contain whitespace or control characters`
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return undefined
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Refines a host certificate principal so only one allowed by
|
|
507
|
+
* {@link hostPrincipalIssue} is accepted.
|
|
508
|
+
*/
|
|
509
|
+
export const refineHostPrincipal = (val: string, ctx: z.RefinementCtx): void => {
|
|
510
|
+
const issue = hostPrincipalIssue(val)
|
|
511
|
+
if (issue !== undefined) {
|
|
512
|
+
fatalIssue(ctx, issue, val)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
359
516
|
type ParsedHostCertificateRenewal = {
|
|
360
517
|
certificate: SshCertificate
|
|
361
518
|
proof: RenewalProofOfPossession
|
|
@@ -363,7 +520,8 @@ type ParsedHostCertificateRenewal = {
|
|
|
363
520
|
|
|
364
521
|
/**
|
|
365
522
|
* Checks a certificate presented for renewal was issued by this CA, is a host
|
|
366
|
-
* certificate
|
|
523
|
+
* certificate whose principals are allowed (see {@link hostPrincipalIssue}),
|
|
524
|
+
* is within its validity period and has not been revoked.
|
|
367
525
|
*
|
|
368
526
|
* @param env The CA bindings
|
|
369
527
|
* @param isRevoked Returns whether a serial has been revoked
|
|
@@ -384,6 +542,22 @@ export const checkRenewableCertificate = async (env: SshCaBindings, isRevoked: t
|
|
|
384
542
|
return "the provided certificate is not a host certificate"
|
|
385
543
|
}
|
|
386
544
|
|
|
545
|
+
// a certificate issued before small RSA keys were refused must not be
|
|
546
|
+
// renewed, as the renewal would keep its key
|
|
547
|
+
const keyIssue = subjectKeyIssue(certificate.publicKey)
|
|
548
|
+
if (keyIssue !== undefined) {
|
|
549
|
+
return `the provided certificate cannot be renewed: ${keyIssue}`
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// a certificate issued before wildcard principals were refused must not
|
|
553
|
+
// be renewed, as the renewal would keep its principals
|
|
554
|
+
for (const principal of certificate.principals) {
|
|
555
|
+
const issue = hostPrincipalIssue(principal)
|
|
556
|
+
if (issue !== undefined) {
|
|
557
|
+
return `the provided certificate cannot be renewed: ${issue}`
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
387
561
|
// check certificate is within its validity period, allowing for clock
|
|
388
562
|
// skew at the start as a certificate issued moments ago may have a
|
|
389
563
|
// validity start up to a second in the future
|
package/src/verify.ts
CHANGED
|
@@ -3,23 +3,63 @@ import { CertificateRequestJWTPayload } from "./types.js"
|
|
|
3
3
|
import type { SshCaBindings } from "./types.js"
|
|
4
4
|
import { split } from "./utils.js"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when no audience is configured for a token, as a token issued for
|
|
8
|
+
* any client of the IdP would otherwise be accepted.
|
|
9
|
+
*/
|
|
10
|
+
export class MissingAudienceError extends Error {
|
|
11
|
+
constructor(message: string) {
|
|
12
|
+
super(message)
|
|
13
|
+
this.name = "MissingAudienceError"
|
|
14
|
+
|
|
15
|
+
Object.setPrototypeOf(this, MissingAudienceError.prototype)
|
|
16
|
+
}
|
|
8
17
|
}
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
type RemoteJWKSet = ReturnType<typeof createRemoteJWKSet>
|
|
20
|
+
|
|
21
|
+
// the remote JWKS for each URL, kept between requests so the keys that jose
|
|
22
|
+
// caches are reused rather than fetched again for every token
|
|
23
|
+
const remoteJWKSets = new Map<string, RemoteJWKSet>()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the remote JWKS for a URL, creating it on first use.
|
|
27
|
+
*
|
|
28
|
+
* jose caches the keys it fetches for up to 10 minutes and fetches them again
|
|
29
|
+
* sooner when a token is signed with a key it does not have, so keys rotated
|
|
30
|
+
* by the IdP are still picked up.
|
|
31
|
+
*
|
|
32
|
+
* @param url The URL of the JWKS
|
|
33
|
+
* @returns The remote JWKS for the URL
|
|
34
|
+
*/
|
|
35
|
+
const remoteJWKSet = (url: string): RemoteJWKSet => {
|
|
36
|
+
let jwks = remoteJWKSets.get(url)
|
|
37
|
+
if (jwks === undefined) {
|
|
38
|
+
jwks = createRemoteJWKSet(new URL(url))
|
|
39
|
+
remoteJWKSets.set(url, jwks)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return jwks
|
|
43
|
+
}
|
|
12
44
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Verifies a JWT from the IdP, including that it was issued for one of the
|
|
47
|
+
* audiences given.
|
|
48
|
+
*
|
|
49
|
+
* @param env The CA bindings
|
|
50
|
+
* @param jwt The JWT to verify
|
|
51
|
+
* @param aud The audiences the JWT may be issued for, which must not be empty
|
|
52
|
+
* @throws {@link MissingAudienceError} when no audience is given
|
|
53
|
+
*/
|
|
54
|
+
export const verifyJWT = async (env: SshCaBindings, jwt: string, aud: string | string[] | undefined) => {
|
|
55
|
+
const audience = split(aud)
|
|
56
|
+
if (audience.length === 0) {
|
|
57
|
+
throw new MissingAudienceError("no audience is configured for verifying tokens, set JWT_AUD")
|
|
19
58
|
}
|
|
20
59
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
60
|
+
return await jwtVerify<CertificateRequestJWTPayload>(jwt, remoteJWKSet(env.JWT_JWKS_URL), {
|
|
61
|
+
issuer: env.JWT_ISSUER,
|
|
62
|
+
algorithms: split(env.JWT_ALGORITHMS),
|
|
63
|
+
audience: audience,
|
|
64
|
+
})
|
|
25
65
|
}
|