@maximemrf/adonisjs-jwt 1.0.0-rc.6 → 1.0.0-rc.8
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.
|
@@ -43,6 +43,7 @@ export function jwtGuard(config) {
|
|
|
43
43
|
* the first request, when the app would already be serving traffic.
|
|
44
44
|
*/
|
|
45
45
|
validateLifetimes(options, PREFIX);
|
|
46
|
+
validateSigningMethod(options, PREFIX, { usesAppKey });
|
|
46
47
|
let driver;
|
|
47
48
|
if (usesAppKey) {
|
|
48
49
|
/**
|
|
@@ -53,7 +54,6 @@ export function jwtGuard(config) {
|
|
|
53
54
|
assertAppKey(process.env.APP_KEY);
|
|
54
55
|
}
|
|
55
56
|
else {
|
|
56
|
-
validateSigningMethod(options, PREFIX);
|
|
57
57
|
driver = resolveDriver(options, PREFIX);
|
|
58
58
|
}
|
|
59
59
|
return {
|
|
@@ -27,8 +27,8 @@ export class AsymmetricDriver {
|
|
|
27
27
|
if (!expectedKeyType) {
|
|
28
28
|
throw new Error(`Unsupported asymmetric algorithm "${algorithm}"`);
|
|
29
29
|
}
|
|
30
|
-
const privateKey = createPrivateKey
|
|
31
|
-
const publicKey = createPublicKey
|
|
30
|
+
const privateKey = parseKey(createPrivateKey, this.#privateKey, 'privateKey');
|
|
31
|
+
const publicKey = parseKey(createPublicKey, this.#publicKey, 'publicKey');
|
|
32
32
|
if (privateKey.asymmetricKeyType !== expectedKeyType) {
|
|
33
33
|
throw new Error(`privateKey type "${privateKey.asymmetricKeyType}" does not match algorithm "${algorithm}"`);
|
|
34
34
|
}
|
|
@@ -42,6 +42,12 @@ export class AsymmetricDriver {
|
|
|
42
42
|
if (!createPublicKey(privateKey).equals(publicKey)) {
|
|
43
43
|
throw new Error('publicKey does not belong to the same key pair as privateKey');
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The checks above do not cover everything jsonwebtoken requires (the
|
|
47
|
+
* curve of ES256/384/512, RSA keys of at least 2048 bits). Sign a token
|
|
48
|
+
* now, so a bad key fails at boot instead of every login.
|
|
49
|
+
*/
|
|
50
|
+
jwt.sign({}, this.#privateKey, { algorithm, expiresIn: 60 });
|
|
45
51
|
}
|
|
46
52
|
sign(payload, options) {
|
|
47
53
|
const signOptions = {
|
|
@@ -62,3 +68,11 @@ export class AsymmetricDriver {
|
|
|
62
68
|
return jwt.verify(token, this.#publicKey, verifyOptions);
|
|
63
69
|
}
|
|
64
70
|
}
|
|
71
|
+
function parseKey(parse, key, name) {
|
|
72
|
+
try {
|
|
73
|
+
return parse(key);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error(`${name} is not a valid PEM key (${error.message}). If it comes from an env variable, replace the literal "\\n" with line breaks`);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/build/src/guard.d.ts
CHANGED
|
@@ -49,10 +49,12 @@ export declare class JwtGuard<UserProvider extends JwtUserProviderContract<unkno
|
|
|
49
49
|
* during this request, or else the one sent by the client (cookie, body
|
|
50
50
|
* or Authorization header).
|
|
51
51
|
*
|
|
52
|
-
* The cookies are
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* The cookies are cleared when the refresh token is revoked, and when
|
|
53
|
+
* there is no valid refresh token to revoke. In that case nothing is
|
|
54
|
+
* revoked and `E_UNAUTHORIZED_ACCESS` is raised, so a client that sends
|
|
55
|
+
* the wrong token (e.g. its access token) does not believe it has logged
|
|
56
|
+
* out while its refresh token stays valid. On any other error (e.g. the
|
|
57
|
+
* database is down), the cookies are kept so the client can retry.
|
|
56
58
|
*/
|
|
57
59
|
revoke(refreshToken?: string): Promise<void>;
|
|
58
60
|
/**
|
package/build/src/guard.js
CHANGED
|
@@ -107,6 +107,14 @@ export class JwtGuard {
|
|
|
107
107
|
}
|
|
108
108
|
const providerUser = await this.#userProvider.createUserForGuard(user);
|
|
109
109
|
const signPayload = this.#options.content(providerUser);
|
|
110
|
+
assertNoGuardClaims(signPayload, this.#options);
|
|
111
|
+
/**
|
|
112
|
+
* Without `userId`, the login succeeds but every request then gets a 401
|
|
113
|
+
* with no log: fail when issuing the token instead
|
|
114
|
+
*/
|
|
115
|
+
if (!this.#options.getUserId && !isUserId(signPayload.userId)) {
|
|
116
|
+
throw new RuntimeException('JWT guard `content` must return `userId`, or set `getUserId` to read the user id from the payload');
|
|
117
|
+
}
|
|
110
118
|
const token = this.#signAccessToken(signPayload);
|
|
111
119
|
let refreshToken;
|
|
112
120
|
if (this.#refreshTokenUserProvider) {
|
|
@@ -327,25 +335,44 @@ export class JwtGuard {
|
|
|
327
335
|
* during this request, or else the one sent by the client (cookie, body
|
|
328
336
|
* or Authorization header).
|
|
329
337
|
*
|
|
330
|
-
* The cookies are
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
338
|
+
* The cookies are cleared when the refresh token is revoked, and when
|
|
339
|
+
* there is no valid refresh token to revoke. In that case nothing is
|
|
340
|
+
* revoked and `E_UNAUTHORIZED_ACCESS` is raised, so a client that sends
|
|
341
|
+
* the wrong token (e.g. its access token) does not believe it has logged
|
|
342
|
+
* out while its refresh token stays valid. On any other error (e.g. the
|
|
343
|
+
* database is down), the cookies are kept so the client can retry.
|
|
334
344
|
*/
|
|
335
345
|
async revoke(refreshToken) {
|
|
336
346
|
if (!this.#refreshTokenUserProvider) {
|
|
337
347
|
throw new RuntimeException(REFRESH_TOKENS_NOT_CONFIGURED);
|
|
338
348
|
}
|
|
349
|
+
try {
|
|
350
|
+
await this.#revokeRefreshToken(this.#refreshTokenUserProvider, refreshToken);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
if (error instanceof errors.E_UNAUTHORIZED_ACCESS)
|
|
354
|
+
this.#clearCookies();
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
339
357
|
this.#clearCookies();
|
|
358
|
+
}
|
|
359
|
+
async #revokeRefreshToken(provider, refreshToken) {
|
|
340
360
|
refreshToken = refreshToken ?? this.#issuedRefreshToken ?? (await this.#findRefreshToken());
|
|
341
361
|
/**
|
|
342
362
|
* Only delete a refresh token whose secret is valid. The tokens
|
|
343
363
|
* provider deletes by id alone, so a forged token with a guessed
|
|
344
364
|
* id would otherwise revoke another user's refresh token.
|
|
365
|
+
*
|
|
366
|
+
* Like the refresh, require the configured abilities: with two guards
|
|
367
|
+
* that share a tokens provider, the logout of one guard must not
|
|
368
|
+
* revoke the refresh tokens of the other.
|
|
345
369
|
*/
|
|
346
370
|
const secret = new Secret(refreshToken);
|
|
347
|
-
|
|
348
|
-
|
|
371
|
+
const token = await provider.verifyToken(secret);
|
|
372
|
+
const requiredAbilities = this.#options.refreshTokenAbilities ?? [];
|
|
373
|
+
if (!token ||
|
|
374
|
+
!requiredAbilities.every((ability) => token.allows(ability)) ||
|
|
375
|
+
!(await provider.invalidateToken(secret))) {
|
|
349
376
|
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
350
377
|
guardDriverName: this.driverName,
|
|
351
378
|
});
|
|
@@ -407,6 +434,23 @@ export class JwtGuard {
|
|
|
407
434
|
}
|
|
408
435
|
}
|
|
409
436
|
const REFRESH_TOKENS_NOT_CONFIGURED = 'JWT guard refresh tokens are not configured: set `refreshTokenUserProvider` in the guard config';
|
|
437
|
+
/**
|
|
438
|
+
* The guard sets `exp`, and `iss` / `aud` when `issuer` / `audience` are
|
|
439
|
+
* configured. jsonwebtoken refuses a payload that sets them too, with a
|
|
440
|
+
* message about its own options (`options.expiresIn`) that the app never set.
|
|
441
|
+
*/
|
|
442
|
+
function assertNoGuardClaims(payload, options) {
|
|
443
|
+
const conflicts = [['exp', 'tokenExpiresIn']];
|
|
444
|
+
if (options.issuer)
|
|
445
|
+
conflicts.push(['iss', 'issuer']);
|
|
446
|
+
if (options.audience)
|
|
447
|
+
conflicts.push(['aud', 'audience']);
|
|
448
|
+
for (const [claim, option] of conflicts) {
|
|
449
|
+
if (payload[claim] !== undefined) {
|
|
450
|
+
throw new RuntimeException(`JWT guard \`content\` must not set \`${claim}\`: the guard sets it from \`${option}\``);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
410
454
|
function isUserId(value) {
|
|
411
455
|
return ((typeof value === 'string' && value !== '') ||
|
|
412
456
|
(typeof value === 'number' && Number.isFinite(value)) ||
|
package/build/src/types.d.ts
CHANGED
|
@@ -135,6 +135,13 @@ type JwtSharedOptions = {
|
|
|
135
135
|
*/
|
|
136
136
|
verifyPayload?: JwtVerifyPayload;
|
|
137
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* Claims set by the guard: `exp` comes from `tokenExpiresIn`, so `content`
|
|
140
|
+
* must not set it
|
|
141
|
+
*/
|
|
142
|
+
type GuardClaims = {
|
|
143
|
+
exp?: never;
|
|
144
|
+
};
|
|
138
145
|
/**
|
|
139
146
|
* `content` builds the payload of the access tokens. Without `getUserId`,
|
|
140
147
|
* the guard reads the user id from the `userId` claim, so `content` must
|
|
@@ -146,14 +153,14 @@ export type JwtContentOptions<RealUser> = {
|
|
|
146
153
|
* Defaults to `payload.userId`. Use e.g. `(payload) => payload.sub` for external providers.
|
|
147
154
|
*/
|
|
148
155
|
getUserId?: undefined;
|
|
149
|
-
content?: (user: JwtGuardUser<RealUser>) => Record<string, any> & BaseJwtContent;
|
|
156
|
+
content?: (user: JwtGuardUser<RealUser>) => Record<string, any> & BaseJwtContent & GuardClaims;
|
|
150
157
|
} | {
|
|
151
158
|
/**
|
|
152
159
|
* Extract the user id from a verified token payload.
|
|
153
160
|
* Defaults to `payload.userId`. Use e.g. `(payload) => payload.sub` for external providers.
|
|
154
161
|
*/
|
|
155
162
|
getUserId: JwtGetUserId;
|
|
156
|
-
content?: (user: JwtGuardUser<RealUser>) => Record<string, any
|
|
163
|
+
content?: (user: JwtGuardUser<RealUser>) => Record<string, any> & GuardClaims;
|
|
157
164
|
};
|
|
158
165
|
/**
|
|
159
166
|
* Options of the `JwtGuard` constructor
|
|
@@ -10,7 +10,9 @@ export declare function validateLifetimes(options: Pick<JwtGuardOptions, 'tokenE
|
|
|
10
10
|
/**
|
|
11
11
|
* Exactly one signing method: a secret, an asymmetric key pair, or a JWKS endpoint
|
|
12
12
|
*/
|
|
13
|
-
export declare function validateSigningMethod(options: ReleasedSecrets<JwtGuardOptions>, prefix: string
|
|
13
|
+
export declare function validateSigningMethod(options: ReleasedSecrets<JwtGuardOptions>, prefix: string, { usesAppKey }?: {
|
|
14
|
+
usesAppKey?: boolean;
|
|
15
|
+
}): void;
|
|
14
16
|
/**
|
|
15
17
|
* Accept `Secret` values (`Env.schema.secret()` in AdonisJS v7) for the
|
|
16
18
|
* signing keys, and work with their string value
|
package/build/src/validation.js
CHANGED
|
@@ -44,7 +44,7 @@ function assertDuration(value, name, prefix) {
|
|
|
44
44
|
/**
|
|
45
45
|
* Exactly one signing method: a secret, an asymmetric key pair, or a JWKS endpoint
|
|
46
46
|
*/
|
|
47
|
-
export function validateSigningMethod(options, prefix) {
|
|
47
|
+
export function validateSigningMethod(options, prefix, { usesAppKey = false } = {}) {
|
|
48
48
|
const asymmetricPartial = options.privateKey !== undefined ||
|
|
49
49
|
options.publicKey !== undefined ||
|
|
50
50
|
options.algorithm !== undefined;
|
|
@@ -89,7 +89,11 @@ export function validateSigningMethod(options, prefix) {
|
|
|
89
89
|
!(typeof options.jwks.jwksUri === 'string' && options.jwks.jwksUri.trim() !== '')) {
|
|
90
90
|
throw new Error(`${prefix} \`jwks.jwksUri\` is required to fetch the identity provider keys`);
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Without a signing method, `jwtGuard()` derives the secret from the app
|
|
94
|
+
* key in the resolver
|
|
95
|
+
*/
|
|
96
|
+
if (!usesAppKey && !options.jwks && !usesAsymmetric && !options.secret) {
|
|
93
97
|
throw new Error(`${prefix} requires \`secret\` (symmetric), or \`privateKey\` + \`publicKey\` + \`algorithm\` (asymmetric), or \`jwks\``);
|
|
94
98
|
}
|
|
95
99
|
}
|