@maximemrf/adonisjs-jwt 1.0.0-rc.4 → 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,5 +1,5 @@
1
1
  import { JwtGuard } from './guard.js';
2
- import { validateLifetimes, validateSigningMethod } 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) {
@@ -13,7 +13,7 @@ export function jwtGuard(config) {
13
13
  * Only fall back on the app key when no other signing method is configured
14
14
  */
15
15
  const usesAppKey = config.secret === undefined && !usesAsymmetric && !config.jwks;
16
- const options = {
16
+ const options = releaseSecrets({
17
17
  ...(usesAsymmetric ? asymmetricKeys : { secret: config.secret }),
18
18
  refreshTokenUserProvider: config.refreshTokenUserProvider,
19
19
  tokenName: config.tokenName,
@@ -32,7 +32,11 @@ export function jwtGuard(config) {
32
32
  clockTolerance: config.clockTolerance,
33
33
  getUserId: config.getUserId,
34
34
  verifyPayload: config.verifyPayload,
35
- };
35
+ /**
36
+ * `content` and `getUserId` come from the same config, so they keep
37
+ * the pairing checked by `JwtContentOptions`
38
+ */
39
+ });
36
40
  /**
37
41
  * Validate the configuration when config/auth.ts is loaded, so a bad
38
42
  * configuration stops the app from booting. The resolver only runs on
@@ -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) {
@@ -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
+ };
@@ -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.
@@ -1,7 +1,8 @@
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';
6
7
  import { installJwtUnauthorizedRenderer } from './errors.js';
7
8
  installJwtUnauthorizedRenderer();
@@ -23,7 +24,10 @@ export class JwtGuard {
23
24
  constructor(ctx, userProvider, option) {
24
25
  this.#ctx = ctx;
25
26
  this.#userProvider = userProvider;
26
- 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 });
27
31
  this.#refreshTokenUserProvider = this.#options.refreshTokenUserProvider;
28
32
  if (!this.#options.content)
29
33
  this.#options.content = (user) => ({ userId: user.getId() });
@@ -33,13 +37,21 @@ export class JwtGuard {
33
37
  * Never issue access tokens without an expiration
34
38
  */
35
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
+ }
36
48
  this.#cookieOptions = {
37
49
  httpOnly: true,
38
50
  secure: true,
39
51
  ...this.#options.cookie,
40
52
  };
41
- validateGuardOptions(this.#options, 'JwtGuard');
42
- this.#driver = resolveDriver(this.#options, 'JwtGuard');
53
+ validateGuardOptions(this.#options, 'JWT guard');
54
+ this.#driver = resolveDriver(this.#options, 'JWT guard');
43
55
  }
44
56
  #signAccessToken(payload) {
45
57
  return this.#driver.sign(payload, { expiresIn: this.#options.tokenExpiresIn });
@@ -91,9 +103,7 @@ export class JwtGuard {
91
103
  */
92
104
  async generate(user) {
93
105
  if (!this.#driver.canSign) {
94
- throw new errors.E_UNAUTHORIZED_ACCESS("You can't use the auth.generate method with jwks", {
95
- guardDriverName: this.driverName,
96
- });
106
+ throw new RuntimeException('JWT guard cannot issue tokens in JWKS mode: tokens are issued by the identity provider');
97
107
  }
98
108
  const providerUser = await this.#userProvider.createUserForGuard(user);
99
109
  const signPayload = this.#options.content(providerUser);
@@ -123,9 +133,11 @@ export class JwtGuard {
123
133
  return {
124
134
  type: 'bearer',
125
135
  token: token,
126
- expiresIn: this.#options.tokenExpiresIn,
136
+ expiresIn: toSeconds(this.#options.tokenExpiresIn),
127
137
  refreshToken: refreshToken,
128
- refreshTokenExpiresIn: this.#options.refreshTokenExpiresIn,
138
+ refreshTokenExpiresIn: refreshToken
139
+ ? toSeconds(this.#options.refreshTokenExpiresIn)
140
+ : undefined,
129
141
  };
130
142
  }
131
143
  /**
@@ -163,7 +175,12 @@ export class JwtGuard {
163
175
  guardDriverName: this.driverName,
164
176
  });
165
177
  }
166
- 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') {
167
184
  throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
168
185
  guardDriverName: this.driverName,
169
186
  });
@@ -185,9 +202,17 @@ export class JwtGuard {
185
202
  });
186
203
  }
187
204
  }
188
- const getUserId = this.#options.getUserId ?? ((claims) => claims.userId);
189
- const userId = getUserId(payload);
190
- 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)) {
191
216
  throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
192
217
  guardDriverName: this.driverName,
193
218
  });
@@ -209,14 +234,10 @@ export class JwtGuard {
209
234
  async generateWithRefreshToken(refreshToken) {
210
235
  this.authenticationAttempted = true;
211
236
  if (!this.#driver.canSign) {
212
- throw new errors.E_UNAUTHORIZED_ACCESS('JWKS is not supported for refresh token', {
213
- guardDriverName: this.driverName,
214
- });
237
+ throw new RuntimeException('JWT guard cannot issue tokens in JWKS mode: tokens are issued by the identity provider');
215
238
  }
216
239
  if (!this.#refreshTokenUserProvider) {
217
- throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
218
- guardDriverName: this.driverName,
219
- });
240
+ throw new RuntimeException(REFRESH_TOKENS_NOT_CONFIGURED);
220
241
  }
221
242
  if (!refreshToken) {
222
243
  refreshToken = await this.#findRefreshToken();
@@ -313,9 +334,7 @@ export class JwtGuard {
313
334
  */
314
335
  async revoke(refreshToken) {
315
336
  if (!this.#refreshTokenUserProvider) {
316
- throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
317
- guardDriverName: this.driverName,
318
- });
337
+ throw new RuntimeException(REFRESH_TOKENS_NOT_CONFIGURED);
319
338
  }
320
339
  this.#clearCookies();
321
340
  refreshToken = refreshToken ?? this.#issuedRefreshToken ?? (await this.#findRefreshToken());
@@ -353,8 +372,14 @@ export class JwtGuard {
353
372
  await this.authenticate();
354
373
  return true;
355
374
  }
356
- catch {
357
- 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;
358
383
  }
359
384
  }
360
385
  /**
@@ -381,3 +406,9 @@ export class JwtGuard {
381
406
  };
382
407
  }
383
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,5 +1,6 @@
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;
3
4
  /**
4
5
  * Token lifetimes and clock tolerance. A lifetime that is not a positive
5
6
  * duration would issue tokens that are already expired (`'-1h'`), or fail
@@ -9,4 +10,21 @@ export declare function validateLifetimes(options: Pick<JwtGuardOptions, 'tokenE
9
10
  /**
10
11
  * Exactly one signing method: a secret, an asymmetric key pair, or a JWKS endpoint
11
12
  */
12
- export declare function validateSigningMethod(options: JwtGuardOptions, prefix: string): void;
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,5 +1,5 @@
1
1
  import ms from 'ms';
2
- export function validateGuardOptions(options, prefix = 'JwtGuard') {
2
+ export function validateGuardOptions(options, prefix = 'JWT guard') {
3
3
  validateLifetimes(options, prefix);
4
4
  if (options.driver) {
5
5
  return;
@@ -54,6 +54,34 @@ export function validateSigningMethod(options, prefix) {
54
54
  if (asymmetricPartial && !usesAsymmetric) {
55
55
  throw new Error(`${prefix} asymmetric mode requires \`privateKey\`, \`publicKey\`, and \`algorithm\` to be set together`);
56
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
+ }
57
85
  if (options.jwks && usesAsymmetric) {
58
86
  throw new Error(`${prefix} cannot use \`jwks\` together with asymmetric \`privateKey\` / \`publicKey\``);
59
87
  }
@@ -65,3 +93,25 @@ export function validateSigningMethod(options, prefix) {
65
93
  throw new Error(`${prefix} requires \`secret\` (symmetric), or \`privateKey\` + \`publicKey\` + \`algorithm\` (asymmetric), or \`jwks\``);
66
94
  }
67
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.4",
4
+ "version": "1.0.0-rc.5",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },