@maximemrf/adonisjs-jwt 1.0.0-rc.3 → 1.0.0-rc.5

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
@@ -7,7 +7,6 @@
7
7
  <a href="https://www.npmjs.com/package/@maximemrf/adonisjs-jwt"><img src="https://img.shields.io/npm/v/@maximemrf/adonisjs-jwt.svg?style=flat-square" alt="Version"></a>
8
8
  <a href="https://www.npmjs.com/package/@maximemrf/adonisjs-jwt"><img src="https://img.shields.io/npm/last-update/@maximemrf/adonisjs-jwt.svg?style=flat-square" alt="NPM Last Update"></a>
9
9
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/npm/l/@maximemrf/adonisjs-jwt.svg?style=flat-square" alt="License"></a>
10
- <a href="https://adonisjs.com/"><img src="https://img.shields.io/badge/AdonisJS-v6-5A45FF.svg?style=flat-square" alt="AdonisV6"></a>
11
10
  <a href="https://adonisjs.com/"><img src="https://img.shields.io/badge/AdonisJS-v7-5A45FF.svg?style=flat-square" alt="AdonisV7"></a>
12
11
  </p>
13
12
 
package/build/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { configure } from './configure.js';
2
2
  export { jwtGuard } from './src/define_config.js';
3
3
  export { JwtGuard } from './src/guard.js';
4
- export { DEFAULT_TOKEN_EXPIRES_IN } from './src/types.js';
4
+ export { DEFAULT_TOKEN_EXPIRES_IN } from './src/constants.js';
package/build/index.js CHANGED
@@ -9,4 +9,4 @@
9
9
  export { configure } from './configure.js';
10
10
  export { jwtGuard } from './src/define_config.js';
11
11
  export { JwtGuard } from './src/guard.js';
12
- export { DEFAULT_TOKEN_EXPIRES_IN } from './src/types.js';
12
+ export { DEFAULT_TOKEN_EXPIRES_IN } from './src/constants.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Lifetime of access tokens when `tokenExpiresIn` is not configured
3
+ */
4
+ export declare const DEFAULT_TOKEN_EXPIRES_IN = "1h";
5
+ /**
6
+ * Lifetime of refresh tokens when `refreshTokenExpiresIn` is not configured
7
+ */
8
+ export declare const DEFAULT_REFRESH_TOKEN_EXPIRES_IN = "7d";
9
+ export declare const ALLOWED_SYMMETRIC_ALGORITHMS: readonly ["HS256", "HS384", "HS512"];
10
+ export declare const ALLOWED_ASYMMETRIC_ALGORITHMS: readonly ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"];
11
+ export declare const ALLOWED_JWKS_ALGORITHMS: readonly ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Lifetime of access tokens when `tokenExpiresIn` is not configured
3
+ */
4
+ export const DEFAULT_TOKEN_EXPIRES_IN = '1h';
5
+ /**
6
+ * Lifetime of refresh tokens when `refreshTokenExpiresIn` is not configured
7
+ */
8
+ export const DEFAULT_REFRESH_TOKEN_EXPIRES_IN = '7d';
9
+ export const ALLOWED_SYMMETRIC_ALGORITHMS = ['HS256', 'HS384', 'HS512'];
10
+ export const ALLOWED_ASYMMETRIC_ALGORITHMS = [
11
+ 'RS256',
12
+ 'RS384',
13
+ 'RS512',
14
+ 'ES256',
15
+ 'ES384',
16
+ 'ES512',
17
+ ];
18
+ export const ALLOWED_JWKS_ALGORITHMS = [
19
+ ...ALLOWED_ASYMMETRIC_ALGORITHMS,
20
+ 'PS256',
21
+ 'PS384',
22
+ 'PS512',
23
+ ];
@@ -1,32 +1,6 @@
1
- import type { symbols } from '@adonisjs/auth';
2
1
  import type { GuardConfigProvider } from '@adonisjs/auth/types';
3
2
  import type { HttpContext } from '@adonisjs/core/http';
4
- import type { JwtAsymmetricAlgorithm, JwtGuardUser, JwtUserProviderContract, JwtCookieOptions, BaseJwtContent, JwtJwksAlgorithm, JwtGetUserId, JwtVerifyPayload } from './types.js';
3
+ import type { JwtUserProviderContract, JwtGuardConfig } from './types.js';
5
4
  import { JwtGuard } from './guard.js';
6
- import type { StringValue } from 'ms';
7
5
  import type { AccessTokensUserProviderContract } from '@adonisjs/auth/types/access_tokens';
