@maximemrf/adonisjs-jwt 1.0.0-rc.7 → 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.
@@ -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(this.#privateKey);
31
- const publicKey = createPublicKey(this.#publicKey);
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
+ }
@@ -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 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.
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
  /**
@@ -108,6 +108,13 @@ export class JwtGuard {
108
108
  const providerUser = await this.#userProvider.createUserForGuard(user);
109
109
  const signPayload = this.#options.content(providerUser);
110
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
+ }
111
118
  const token = this.#signAccessToken(signPayload);
112
119
  let refreshToken;
113
120
  if (this.#refreshTokenUserProvider) {
@@ -328,25 +335,44 @@ export class JwtGuard {
328
335
  * during this request, or else the one sent by the client (cookie, body
329
336
  * or Authorization header).
330
337
  *
331
- * The cookies are always cleared. When no valid refresh token is found,
332
- * nothing is revoked and `E_UNAUTHORIZED_ACCESS` is raised, so a client
333
- * that sends the wrong token (e.g. its access token) does not believe it
334
- * has logged out while its refresh token stays valid.
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.
335
344
  */
336
345
  async revoke(refreshToken) {
337
346
  if (!this.#refreshTokenUserProvider) {
338
347
  throw new RuntimeException(REFRESH_TOKENS_NOT_CONFIGURED);
339
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
+ }
340
357
  this.#clearCookies();
358
+ }
359
+ async #revokeRefreshToken(provider, refreshToken) {
341
360
  refreshToken = refreshToken ?? this.#issuedRefreshToken ?? (await this.#findRefreshToken());
342
361
  /**
343
362
  * Only delete a refresh token whose secret is valid. The tokens
344
363
  * provider deletes by id alone, so a forged token with a guessed
345
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.
346
369
  */
347
370
  const secret = new Secret(refreshToken);
348
- if (!(await this.#refreshTokenUserProvider.verifyToken(secret)) ||
349
- !(await this.#refreshTokenUserProvider.invalidateToken(secret))) {
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))) {
350
376
  throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
351
377
  guardDriverName: this.driverName,
352
378
  });
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.7",
4
+ "version": "1.0.0-rc.8",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },