@maximemrf/adonisjs-jwt 1.0.0-rc.3 → 1.0.0-rc.4
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/build/src/define_config.js +63 -38
- package/build/src/drivers/asymmetric.js +7 -0
- package/build/src/errors.d.ts +23 -0
- package/build/src/errors.js +56 -0
- package/build/src/guard.d.ts +8 -1
- package/build/src/guard.js +25 -12
- package/build/src/validation.d.ts +10 -0
- package/build/src/validation.js +47 -4
- package/package.json +1 -1
|
@@ -1,56 +1,81 @@
|
|
|
1
1
|
import { JwtGuard } from './guard.js';
|
|
2
|
-
import {
|
|
2
|
+
import { validateLifetimes, validateSigningMethod } from './validation.js';
|
|
3
3
|
import { resolveDriver } from './driver_resolver.js';
|
|
4
4
|
import { hkdfSync } from 'node:crypto';
|
|
5
5
|
export function jwtGuard(config) {
|
|
6
|
+
const asymmetricKeys = {
|
|
7
|
+
privateKey: config.privateKey,
|
|
8
|
+
publicKey: config.publicKey,
|
|
9
|
+
algorithm: config.algorithm,
|
|
10
|
+
};
|
|
11
|
+
const usesAsymmetric = Object.values(asymmetricKeys).some((value) => value !== undefined);
|
|
12
|
+
/**
|
|
13
|
+
* Only fall back on the app key when no other signing method is configured
|
|
14
|
+
*/
|
|
15
|
+
const usesAppKey = config.secret === undefined && !usesAsymmetric && !config.jwks;
|
|
16
|
+
const options = {
|
|
17
|
+
...(usesAsymmetric ? asymmetricKeys : { secret: config.secret }),
|
|
18
|
+
refreshTokenUserProvider: config.refreshTokenUserProvider,
|
|
19
|
+
tokenName: config.tokenName,
|
|
20
|
+
refreshTokenName: config.refreshTokenName,
|
|
21
|
+
tokenExpiresIn: config.tokenExpiresIn,
|
|
22
|
+
refreshTokenExpiresIn: config.refreshTokenExpiresIn,
|
|
23
|
+
useCookies: config.useCookies,
|
|
24
|
+
useCookiesForRefreshToken: config.useCookiesForRefreshToken,
|
|
25
|
+
refreshTokenAbilities: config.refreshTokenAbilities,
|
|
26
|
+
content: config.content,
|
|
27
|
+
jwks: config.jwks,
|
|
28
|
+
cookie: config.cookie,
|
|
29
|
+
issuer: config.issuer,
|
|
30
|
+
audience: config.audience,
|
|
31
|
+
algorithms: config.algorithms,
|
|
32
|
+
clockTolerance: config.clockTolerance,
|
|
33
|
+
getUserId: config.getUserId,
|
|
34
|
+
verifyPayload: config.verifyPayload,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Validate the configuration when config/auth.ts is loaded, so a bad
|
|
38
|
+
* configuration stops the app from booting. The resolver only runs on
|
|
39
|
+
* the first request, when the app would already be serving traffic.
|
|
40
|
+
*/
|
|
41
|
+
validateLifetimes(options, PREFIX);
|
|
42
|
+
let driver;
|
|
43
|
+
if (usesAppKey) {
|
|
44
|
+
/**
|
|
45
|
+
* The derived key depends on the guard name, only known in the resolver,
|
|
46
|
+
* but the app key itself can already be checked when the env has it
|
|
47
|
+
*/
|
|
48
|
+
if (process.env.APP_KEY !== undefined)
|
|
49
|
+
assertAppKey(process.env.APP_KEY);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
validateSigningMethod(options, PREFIX);
|
|
53
|
+
driver = resolveDriver(options, PREFIX);
|
|
54
|
+
}
|
|
6
55
|
return {
|
|
7
56
|
async resolver(name, app) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Only fall back on the app key when no other signing method is configured
|
|
16
|
-
*/
|
|
17
|
-
const resolvedSecret = config.secret ??
|
|
18
|
-
(usesAsymmetric || config.jwks ? undefined : deriveSecretFromAppKey(readAppKey(app), name));
|
|
19
|
-
const options = {
|
|
20
|
-
...(usesAsymmetric ? asymmetricKeys : { secret: resolvedSecret }),
|
|
21
|
-
refreshTokenUserProvider: config.refreshTokenUserProvider,
|
|
22
|
-
tokenName: config.tokenName,
|
|
23
|
-
refreshTokenName: config.refreshTokenName,
|
|
24
|
-
tokenExpiresIn: config.tokenExpiresIn,
|
|
25
|
-
refreshTokenExpiresIn: config.refreshTokenExpiresIn,
|
|
26
|
-
useCookies: config.useCookies,
|
|
27
|
-
useCookiesForRefreshToken: config.useCookiesForRefreshToken,
|
|
28
|
-
refreshTokenAbilities: config.refreshTokenAbilities,
|
|
29
|
-
content: config.content,
|
|
30
|
-
jwks: config.jwks,
|
|
31
|
-
cookie: config.cookie,
|
|
32
|
-
issuer: config.issuer,
|
|
33
|
-
audience: config.audience,
|
|
34
|
-
algorithms: config.algorithms,
|
|
35
|
-
clockTolerance: config.clockTolerance,
|
|
36
|
-
getUserId: config.getUserId,
|
|
37
|
-
verifyPayload: config.verifyPayload,
|
|
38
|
-
};
|
|
39
|
-
validateGuardOptions(options, 'JWT guard');
|
|
40
|
-
const driver = resolveDriver(options, 'JWT guard');
|
|
41
|
-
return (ctx) => new JwtGuard(ctx, config.provider, { ...options, driver });
|
|
57
|
+
if (usesAppKey) {
|
|
58
|
+
const secret = deriveSecretFromAppKey(readAppKey(app), name);
|
|
59
|
+
driver = resolveDriver({ ...options, secret }, PREFIX);
|
|
60
|
+
}
|
|
61
|
+
const resolvedDriver = driver;
|
|
62
|
+
return (ctx) => new JwtGuard(ctx, config.provider, { ...options, driver: resolvedDriver });
|
|
42
63
|
},
|
|
43
64
|
};
|
|
44
65
|
}
|
|
66
|
+
const PREFIX = 'JWT guard';
|
|
67
|
+
function assertAppKey(rawAppKey) {
|
|
68
|
+
if (!rawAppKey || rawAppKey.length < 32) {
|
|
69
|
+
throw new Error('JWT guard requires the application key (APP_KEY) to be at least 32 characters when no `secret` is configured');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
45
72
|
/**
|
|
46
73
|
* Derive a dedicated signing key from the app key, so the guard never signs
|
|
47
74
|
* with the raw app key used elsewhere by the application, and two JWT guards
|
|
48
75
|
* never accept each other's tokens.
|
|
49
76
|
*/
|
|
50
77
|
function deriveSecretFromAppKey(rawAppKey, guardName) {
|
|
51
|
-
|
|
52
|
-
throw new Error('JWT guard requires the application key (APP_KEY) to be at least 32 characters when no `secret` is configured');
|
|
53
|
-
}
|
|
78
|
+
assertAppKey(rawAppKey);
|
|
54
79
|
return Buffer.from(hkdfSync('sha256', rawAppKey, '', `@maximemrf/adonisjs-jwt:${guardName}`, 32)).toString('hex');
|
|
55
80
|
}
|
|
56
81
|
/**
|
|
@@ -35,6 +35,13 @@ export class AsymmetricDriver {
|
|
|
35
35
|
if (publicKey.asymmetricKeyType !== expectedKeyType) {
|
|
36
36
|
throw new Error(`publicKey type "${publicKey.asymmetricKeyType}" does not match algorithm "${algorithm}"`);
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* With keys from two different pairs, every token the guard issues
|
|
40
|
+
* would be rejected by the same guard: all users would be locked out
|
|
41
|
+
*/
|
|
42
|
+
if (!createPublicKey(privateKey).equals(publicKey)) {
|
|
43
|
+
throw new Error('publicKey does not belong to the same key pair as privateKey');
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
sign(payload, options) {
|
|
40
47
|
const signOptions = {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { errors } from '@adonisjs/auth';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
type UnauthorizedAccess = InstanceType<typeof errors.E_UNAUTHORIZED_ACCESS>;
|
|
4
|
+
type Renderer = (message: string, error: UnauthorizedAccess, ctx: HttpContext) => any;
|
|
5
|
+
/**
|
|
6
|
+
* Renders the 401 raised by the JWT guard, or by the `auth` middleware when
|
|
7
|
+
* the JWT guard was the last one tried.
|
|
8
|
+
*
|
|
9
|
+
* - JSON and JSON:API requests get the same body as the `access_tokens` guard.
|
|
10
|
+
* - HTML requests (pages, Inertia visits) are redirected to the login route
|
|
11
|
+
* when the `auth` middleware passes one (`loginRoute`), like the `session`
|
|
12
|
+
* guard does. Without a login route, they get a plain text 401.
|
|
13
|
+
*/
|
|
14
|
+
export declare const renderJwtUnauthorizedAccess: Renderer;
|
|
15
|
+
/**
|
|
16
|
+
* @adonisjs/auth only ships renderers for its own guards, and the `auth`
|
|
17
|
+
* middleware re-throws its own `E_UNAUTHORIZED_ACCESS` with
|
|
18
|
+
* `guardDriverName: 'jwt'`, so a subclass would never reach the exception
|
|
19
|
+
* handler. Register the `jwt` renderer on every instance instead, unless
|
|
20
|
+
* the application already registered its own.
|
|
21
|
+
*/
|
|
22
|
+
export declare function installJwtUnauthorizedRenderer(): void;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { errors } from '@adonisjs/auth';
|
|
2
|
+
/**
|
|
3
|
+
* Renders the 401 raised by the JWT guard, or by the `auth` middleware when
|
|
4
|
+
* the JWT guard was the last one tried.
|
|
5
|
+
*
|
|
6
|
+
* - JSON and JSON:API requests get the same body as the `access_tokens` guard.
|
|
7
|
+
* - HTML requests (pages, Inertia visits) are redirected to the login route
|
|
8
|
+
* when the `auth` middleware passes one (`loginRoute`), like the `session`
|
|
9
|
+
* guard does. Without a login route, they get a plain text 401.
|
|
10
|
+
*/
|
|
11
|
+
export const renderJwtUnauthorizedAccess = (message, error, ctx) => {
|
|
12
|
+
switch (ctx.request.accepts(['html', 'application/vnd.api+json', 'json'])) {
|
|
13
|
+
case 'html':
|
|
14
|
+
case null:
|
|
15
|
+
if (error.redirectTo) {
|
|
16
|
+
/**
|
|
17
|
+
* `withIntendedUrl` is added by @adonisjs/session, which is optional
|
|
18
|
+
*/
|
|
19
|
+
const redirect = ctx.response.redirect();
|
|
20
|
+
if (typeof redirect.withIntendedUrl === 'function')
|
|
21
|
+
redirect.withIntendedUrl();
|
|
22
|
+
redirect.withQs().toPath(error.redirectTo);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
ctx.response.status(error.status).send(message);
|
|
26
|
+
return;
|
|
27
|
+
case 'json':
|
|
28
|
+
ctx.response.status(error.status).send({ errors: [{ message }] });
|
|
29
|
+
return;
|
|
30
|
+
case 'application/vnd.api+json':
|
|
31
|
+
ctx.response.status(error.status).send({ errors: [{ code: error.code, title: message }] });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const INSTALLED = Symbol.for('@maximemrf/adonisjs-jwt/unauthorized-renderer');
|
|
36
|
+
/**
|
|
37
|
+
* @adonisjs/auth only ships renderers for its own guards, and the `auth`
|
|
38
|
+
* middleware re-throws its own `E_UNAUTHORIZED_ACCESS` with
|
|
39
|
+
* `guardDriverName: 'jwt'`, so a subclass would never reach the exception
|
|
40
|
+
* handler. Register the `jwt` renderer on every instance instead, unless
|
|
41
|
+
* the application already registered its own.
|
|
42
|
+
*/
|
|
43
|
+
export function installJwtUnauthorizedRenderer() {
|
|
44
|
+
const prototype = errors.E_UNAUTHORIZED_ACCESS.prototype;
|
|
45
|
+
if (prototype[INSTALLED])
|
|
46
|
+
return;
|
|
47
|
+
const handle = prototype.handle;
|
|
48
|
+
prototype.handle = function (error, ctx) {
|
|
49
|
+
const renderers = this.renderers;
|
|
50
|
+
if (this.guardDriverName === 'jwt' && !renderers.jwt) {
|
|
51
|
+
renderers.jwt = renderJwtUnauthorizedAccess;
|
|
52
|
+
}
|
|
53
|
+
return handle.call(this, error, ctx);
|
|
54
|
+
};
|
|
55
|
+
prototype[INSTALLED] = true;
|
|
56
|
+
}
|
package/build/src/guard.d.ts
CHANGED
|
@@ -44,7 +44,14 @@ export declare class JwtGuard<UserProvider extends JwtUserProviderContract<unkno
|
|
|
44
44
|
}>;
|
|
45
45
|
generateWithRefreshToken(refreshToken?: string): Promise<JwtGenerateResult<true>>;
|
|
46
46
|
/**
|
|
47
|
-
* Revoke the refresh token
|
|
47
|
+
* Revoke the refresh token passed as argument, or else the one issued
|
|
48
|
+
* during this request, or else the one sent by the client (cookie, body
|
|
49
|
+
* or Authorization header).
|
|
50
|
+
*
|
|
51
|
+
* The cookies are always cleared. When no valid refresh token is found,
|
|
52
|
+
* nothing is revoked and `E_UNAUTHORIZED_ACCESS` is raised, so a client
|
|
53
|
+
* that sends the wrong token (e.g. its access token) does not believe it
|
|
54
|
+
* has logged out while its refresh token stays valid.
|
|
48
55
|
*/
|
|
49
56
|
revoke(refreshToken?: string): Promise<void>;
|
|
50
57
|
/**
|
package/build/src/guard.js
CHANGED
|
@@ -3,6 +3,8 @@ import { DEFAULT_TOKEN_EXPIRES_IN, } from './types.js';
|
|
|
3
3
|
import { Secret } from '@adonisjs/core/helpers';
|
|
4
4
|
import { validateGuardOptions } from './validation.js';
|
|
5
5
|
import { resolveDriver } from './driver_resolver.js';
|
|
6
|
+
import { installJwtUnauthorizedRenderer } from './errors.js';
|
|
7
|
+
installJwtUnauthorizedRenderer();
|
|
6
8
|
export class JwtGuard {
|
|
7
9
|
#ctx;
|
|
8
10
|
#userProvider;
|
|
@@ -12,6 +14,12 @@ export class JwtGuard {
|
|
|
12
14
|
#refreshTokenName = 'refreshToken';
|
|
13
15
|
#driver;
|
|
14
16
|
#cookieOptions;
|
|
17
|
+
/**
|
|
18
|
+
* Refresh token issued during the current request, by `generate()` or a
|
|
19
|
+
* refresh. A logout in the same request revokes it: the request still
|
|
20
|
+
* carries the previous one, which the refresh has already deleted.
|
|
21
|
+
*/
|
|
22
|
+
#issuedRefreshToken;
|
|
15
23
|
constructor(ctx, userProvider, option) {
|
|
16
24
|
this.#ctx = ctx;
|
|
17
25
|
this.#userProvider = userProvider;
|
|
@@ -98,6 +106,7 @@ export class JwtGuard {
|
|
|
98
106
|
}
|
|
99
107
|
: undefined);
|
|
100
108
|
refreshToken = generatedRefreshToken.value.release();
|
|
109
|
+
this.#issuedRefreshToken = refreshToken;
|
|
101
110
|
}
|
|
102
111
|
if (this.#options.useCookies) {
|
|
103
112
|
this.#ctx.response.cookie(`${this.#tokenName}`, token, {
|
|
@@ -293,7 +302,14 @@ export class JwtGuard {
|
|
|
293
302
|
});
|
|
294
303
|
}
|
|
295
304
|
/**
|
|
296
|
-
* Revoke the refresh token
|
|
305
|
+
* Revoke the refresh token passed as argument, or else the one issued
|
|
306
|
+
* during this request, or else the one sent by the client (cookie, body
|
|
307
|
+
* or Authorization header).
|
|
308
|
+
*
|
|
309
|
+
* The cookies are always cleared. When no valid refresh token is found,
|
|
310
|
+
* nothing is revoked and `E_UNAUTHORIZED_ACCESS` is raised, so a client
|
|
311
|
+
* that sends the wrong token (e.g. its access token) does not believe it
|
|
312
|
+
* has logged out while its refresh token stays valid.
|
|
297
313
|
*/
|
|
298
314
|
async revoke(refreshToken) {
|
|
299
315
|
if (!this.#refreshTokenUserProvider) {
|
|
@@ -302,24 +318,21 @@ export class JwtGuard {
|
|
|
302
318
|
});
|
|
303
319
|
}
|
|
304
320
|
this.#clearCookies();
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
refreshToken = await this.#findRefreshToken();
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
321
|
+
refreshToken = refreshToken ?? this.#issuedRefreshToken ?? (await this.#findRefreshToken());
|
|
313
322
|
/**
|
|
314
323
|
* Only delete a refresh token whose secret is valid. The tokens
|
|
315
324
|
* provider deletes by id alone, so a forged token with a guessed
|
|
316
325
|
* id would otherwise revoke another user's refresh token.
|
|
317
326
|
*/
|
|
318
327
|
const secret = new Secret(refreshToken);
|
|
319
|
-
if (!(await this.#refreshTokenUserProvider.verifyToken(secret))
|
|
320
|
-
|
|
328
|
+
if (!(await this.#refreshTokenUserProvider.verifyToken(secret)) ||
|
|
329
|
+
!(await this.#refreshTokenUserProvider.invalidateToken(secret))) {
|
|
330
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
331
|
+
guardDriverName: this.driverName,
|
|
332
|
+
});
|
|
321
333
|
}
|
|
322
|
-
|
|
334
|
+
if (refreshToken === this.#issuedRefreshToken)
|
|
335
|
+
this.#issuedRefreshToken = undefined;
|
|
323
336
|
}
|
|
324
337
|
/**
|
|
325
338
|
* Remove the access and refresh token cookies set by the guard
|
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
import type { JwtGuardOptions } from './types.js';
|
|
2
2
|
export declare function validateGuardOptions(options: JwtGuardOptions, prefix?: string): void;
|
|
3
|
+
/**
|
|
4
|
+
* Token lifetimes and clock tolerance. A lifetime that is not a positive
|
|
5
|
+
* duration would issue tokens that are already expired (`'-1h'`), or fail
|
|
6
|
+
* on every login (`'abc'`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateLifetimes(options: Pick<JwtGuardOptions, 'tokenExpiresIn' | 'refreshTokenExpiresIn' | 'clockTolerance'>, prefix: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Exactly one signing method: a secret, an asymmetric key pair, or a JWKS endpoint
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateSigningMethod(options: JwtGuardOptions, prefix: string): void;
|
package/build/src/validation.js
CHANGED
|
@@ -1,11 +1,50 @@
|
|
|
1
|
+
import ms from 'ms';
|
|
1
2
|
export function validateGuardOptions(options, prefix = 'JwtGuard') {
|
|
2
|
-
|
|
3
|
-
!(Number.isFinite(options.tokenExpiresIn) && options.tokenExpiresIn > 0)) {
|
|
4
|
-
throw new Error(`${prefix} \`tokenExpiresIn\` must be a positive number of seconds`);
|
|
5
|
-
}
|
|
3
|
+
validateLifetimes(options, prefix);
|
|
6
4
|
if (options.driver) {
|
|
7
5
|
return;
|
|
8
6
|
}
|
|
7
|
+
validateSigningMethod(options, prefix);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Token lifetimes and clock tolerance. A lifetime that is not a positive
|
|
11
|
+
* duration would issue tokens that are already expired (`'-1h'`), or fail
|
|
12
|
+
* on every login (`'abc'`).
|
|
13
|
+
*/
|
|
14
|
+
export function validateLifetimes(options, prefix) {
|
|
15
|
+
assertDuration(options.tokenExpiresIn, 'tokenExpiresIn', prefix);
|
|
16
|
+
assertDuration(options.refreshTokenExpiresIn, 'refreshTokenExpiresIn', prefix);
|
|
17
|
+
if (options.clockTolerance !== undefined &&
|
|
18
|
+
!(Number.isFinite(options.clockTolerance) && options.clockTolerance >= 0)) {
|
|
19
|
+
throw new Error(`${prefix} \`clockTolerance\` must be a number of seconds greater than or equal to 0`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function assertDuration(value, name, prefix) {
|
|
23
|
+
if (value === undefined) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'number') {
|
|
27
|
+
if (!(Number.isFinite(value) && value > 0)) {
|
|
28
|
+
throw new Error(`${prefix} \`${name}\` must be a positive number of seconds`);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A numeric string such as '3600' (read from an env variable) is parsed as
|
|
34
|
+
* milliseconds by jsonwebtoken, so it is ambiguous: ask for a unit
|
|
35
|
+
*/
|
|
36
|
+
if (typeof value !== 'string' || /^\s*[-+]?\d*\.?\d+\s*$/.test(value)) {
|
|
37
|
+
throw new Error(`${prefix} \`${name}\` must be a number of seconds or a duration with a unit, such as '15m' or '7d' (received ${JSON.stringify(value)})`);
|
|
38
|
+
}
|
|
39
|
+
const milliseconds = ms(value);
|
|
40
|
+
if (milliseconds === undefined || !Number.isFinite(milliseconds) || milliseconds < 1000) {
|
|
41
|
+
throw new Error(`${prefix} \`${name}\` must be a duration of at least 1 second, such as '15m' or '7d' (received ${JSON.stringify(value)})`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Exactly one signing method: a secret, an asymmetric key pair, or a JWKS endpoint
|
|
46
|
+
*/
|
|
47
|
+
export function validateSigningMethod(options, prefix) {
|
|
9
48
|
const asymmetricPartial = options.privateKey !== undefined ||
|
|
10
49
|
options.publicKey !== undefined ||
|
|
11
50
|
options.algorithm !== undefined;
|
|
@@ -18,6 +57,10 @@ export function validateGuardOptions(options, prefix = 'JwtGuard') {
|
|
|
18
57
|
if (options.jwks && usesAsymmetric) {
|
|
19
58
|
throw new Error(`${prefix} cannot use \`jwks\` together with asymmetric \`privateKey\` / \`publicKey\``);
|
|
20
59
|
}
|
|
60
|
+
if (options.jwks &&
|
|
61
|
+
!(typeof options.jwks.jwksUri === 'string' && options.jwks.jwksUri.trim() !== '')) {
|
|
62
|
+
throw new Error(`${prefix} \`jwks.jwksUri\` is required to fetch the identity provider keys`);
|
|
63
|
+
}
|
|
21
64
|
if (!options.jwks && !usesAsymmetric && !options.secret) {
|
|
22
65
|
throw new Error(`${prefix} requires \`secret\` (symmetric), or \`privateKey\` + \`publicKey\` + \`algorithm\` (asymmetric), or \`jwks\``);
|
|
23
66
|
}
|