8
- import type { Options } from 'jwks-rsa';
9
- export declare function jwtGuard<UserProvider extends JwtUserProviderContract<unknown>, RefreshTokenUserProvider extends AccessTokensUserProviderContract<unknown> | undefined = undefined>(config: {
10
- provider: UserProvider;
11
- refreshTokenUserProvider?: RefreshTokenUserProvider;
12
- tokenName?: string;
13
- refreshTokenName?: string;
14
- tokenExpiresIn?: number | StringValue;
15
- refreshTokenExpiresIn?: number | StringValue;
16
- useCookies?: boolean;
17
- useCookiesForRefreshToken?: boolean;
18
- refreshTokenAbilities?: string[];
19
- secret?: string;
20
- privateKey?: string;
21
- publicKey?: string;
22
- algorithm?: JwtAsymmetricAlgorithm;
23
- content?: (user: JwtGuardUser<UserProvider[typeof symbols.PROVIDER_REAL_USER]>) => Record<string, any> & BaseJwtContent;
24
- jwks?: Options;
25
- cookie?: JwtCookieOptions;
26
- issuer?: string;
27
- audience?: string | string[];
28
- algorithms?: JwtJwksAlgorithm[];
29
- clockTolerance?: number;
30
- getUserId?: JwtGetUserId;
31
- verifyPayload?: JwtVerifyPayload;
32
- }): GuardConfigProvider<(ctx: HttpContext) => JwtGuard<UserProvider, [RefreshTokenUserProvider] extends [undefined] ? false : true>>;
6
+ export declare function jwtGuard<UserProvider extends JwtUserProviderContract<unknown>, RefreshTokenUserProvider extends AccessTokensUserProviderContract<unknown> | undefined = undefined>(config: JwtGuardConfig<UserProvider, RefreshTokenUserProvider>): GuardConfigProvider<(ctx: HttpContext) => JwtGuard<UserProvider, [RefreshTokenUserProvider] extends [undefined] ? false : true>>;
@@ -1,56 +1,85 @@
1
1
  import { JwtGuard } from './guard.js';
2
- import { validateGuardOptions } from './validation.js';
2
+ import { releaseSecrets, 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 = releaseSecrets({
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
+ * `content` and `getUserId` come from the same config, so they keep
37
+ * the pairing checked by `JwtContentOptions`
38
+ */
39
+ });
40
+ /**
41
+ * Validate the configuration when config/auth.ts is loaded, so a bad
42
+ * configuration stops the app from booting. The resolver only runs on
43
+ * the first request, when the app would already be serving traffic.
44
+ */
45
+ validateLifetimes(options, PREFIX);
46
+ let driver;
47
+ if (usesAppKey) {
48
+ /**
49
+ * The derived key depends on the guard name, only known in the resolver,
50
+ * but the app key itself can already be checked when the env has it
51
+ */
52
+ if (process.env.APP_KEY !== undefined)
53
+ assertAppKey(process.env.APP_KEY);
54
+ }
55
+ else {
56
+ validateSigningMethod(options, PREFIX);
57
+ driver = resolveDriver(options, PREFIX);
58
+ }
6
59
  return {
7
60
  async resolver(name, app) {
8
- const asymmetricKeys = {
9
- privateKey: config.privateKey,
10
- publicKey: config.publicKey,
11
- algorithm: config.algorithm,
12
- };
13
- const usesAsymmetric = Object.values(asymmetricKeys).some((value) => value !== undefined);
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 });
61
+ if (usesAppKey) {
62
+ const secret = deriveSecretFromAppKey(readAppKey(app), name);
63
+ driver = resolveDriver({ ...options, secret }, PREFIX);
64
+ }
65
+ const resolvedDriver = driver;
66
+ return (ctx) => new JwtGuard(ctx, config.provider, { ...options, driver: resolvedDriver });
42
67
  },
43
68
  };
44
69
  }
70
+ const PREFIX = 'JWT guard';
71
+ function assertAppKey(rawAppKey) {
72
+ if (!rawAppKey || rawAppKey.length < 32) {
73
+ throw new Error('JWT guard requires the application key (APP_KEY) to be at least 32 characters when no `secret` is configured');
74
+ }
75
+ }
45
76
  /**
46
77
  * Derive a dedicated signing key from the app key, so the guard never signs
47
78
  * with the raw app key used elsewhere by the application, and two JWT guards
48
79
  * never accept each other's tokens.
49
80
  */
50
81
  function deriveSecretFromAppKey(rawAppKey, guardName) {
51
- if (!rawAppKey || rawAppKey.length < 32) {
52
- throw new Error('JWT guard requires the application key (APP_KEY) to be at least 32 characters when no `secret` is configured');
53
- }
82
+ assertAppKey(rawAppKey);
54
83
  return Buffer.from(hkdfSync('sha256', rawAppKey, '', `@maximemrf/adonisjs-jwt:${guardName}`, 32)).toString('hex');
55
84
  }
