@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 +0 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/src/constants.d.ts +11 -0
- package/build/src/constants.js +23 -0
- package/build/src/define_config.d.ts +2 -28
- package/build/src/define_config.js +7 -3
- package/build/src/driver_resolver.js +1 -1
- package/build/src/drivers/jwks.d.ts +1 -1
- package/build/src/drivers/jwks.js +3 -4
- package/build/src/drivers/symmetric.js +3 -3
- package/build/src/drivers/types.d.ts +11 -0
- package/build/src/guard.d.ts +3 -2
- package/build/src/guard.js +56 -25
- package/build/src/types.d.ts +53 -29
- package/build/src/types.js +1 -19
- package/build/src/validation.d.ts +20 -2
- package/build/src/validation.js +51 -1
- package/package.json +1 -1
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
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/
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 '../
|
|
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
|
|
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 '../
|
|
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('
|
|
11
|
+
throw new Error('JWT guard requires a `secret`');
|
|
12
12
|
}
|
|
13
13
|
if (options.secret.length < 32) {
|
|
14
|
-
throw new Error('
|
|
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
|
+
};
|
package/build/src/guard.d.ts
CHANGED
|
@@ -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 {
|
|
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.
|
package/build/src/guard.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { errors } from '@adonisjs/auth';
|
|
2
|
-
import {
|
|
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
|
-
|
|
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, '
|
|
42
|
-
this.#driver = resolveDriver(this.#options, '
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/build/src/types.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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]
|
|
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
|
-
*
|
|
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:
|
|
61
|
+
type: 'bearer';
|
|
64
62
|
token: string;
|
|
65
|
-
expiresIn
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 {};
|
package/build/src/types.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|
package/build/src/validation.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ms from 'ms';
|
|
2
|
-
export function validateGuardOptions(options, prefix = '
|
|
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
|
+
}
|