56
85
  /**
@@ -15,7 +15,7 @@ export function resolveDriver(options, contextName = 'JWT guard') {
15
15
  audience: options.audience,
16
16
  clockTolerance: options.clockTolerance,
17
17
  algorithms: options.algorithms,
18
- verifiesPayload: options.verifyPayload !== undefined,
18
+ verifiesPayload: typeof options.verifyPayload === 'function',
19
19
  });
20
20
  }
21
21
  catch (error) {
@@ -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 = {
@@ -1,7 +1,7 @@
1
1
  import type { Options } from 'jwks-rsa';
2
2
  import type { StringValue } from 'ms';
3
3
  import type { JwtDriver } from './types.js';
4
- import { type JwtJwksAlgorithm } from '../types.js';
4
+ import type { JwtJwksAlgorithm } from '../types.js';
5
5
  /**
6
6
  * Safe jwks-rsa defaults: tokens with an unknown `kid` trigger a JWKS
7
7
  * fetch, so fetches must be cached and rate limited to avoid hammering
@@ -1,7 +1,8 @@
1
1
  import jwt from 'jsonwebtoken';
2
2
  import { errors } from '@adonisjs/auth';
3
+ import { RuntimeException } from '@adonisjs/core/exceptions';
3
4
  import { JwksManager } from '../jwks.js';
4
- import { ALLOWED_JWKS_ALGORITHMS } from '../types.js';
5
+ import { ALLOWED_JWKS_ALGORITHMS } from '../constants.js';
5
6
  /**
6
7
  * Safe jwks-rsa defaults: tokens with an unknown `kid` trigger a JWKS
7
8
  * fetch, so fetches must be cached and rate limited to avoid hammering
@@ -40,9 +41,7 @@ export class JwksDriver {
40
41
  this.#algorithms = algorithms;
41
42
  }
42
43
  sign(_payload, _options) {
43
- throw new errors.E_UNAUTHORIZED_ACCESS("You can't use the auth.generate method with jwks", {
44
- guardDriverName: 'jwt',
45
- });
44
+ throw new RuntimeException('JWT guard cannot issue tokens in JWKS mode: tokens are issued by the identity provider');
46
45
  }
47
46
  async verify(token) {
48
47
  const decoded = jwt.decode(token, { complete: true });
@@ -1,5 +1,5 @@
1
1
  import jwt from 'jsonwebtoken';
2
- import { ALLOWED_SYMMETRIC_ALGORITHMS } from '../types.js';
2
+ import { ALLOWED_SYMMETRIC_ALGORITHMS } from '../constants.js';
3
3
  export class SymmetricDriver {
4
4
  canSign = true;
5
5
  #secret;
@@ -8,10 +8,10 @@ export class SymmetricDriver {
8
8
  #clockTolerance;
9
9
  constructor(options) {
10
10
  if (!options.secret) {
11
- throw new Error('Symmetric JWT driver requires a secret key');
11
+ throw new Error('JWT guard requires a `secret`');
12
12
  }
13
13
  if (options.secret.length < 32) {
14
- throw new Error('Symmetric JWT driver requires a secret of at least 32 characters to ensure sufficient entropy');
14
+ throw new Error('JWT guard `secret` must be at least 32 characters to ensure sufficient entropy');
15
15
  }
16
16
  this.#secret = options.secret;
17
17
  this.#issuer = options.issuer;
@@ -6,3 +6,14 @@ export interface JwtDriver {
6
6
  }): string;
7
7
  verify(token: string): Promise<Record<string, any> | string> | Record<string, any> | string;
8
8
  }
9
+ /**
10
+ * Options of the `JwtGuard` constructor that are not part of the public
11
+ * API: `jwtGuard()` resolves the driver once and passes it to every guard.
12
+ * Custom drivers are not supported and the `JwtDriver` interface may change.
13
+ */
14
+ export type JwtGuardInternalOptions = {
15
+ /**
16
+ * @internal
17
+ */
18
+ driver?: JwtDriver;
19
+ };
@@ -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
+ }
@@ -1,10 +1,11 @@
1
1
  import { symbols } from '@adonisjs/auth';
2
2
  import type { AuthClientResponse, GuardContract } from '@adonisjs/auth/types';
3
3
  import type { HttpContext } from '@adonisjs/core/http';
4
- import { type JwtUserProviderContract, type JwtGuardOptions, type JwtGenerateResult } from './types.js';
4
+ import type { JwtUserProviderContract, JwtGuardOptions, JwtGenerateResult } from './types.js';
5
+ import type { JwtGuardInternalOptions } from './drivers/types.js';
5
6
  export declare class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>, WithRefreshToken extends boolean = boolean> implements GuardContract<UserProvider[typeof symbols.PROVIDER_REAL_USER]> {
6
7
  #private;
7
- constructor(ctx: HttpContext, userProvider: UserProvider, option: JwtGuardOptions<UserProvider[typeof symbols.PROVIDER_REAL_USER]>);
8
+ constructor(ctx: HttpContext, userProvider: UserProvider, option: JwtGuardOptions<UserProvider[typeof symbols.PROVIDER_REAL_USER]> & JwtGuardInternalOptions);
8
9
  /**
9
10
  * A list of events and their types emitted by
10
11
  * the guard.
@@ -44,7 +45,14 @@ export declare class JwtGuard<UserProvider extends JwtUserProviderContract<unkno
44
45
  }>;
45
46
  generateWithRefreshToken(refreshToken?: string): Promise<JwtGenerateResult<true>>;
46
47
  /**
47
- * Revoke the refresh token
48
+ * Revoke the refresh token passed as argument, or else the one issued
49
+ * during this request, or else the one sent by the client (cookie, body
50
+ * or Authorization header).
51
+ *
52
+ * The cookies are always cleared. When no valid refresh token is found,
53
+ * nothing is revoked and `E_UNAUTHORIZED_ACCESS` is raised, so a client
54
+ * that sends the wrong token (e.g. its access token) does not believe it
55
+ * has logged out while its refresh token stays valid.
48
56
  */
49
57
  revoke(refreshToken?: string): Promise<void>;
50
58
  /**
@@ -1,8 +1,11 @@
1
1
  import { errors } from '@adonisjs/auth';
2
- import { DEFAULT_TOKEN_EXPIRES_IN, } from './types.js';
2
+ import { RuntimeException } from '@adonisjs/core/exceptions';
3
+ import { DEFAULT_REFRESH_TOKEN_EXPIRES_IN, DEFAULT_TOKEN_EXPIRES_IN } from './constants.js';
3
4
  import { Secret } from '@adonisjs/core/helpers';
4
- import { validateGuardOptions } from './validation.js';
5
+ import { releaseSecrets, toSeconds, validateGuardOptions, } from './validation.js';
5
6
  import { resolveDriver } from './driver_resolver.js';
7
+ import { installJwtUnauthorizedRenderer } from './errors.js';
8
+ installJwtUnauthorizedRenderer();
6
9
  export class JwtGuard {
7
10
  #ctx;
8
11
  #userProvider;
@@ -12,10 +15,19 @@ export class JwtGuard {
12
15
  #refreshTokenName = 'refreshToken';
13
16
  #driver;
14
17
  #cookieOptions;
18
+ /**
19
+ * Refresh token issued during the current request, by `generate()` or a
20
+ * refresh. A logout in the same request revokes it: the request still
21
+ * carries the previous one, which the refresh has already deleted.
22
+ */
23
+ #issuedRefreshToken;
15
24
  constructor(ctx, userProvider, option) {
16
25
  this.#ctx = ctx;
17
26
  this.#userProvider = userProvider;
18
- this.#options = option;
27
+ /**
28
+ * Copy the options: the defaults below must not leak into the caller's object
29
+ */
30
+ this.#options = releaseSecrets({ ...option });
19
31
  this.#refreshTokenUserProvider = this.#options.refreshTokenUserProvider;
20
32
  if (!this.#options.content)
21
33
  this.#options.content = (user) => ({ userId: user.getId() });
@@ -25,13 +37,21 @@ export class JwtGuard {
25
37
  * Never issue access tokens without an expiration
26
38
  */
27
39
  this.#options.tokenExpiresIn = this.#options.tokenExpiresIn ?? DEFAULT_TOKEN_EXPIRES_IN;
40
+ /**
41
+ * Give refresh tokens a lifetime, so the database row, the cookie and
42
+ * the response always agree
43
+ */
44
+ if (this.#refreshTokenUserProvider) {
45
+ this.#options.refreshTokenExpiresIn =
46
+ this.#options.refreshTokenExpiresIn ?? DEFAULT_REFRESH_TOKEN_EXPIRES_IN;
47
+ }
28
48
  this.#cookieOptions = {
29
49
  httpOnly: true,
30
50
  secure: true,
31
51
  ...this.#options.cookie,
32
52
  };
33
- validateGuardOptions(this.#options, 'JwtGuard');
34
- this.#driver = resolveDriver(this.#options, 'JwtGuard');
53
+ validateGuardOptions(this.#options, 'JWT guard');
54
+ this.#driver = resolveDriver(this.#options, 'JWT guard');
35
55
  }
36
56
  #signAccessToken(payload) {
37
57
  return this.#driver.sign(payload, { expiresIn: this.#options.tokenExpiresIn });
@@ -83,9 +103,7 @@ export class JwtGuard {
83
103
  */
84
104
  async generate(user) {
85
105
  if (!this.#driver.canSign) {
86
- throw new errors.E_UNAUTHORIZED_ACCESS("You can't use the auth.generate method with jwks", {
87
- guardDriverName: this.driverName,
88
- });
106
+ throw new RuntimeException('JWT guard cannot issue tokens in JWKS mode: tokens are issued by the identity provider');
89
107
  }
90
108
  const providerUser = await this.#userProvider.createUserForGuard(user);
91
109
  const signPayload = this.#options.content(providerUser);
@@ -98,6 +116,7 @@ export class JwtGuard {
98
116
  }
99
117
  : undefined);
100
118
  refreshToken = generatedRefreshToken.value.release();
119
+ this.#issuedRefreshToken = refreshToken;
101
120
  }
102
121
  if (this.#options.useCookies) {
103
122
  this.#ctx.response.cookie(`${this.#tokenName}`, token, {
@@ -114,9 +133,11 @@ export class JwtGuard {
114
133
  return {
115
134
  type: 'bearer',
116
135
  token: token,
117
- expiresIn: this.#options.tokenExpiresIn,
136
+ expiresIn: toSeconds(this.#options.tokenExpiresIn),
118
137
  refreshToken: refreshToken,
119
- refreshTokenExpiresIn: this.#options.refreshTokenExpiresIn,
138
+ refreshTokenExpiresIn: refreshToken
139
+ ? toSeconds(this.#options.refreshTokenExpiresIn)
140
+ : undefined,
120
141
  };
121
142
  }
122
143
  /**
@@ -154,7 +175,12 @@ export class JwtGuard {
154
175
  guardDriverName: this.driverName,
155
176
  });
156
177
  }
157
- if (!payload || typeof payload !== 'object') {
178
+ /**
179
+ * jsonwebtoken only checks `exp` when the claim is present: a token
180
+ * without it (issued by another service sharing the secret, or by an
181
+ * identity provider) would otherwise be valid forever
182
+ */
183
+ if (!payload || typeof payload !== 'object' || typeof payload.exp !== 'number') {
158
184
  throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
159
185
  guardDriverName: this.driverName,
160
186
  });
@@ -176,9 +202,17 @@ export class JwtGuard {
176
202
  });
177
203
  }
178
204
  }
179
- const getUserId = this.#options.getUserId ?? ((claims) => claims.userId);
180
- const userId = getUserId(payload);
181
- if (userId === undefined || userId === null) {
205
+ /**
206
+ * A `getUserId` that throws, or a claim of an unexpected type (an object
207
+ * or an array from an identity provider), rejects the token instead of
208
+ * failing the request or reaching the database
209
+ */
210
+ let userId;
211
+ try {
212
+ userId = (this.#options.getUserId ?? ((claims) => claims.userId))(payload);
213
+ }
214
+ catch { }
215
+ if (!isUserId(userId)) {
182
216
  throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
183
217
  guardDriverName: this.driverName,
184
218
  });
@@ -200,14 +234,10 @@ export class JwtGuard {
200
234
  async generateWithRefreshToken(refreshToken) {
201
235
  this.authenticationAttempted = true;
202
236
  if (!this.#driver.canSign) {
203
- throw new errors.E_UNAUTHORIZED_ACCESS('JWKS is not supported for refresh token', {
204
- guardDriverName: this.driverName,
205
- });
237
+ throw new RuntimeException('JWT guard cannot issue tokens in JWKS mode: tokens are issued by the identity provider');
206
238
  }
207
239
  if (!this.#refreshTokenUserProvider) {
208
- throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
209
- guardDriverName: this.driverName,
210
- });
240
+ throw new RuntimeException(REFRESH_TOKENS_NOT_CONFIGURED);
211
241
  }
212
242
  if (!refreshToken) {
213
243
  refreshToken = await this.#findRefreshToken();
@@ -293,33 +323,35 @@ export class JwtGuard {
293
323
  });
294
324
  }
295
325
  /**
296
- * Revoke the refresh token
326
+ * Revoke the refresh token passed as argument, or else the one issued
327
+ * during this request, or else the one sent by the client (cookie, body
328
+ * or Authorization header).
329
+ *
330
+ * The cookies are always cleared. When no valid refresh token is found,
331
+ * nothing is revoked and `E_UNAUTHORIZED_ACCESS` is raised, so a client
332
+ * that sends the wrong token (e.g. its access token) does not believe it
333
+ * has logged out while its refresh token stays valid.
297
334
  */
298
335
  async revoke(refreshToken) {
299
336
  if (!this.#refreshTokenUserProvider) {
300
- throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
301
- guardDriverName: this.driverName,
302
- });
337
+ throw new RuntimeException(REFRESH_TOKENS_NOT_CONFIGURED);
303
338
  }
304
339
  this.#clearCookies();
305
- if (!refreshToken) {
306
- try {
307
- refreshToken = await this.#findRefreshToken();
308
- }
309
- catch {
310
- return;
311
- }
312
- }
340
+ refreshToken = refreshToken ?? this.#issuedRefreshToken ?? (await this.#findRefreshToken());
313
341
  /**
314
342
  * Only delete a refresh token whose secret is valid. The tokens
315
343
  * provider deletes by id alone, so a forged token with a guessed
316
344
  * id would otherwise revoke another user's refresh token.
317
345
  */
318
346
  const secret = new Secret(refreshToken);
319
- if (!(await this.#refreshTokenUserProvider.verifyToken(secret))) {
320
- return;
347
+ if (!(await this.#refreshTokenUserProvider.verifyToken(secret)) ||
348
+ !(await this.#refreshTokenUserProvider.invalidateToken(secret))) {
349
+ throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
350
+ guardDriverName: this.driverName,
351
+ });
321
352
  }
322
- await this.#refreshTokenUserProvider.invalidateToken(secret);
353
+ if (refreshToken === this.#issuedRefreshToken)
354
+ this.#issuedRefreshToken = undefined;
323
355
  }
324
356
  /**
325
357
  * Remove the access and refresh token cookies set by the guard
@@ -340,8 +372,14 @@ export class JwtGuard {
340
372
  await this.authenticate();
341
373
  return true;
342
374
  }
343
- catch {
344
- return false;
375
+ catch (error) {
376
+ /**
377
+ * Like the official guards, only an authentication failure means
378
+ * "not logged in": a database outage must not look like a guest
379
+ */
380
+ if (error instanceof errors.E_UNAUTHORIZED_ACCESS)
381
+ return false;
382
+ throw error;
345
383
  }
346
384
  }
347
385
  /**
@@ -368,3 +406,9 @@ export class JwtGuard {
368
406
  };
369
407
  }
370
408
  }
409
+ const REFRESH_TOKENS_NOT_CONFIGURED = 'JWT guard refresh tokens are not configured: set `refreshTokenUserProvider` in the guard config';
410
+ function isUserId(value) {
411
+ return ((typeof value === 'string' && value !== '') ||
412
+ (typeof value === 'number' && Number.isFinite(value)) ||
413
+ typeof value === 'bigint');
414
+ }
@@ -1,9 +1,10 @@
1
- import { type symbols } from '@adonisjs/auth';
1
+ import type { symbols } from '@adonisjs/auth';
2
2
  import type { StringValue } from 'ms';
3
3
  import type { AccessTokensUserProviderContract } from '@adonisjs/auth/types/access_tokens';
4
4
  import type { Options } from 'jwks-rsa';
5
5
  import type { CookieOptions } from '@adonisjs/core/types/http';
6
- import type { JwtDriver } from './drivers/types.js';
6
+ import type { ALLOWED_ASYMMETRIC_ALGORITHMS, ALLOWED_JWKS_ALGORITHMS, ALLOWED_SYMMETRIC_ALGORITHMS } from './constants.js';
7
+ import type { Secret } from '@adonisjs/core/helpers';
7
8
  export type JwtCookieOptions = Omit<Partial<CookieOptions>, 'maxAge' | 'expires'>;
8
9
  /**
9
10
  * The bridge between the User provider and the
@@ -28,7 +29,7 @@ export interface JwtUserProviderContract<RealUser> {
28
29
  * A property the guard implementation can use to infer
29
30
  * the data type of the actual user (aka RealUser)
30
31
  */
31
- [symbols.PROVIDER_REAL_USER]?: RealUser;
32
+ [symbols.PROVIDER_REAL_USER]: RealUser;
32
33
  /**
33
34
  * Create a user object that acts as an adapter between
34
35
  * the guard and real user value.
@@ -42,47 +43,43 @@ export interface JwtUserProviderContract<RealUser> {
42
43
  export type BaseJwtContent = {
43
44
  userId: string | number | BigInt;
44
45
  };
45
- /**
46
- * Lifetime of access tokens when `tokenExpiresIn` is not configured
47
- */
48
- export declare const DEFAULT_TOKEN_EXPIRES_IN = "1h";
49
- export declare const ALLOWED_SYMMETRIC_ALGORITHMS: readonly ["HS256", "HS384", "HS512"];
50
46
  export type JwtSymmetricAlgorithm = (typeof ALLOWED_SYMMETRIC_ALGORITHMS)[number];
51
- export declare const ALLOWED_ASYMMETRIC_ALGORITHMS: readonly ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"];
52
47
  /**
53
48
  * Algorithms supported for asymmetric (RSA / ECDSA) JWT signing and verification.
54
49
  * Use with `privateKey` + `publicKey` so access tokens can be verified elsewhere with only the public key.
55
50
  */
56
51
  export type JwtAsymmetricAlgorithm = (typeof ALLOWED_ASYMMETRIC_ALGORITHMS)[number];
57
- export declare const ALLOWED_JWKS_ALGORITHMS: readonly ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"];
58
52
  export type JwtJwksAlgorithm = (typeof ALLOWED_JWKS_ALGORITHMS)[number];
59
53
  /**
60
- * `refreshToken` is a string when the guard has a `refreshTokenUserProvider`
54
+ * Tokens returned by `generate()` and `generateWithRefreshToken()`.
55
+ * Lifetimes are in seconds, like `expires_in` in OAuth 2.0 (RFC 6749),
56
+ * whatever the format used in the config (`'1h'` gives `3600`).
57
+ * `refreshToken` and `refreshTokenExpiresIn` are set when the guard has
58
+ * a `refreshTokenUserProvider`.
61
59
  */
62
60
  export type JwtGenerateResult<WithRefreshToken extends boolean = boolean> = {
63
- type: string;
61
+ type: 'bearer';
64
62
  token: string;
65
- expiresIn?: number | StringValue;
66
- refreshTokenExpiresIn?: number | StringValue;
63
+ expiresIn: number;
67
64
  } & ([WithRefreshToken] extends [true] ? {
68
65
  refreshToken: string;
66
+ refreshTokenExpiresIn: number;
69
67
  } : {
70
68
  refreshToken?: string;
69
+ refreshTokenExpiresIn?: number;
71
70
  });
72
- export type JwtGuardOptions<RealUser extends any = unknown> = {
73
- /**
74
- * Driver resolved by `jwtGuard()`. Not part of the public API: custom
75
- * drivers are not supported and the `JwtDriver` interface may change.
76
- */
77
- driver?: JwtDriver;
71
+ /**
72
+ * Options shared by `jwtGuard()` and the `JwtGuard` constructor
73
+ */
74
+ type JwtSharedOptions = {
78
75
  /**
79
76
  * Symmetric signing secret (HMAC). Used when asymmetric keys are not set.
80
77
  */
81
- secret?: string;
78
+ secret?: string | Secret<string>;
82
79
  /**
83
80
  * PEM-encoded private key for signing access tokens (asymmetric mode).
84
81
  */
85
- privateKey?: string;
82
+ privateKey?: string | Secret<string>;
86
83
  /**
87
84
  * PEM-encoded public key for verifying access tokens (asymmetric mode).
88
85
  */
@@ -92,7 +89,6 @@ export type JwtGuardOptions<RealUser extends any = unknown> = {
92
89
  */
93
90
  algorithm?: JwtAsymmetricAlgorithm;
94
91
  jwks?: Options;
95
- refreshTokenUserProvider?: AccessTokensUserProviderContract<RealUser>;
96
92
  tokenName?: string;
97
93
  /**
98
94
  * Name of the refresh token cookie and request body field.
@@ -112,7 +108,6 @@ export type JwtGuardOptions<RealUser extends any = unknown> = {
112
108
  */
113
109
  refreshTokenAbilities?: string[];
114
110
  cookie?: JwtCookieOptions;
115
- content?: (user: JwtGuardUser<RealUser>) => Record<string, any> & BaseJwtContent;
116
111
  /**
117
112
  * Expected JWT issuer (iss). Validated during token verification.
118
113
  * Required in JWKS mode.
@@ -132,11 +127,6 @@ export type JwtGuardOptions<RealUser extends any = unknown> = {
132
127
  * Number of seconds of clock skew tolerated when checking `exp` / `nbf`.
133
128
  */
134
129
  clockTolerance?: number;
135
- /**
136
- * Extract the user id from a verified token payload.
137
- * Defaults to `payload.userId`. Use e.g. `(payload) => payload.sub` for external providers.
138
- */
139
- getUserId?: JwtGetUserId;
140
130
  /**
141
131
  * Extra check on a verified token payload, run before looking up the user.
142
132
  * Only an explicit `true` accepts the token: any other value, or an
@@ -145,5 +135,39 @@ export type JwtGuardOptions<RealUser extends any = unknown> = {
145
135
  */
146
136
  verifyPayload?: JwtVerifyPayload;
147
137
  };
138
+ /**
139
+ * `content` builds the payload of the access tokens. Without `getUserId`,
140
+ * the guard reads the user id from the `userId` claim, so `content` must
141
+ * set it. With `getUserId`, `content` is free to use another claim.
142
+ */
143
+ export type JwtContentOptions<RealUser> = {
144
+ /**
145
+ * Extract the user id from a verified token payload.
146
+ * Defaults to `payload.userId`. Use e.g. `(payload) => payload.sub` for external providers.
147
+ */
148
+ getUserId?: undefined;
149
+ content?: (user: JwtGuardUser<RealUser>) => Record<string, any> & BaseJwtContent;
150
+ } | {
151
+ /**
152
+ * Extract the user id from a verified token payload.
153
+ * Defaults to `payload.userId`. Use e.g. `(payload) => payload.sub` for external providers.
154
+ */
155
+ getUserId: JwtGetUserId;
156
+ content?: (user: JwtGuardUser<RealUser>) => Record<string, any>;
157
+ };
158
+ /**
159
+ * Options of the `JwtGuard` constructor
160
+ */
161
+ export type JwtGuardOptions<RealUser = unknown> = JwtSharedOptions & {
162
+ refreshTokenUserProvider?: AccessTokensUserProviderContract<RealUser>;
163
+ } & JwtContentOptions<RealUser>;
164
+ /**
165
+ * Config accepted by `jwtGuard()` in `config/auth.ts`
166
+ */
167
+ export type JwtGuardConfig<UserProvider extends JwtUserProviderContract<unknown>, RefreshTokenUserProvider extends AccessTokensUserProviderContract<unknown> | undefined = undefined> = JwtSharedOptions & {
168
+ provider: UserProvider;
169
+ refreshTokenUserProvider?: RefreshTokenUserProvider;
170
+ } & JwtContentOptions<UserProvider[typeof symbols.PROVIDER_REAL_USER]>;
148
171
  export type JwtVerifyPayload = (payload: Record<string, any>) => boolean | Promise<boolean>;
149
172
  export type JwtGetUserId = (payload: Record<string, any>) => string | number | BigInt | undefined | null;
173
+ export {};
@@ -1,19 +1 @@
1
- /**
2
- * Lifetime of access tokens when `tokenExpiresIn` is not configured
3
- */
4
- export const DEFAULT_TOKEN_EXPIRES_IN = '1h';
5
- export const ALLOWED_SYMMETRIC_ALGORITHMS = ['HS256', 'HS384', 'HS512'];
6
- export const ALLOWED_ASYMMETRIC_ALGORITHMS = [
7
- 'RS256',
8
- 'RS384',
9
- 'RS512',
10
- 'ES256',
11
- 'ES384',
12
- 'ES512',
13
- ];
14
- export const ALLOWED_JWKS_ALGORITHMS = [
15
- ...ALLOWED_ASYMMETRIC_ALGORITHMS,
16
- 'PS256',
17
- 'PS384',
18
- 'PS512',
19
- ];
1
+ export {};
@@ -1,2 +1,30 @@
1
1
  import type { JwtGuardOptions } from './types.js';
2
- export declare function validateGuardOptions(options: JwtGuardOptions, prefix?: string): void;
2
+ import type { JwtGuardInternalOptions } from './drivers/types.js';
3
+ export declare function validateGuardOptions(options: ReleasedSecrets<JwtGuardOptions & JwtGuardInternalOptions>, prefix?: string): void;
4
+ /**
5
+ * Token lifetimes and clock tolerance. A lifetime that is not a positive
6
+ * duration would issue tokens that are already expired (`'-1h'`), or fail
7
+ * on every login (`'abc'`).
8
+ */
9
+ export declare function validateLifetimes(options: Pick<JwtGuardOptions, 'tokenExpiresIn' | 'refreshTokenExpiresIn' | 'clockTolerance'>, prefix: string): void;
10
+ /**
11
+ * Exactly one signing method: a secret, an asymmetric key pair, or a JWKS endpoint
12
+ */
13
+ export declare function validateSigningMethod(options: ReleasedSecrets<JwtGuardOptions>, prefix: string): void;
14
+ /**
15
+ * Accept `Secret` values (`Env.schema.secret()` in AdonisJS v7) for the
16
+ * signing keys, and work with their string value
17
+ */
18
+ export type ReleasedSecrets<Options> = Options extends any ? Omit<Options, 'secret' | 'privateKey'> & {
19
+ secret?: string;
20
+ privateKey?: string;
21
+ } : never;
22
+ export declare function releaseSecrets<Options extends {
23
+ secret?: unknown;
24
+ privateKey?: unknown;
25
+ }>(options: Options): ReleasedSecrets<Options>;
26
+ /**
27
+ * A lifetime from the config (a number of seconds or a `ms` duration such
28
+ * as `'1h'`), as a whole number of seconds. Lifetimes are validated first.
29
+ */
30
+ export declare function toSeconds(value: number | string): number;
@@ -1,11 +1,50 @@
1
- export function validateGuardOptions(options, prefix = 'JwtGuard') {
2
- if (typeof options.tokenExpiresIn === 'number' &&
3
- !(Number.isFinite(options.tokenExpiresIn) && options.tokenExpiresIn > 0)) {
4
- throw new Error(`${prefix} \`tokenExpiresIn\` must be a positive number of seconds`);
5
- }
1
+ import ms from 'ms';
2
+ export function validateGuardOptions(options, prefix = 'JWT guard') {
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;
@@ -15,10 +54,64 @@ export function validateGuardOptions(options, prefix = 'JwtGuard') {
15
54
  if (asymmetricPartial && !usesAsymmetric) {
16
55
  throw new Error(`${prefix} asymmetric mode requires \`privateKey\`, \`publicKey\`, and \`algorithm\` to be set together`);
17
56
  }
57
+ /**
58
+ * A falsy `verifyPayload` (`null`, `false`) must not count as a payload
59
+ * check: in JWKS mode it would replace the required `audience`
60
+ */
61
+ if (options.verifyPayload !== undefined && typeof options.verifyPayload !== 'function') {
62
+ throw new Error(`${prefix} \`verifyPayload\` must be a function`);
63
+ }
64
+ if (options.jwks && options.refreshTokenUserProvider) {
65
+ throw new Error(`${prefix} cannot use refresh tokens with \`jwks\`: tokens are issued by the identity provider, remove \`refreshTokenUserProvider\``);
66
+ }
67
+ if (options.secret !== undefined && typeof options.secret !== 'string') {
68
+ throw new Error(`${prefix} \`secret\` must be a string or a Secret`);
69
+ }
70
+ /**
71
+ * In JWKS mode, the driver checks them with its own message. Elsewhere,
72
+ * an empty value is signed into every token, then rejects all of them.
73
+ */
74
+ if (!options.jwks) {
75
+ if (options.issuer !== undefined && !isNonEmptyString(options.issuer)) {
76
+ throw new Error(`${prefix} \`issuer\` must be a non-empty string`);
77
+ }
78
+ if (options.audience !== undefined &&
79
+ !(Array.isArray(options.audience)
80
+ ? options.audience.length > 0 && options.audience.every(isNonEmptyString)
81
+ : isNonEmptyString(options.audience))) {
82
+ throw new Error(`${prefix} \`audience\` must be a non-empty string or array of strings`);
83
+ }
84
+ }
18
85
  if (options.jwks && usesAsymmetric) {
19
86
  throw new Error(`${prefix} cannot use \`jwks\` together with asymmetric \`privateKey\` / \`publicKey\``);
20
87
  }
88
+ if (options.jwks &&
89
+ !(typeof options.jwks.jwksUri === 'string' && options.jwks.jwksUri.trim() !== '')) {
90
+ throw new Error(`${prefix} \`jwks.jwksUri\` is required to fetch the identity provider keys`);
91
+ }
21
92
  if (!options.jwks && !usesAsymmetric && !options.secret) {
22
93
  throw new Error(`${prefix} requires \`secret\` (symmetric), or \`privateKey\` + \`publicKey\` + \`algorithm\` (asymmetric), or \`jwks\``);
23
94
  }
24
95
  }
96
+ function isNonEmptyString(value) {
97
+ return typeof value === 'string' && value.trim() !== '';
98
+ }
99
+ export function releaseSecrets(options) {
100
+ return {
101
+ ...options,
102
+ ...(options.secret !== undefined ? { secret: release(options.secret) } : {}),
103
+ ...(options.privateKey !== undefined ? { privateKey: release(options.privateKey) } : {}),
104
+ };
105
+ }
106
+ function release(value) {
107
+ return value && typeof value.release === 'function'
108
+ ? value.release()
109
+ : value;
110
+ }
111
+ /**
112
+ * A lifetime from the config (a number of seconds or a `ms` duration such
113
+ * as `'1h'`), as a whole number of seconds. Lifetimes are validated first.
114
+ */
115
+ export function toSeconds(value) {
116
+ return Math.floor(typeof value === 'number' ? value : ms(value) / 1000);
117
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@maximemrf/adonisjs-jwt",
3
3
  "description": "AdonisJS package for authentication with JWT tokens",
4
- "version": "1.0.0-rc.3",
4
+ "version": "1.0.0-rc.5",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },