@maximemrf/adonisjs-jwt 0.8.0 → 0.9.0

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,6 +7,8 @@
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
+ <a href="https://adonisjs.com/"><img src="https://img.shields.io/badge/AdonisJS-v7-5A45FF.svg?style=flat-square" alt="AdonisV7"></a>
10
12
  </p>
11
13
 
12
14
  > AdonisJS package to authenticate users using JWT tokens.
@@ -149,6 +151,29 @@ cookie: {
149
151
  }
150
152
  ```
151
153
 
154
+ ## Asymmetric signing (RSA / ECDSA)
155
+
156
+ You can sign access tokens with a **private key** and verify them with a **public key** only. Other services can validate JWTs using the public key without receiving your signing secret.
157
+
158
+ Set `privateKey`, `publicKey`, and `algorithm` together on `jwtGuard`. Supported algorithms: `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`.
159
+
160
+ Do not set `secret` for this mode (the guard resolver omits it when asymmetric keys are configured). Asymmetric signing cannot be combined with `jwks`.
161
+
162
+ ```typescript
163
+ import env from '#start/env'
164
+
165
+ jwt: jwtGuard({
166
+ tokenExpiresIn: '15m',
167
+ privateKey: env.get('JWT_PRIVATE_KEY').replace(/\\n/g, '\n'),
168
+ publicKey: env.get('JWT_PUBLIC_KEY').replace(/\\n/g, '\n'),
169
+ algorithm: 'RS256',
170
+ content: (user) => ({ userId: user.getId() }),
171
+ provider: sessionUserProvider({
172
+ model: () => import('#models/user'),
173
+ }),
174
+ }),
175
+ ```
176
+
152
177
  ## JWKS
153
178
 
154
179
  You can use JWKS to verify the token by setting the `jwks` option in the guard configuration.
@@ -1,6 +1,6 @@
1
1
  import type { GuardConfigProvider } from '@adonisjs/auth/types';
2
2
  import type { HttpContext } from '@adonisjs/core/http';
3
- import type { JwtGuardUser, JwtUserProviderContract, JwtCookieOptions } from './types.js';
3
+ import type { JwtAsymmetricAlgorithm, JwtGuardUser, JwtUserProviderContract, JwtCookieOptions } from './types.js';
4
4
  import { JwtGuard } from './jwt.js';
5
5
  import type { StringValue } from 'ms';
6
6
  import type { AccessTokensUserProviderContract } from '@adonisjs/auth/types/access_tokens';
@@ -15,6 +15,9 @@ export declare function jwtGuard<UserProvider extends JwtUserProviderContract<un
15
15
  useCookiesForRefreshToken?: boolean;
16
16
  refreshTokenAbilities?: string[];
17
17
  secret?: string;
18
+ privateKey?: string;
19
+ publicKey?: string;
20
+ algorithm?: JwtAsymmetricAlgorithm;
18
21
  content: <T>(user: JwtGuardUser<T>) => Record<string | number, any>;
19
22
  jwks?: Options;
20
23
  cookie?: JwtCookieOptions;
@@ -1,10 +1,59 @@
1
1
  import { JwtGuard } from './jwt.js';
2
+ import { createPrivateKey, createPublicKey } from 'node:crypto';
3
+ function validateAsymmetricConfig(config) {
4
+ const partial = config.privateKey !== undefined ||
5
+ config.publicKey !== undefined ||
6
+ config.algorithm !== undefined;
7
+ if (!partial) {
8
+ return;
9
+ }
10
+ if (!config.privateKey || !config.publicKey || !config.algorithm) {
11
+ throw new Error('JWT guard asymmetric mode requires `privateKey`, `publicKey`, and `algorithm` to be set together');
12
+ }
13
+ if (config.jwks) {
14
+ throw new Error('JWT guard cannot use `jwks` together with asymmetric `privateKey` / `publicKey`');
15
+ }
16
+ const expectedKeyType = config.algorithm.startsWith('RS')
17
+ ? 'rsa'
18
+ : config.algorithm.startsWith('ES')
19
+ ? 'ec'
20
+ : null;
21
+ if (!expectedKeyType) {
22
+ throw new Error(`Unsupported asymmetric algorithm "${config.algorithm}"`);
23
+ }
24
+ try {
25
+ const privateKey = createPrivateKey(config.privateKey);
26
+ const publicKey = createPublicKey(config.publicKey);
27
+ if (privateKey.asymmetricKeyType !== expectedKeyType) {
28
+ throw new Error(`privateKey type "${privateKey.asymmetricKeyType}" does not match algorithm "${config.algorithm}"`);
29
+ }
30
+ if (publicKey.asymmetricKeyType !== expectedKeyType) {
31
+ throw new Error(`publicKey type "${publicKey.asymmetricKeyType}" does not match algorithm "${config.algorithm}"`);
32
+ }
33
+ }
34
+ catch (error) {
35
+ const message = error instanceof Error ? error.message : 'Invalid asymmetric key configuration';
36
+ throw new Error(`JWT guard asymmetric key validation failed: ${message}`);
37
+ }
38
+ }
2
39
  export function jwtGuard(config) {
3
40
  return {
4
41
  async resolver(_, app) {
42
+ validateAsymmetricConfig(config);
5
43
  const appKey = app.config.get('app.appKey').release();
44
+ const usesAsymmetric = config.privateKey !== undefined &&
45
+ config.publicKey !== undefined &&
46
+ config.algorithm !== undefined;
6
47
  const options = {
7
- secret: config.secret ?? appKey,
48
+ ...(usesAsymmetric
49
+ ? {
50
+ privateKey: config.privateKey,
51
+ publicKey: config.publicKey,
52
+ algorithm: config.algorithm,
53
+ }
54
+ : {
55
+ secret: config.secret ?? appKey,
56
+ }),
8
57
  refreshTokenUserProvider: config.refreshTokenUserProvider,
9
58
  tokenName: config.tokenName,
10
59
  expiresIn: config.tokenExpiresIn,
package/build/src/jwt.js CHANGED
@@ -2,6 +2,7 @@ import { errors } from '@adonisjs/auth';
2
2
  import jwt from 'jsonwebtoken';
3
3
  import { Secret } from '@adonisjs/core/helpers';
4
4
  import { JwksManager } from './jwks.js';
5
+ import { createPrivateKey, createPublicKey } from 'node:crypto';
5
6
  export class JwtGuard {
6
7
  #ctx;
7
8
  #userProvider;
@@ -25,6 +26,79 @@ export class JwtGuard {
25
26
  secure: true,
26
27
  ...this.#options.cookie,
27
28
  };
29
+ const asymmetricPartial = this.#options.privateKey !== undefined ||
30
+ this.#options.publicKey !== undefined ||
31
+ this.#options.algorithm !== undefined;
32
+ if (asymmetricPartial && !this.#usesAsymmetric()) {
33
+ throw new Error('JwtGuard asymmetric mode requires `privateKey`, `publicKey`, and `algorithm` to be set together');
34
+ }
35
+ if (this.#options.jwks && this.#usesAsymmetric()) {
36
+ throw new Error('JwtGuard cannot use `jwks` together with asymmetric `privateKey` / `publicKey`');
37
+ }
38
+ if (!this.#options.jwks && !this.#usesAsymmetric() && !this.#options.secret) {
39
+ throw new Error('JwtGuard requires `secret` (symmetric), or `privateKey` + `publicKey` + `algorithm` (asymmetric), or `jwks`');
40
+ }
41
+ if (this.#usesAsymmetric()) {
42
+ this.#assertAsymmetricKeyMatchesAlgorithm();
43
+ }
44
+ }
45
+ #usesAsymmetric() {
46
+ return (this.#options.privateKey !== undefined &&
47
+ this.#options.publicKey !== undefined &&
48
+ this.#options.algorithm !== undefined);
49
+ }
50
+ #assertAsymmetricKeyMatchesAlgorithm() {
51
+ const algorithm = this.#options.algorithm;
52
+ const expectedKeyType = algorithm.startsWith('RS')
53
+ ? 'rsa'
54
+ : algorithm.startsWith('ES')
55
+ ? 'ec'
56
+ : null;
57
+ if (!expectedKeyType) {
58
+ throw new Error(`Unsupported asymmetric algorithm "${algorithm}"`);
59
+ }
60
+ try {
61
+ const privateKey = createPrivateKey(this.#options.privateKey);
62
+ const publicKey = createPublicKey(this.#options.publicKey);
63
+ if (privateKey.asymmetricKeyType !== expectedKeyType) {
64
+ throw new Error(`privateKey type "${privateKey.asymmetricKeyType}" does not match algorithm "${algorithm}"`);
65
+ }
66
+ if (publicKey.asymmetricKeyType !== expectedKeyType) {
67
+ throw new Error(`publicKey type "${publicKey.asymmetricKeyType}" does not match algorithm "${algorithm}"`);
68
+ }
69
+ }
70
+ catch (error) {
71
+ const message = error instanceof Error ? error.message : 'Invalid asymmetric key configuration';
72
+ throw new Error(`JwtGuard asymmetric key validation failed: ${message}`);
73
+ }
74
+ }
75
+ #signAccessToken(payload) {
76
+ const expires = this.#options.expiresIn ? { expiresIn: this.#options.expiresIn } : {};
77
+ if (this.#usesAsymmetric()) {
78
+ return jwt.sign(payload, this.#options.privateKey, {
79
+ ...expires,
80
+ algorithm: this.#options.algorithm,
81
+ });
82
+ }
83
+ return jwt.sign(payload, this.#options.secret, expires);
84
+ }
85
+ async #verifyAccessToken(token) {
86
+ if (this.#jwksManager) {
87
+ const decoded = jwt.decode(token, { complete: true });
88
+ if (!decoded || !decoded.header || !decoded.header.kid) {
89
+ throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
90
+ guardDriverName: this.driverName,
91
+ });
92
+ }
93
+ const key = await this.#jwksManager.getSigningKey(decoded.header.kid);
94
+ return jwt.verify(token, key);
95
+ }
96
+ if (this.#usesAsymmetric()) {
97
+ return jwt.verify(token, this.#options.publicKey, {
98
+ algorithms: [this.#options.algorithm],
99
+ });
100
+ }
101
+ return jwt.verify(token, this.#options.secret);
28
102
  }
29
103
  /**
30
104
  * A unique name for the guard driver
@@ -54,11 +128,8 @@ export class JwtGuard {
54
128
  });
55
129
  }
56
130
  const providerUser = await this.#userProvider.createUserForGuard(user);
57
- const token = jwt.sign(this.#options.content(providerUser), this.#options.secret, this.#options.expiresIn
58
- ? {
59
- expiresIn: this.#options.expiresIn,
60
- }
61
- : {});
131
+ const signPayload = this.#options.content(providerUser);
132
+ const token = this.#signAccessToken(signPayload);
62
133
  let refreshToken;
63
134
  if (this.#refreshTokenUserProvider) {
64
135
  const generatedRefreshToken = await this.#refreshTokenUserProvider.createToken(user, this.#options.refreshTokenAbilities ?? [], this.#options.refreshTokenExpiresIn
@@ -135,19 +206,7 @@ export class JwtGuard {
135
206
  */
136
207
  let payload;
137
208
  try {
138
- if (this.#jwksManager) {
139
- const decoded = jwt.decode(token, { complete: true });
140
- if (!decoded || !decoded.header || !decoded.header.kid) {
141
- throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
142
- guardDriverName: this.driverName,
143
- });
144
- }
145
- const key = await this.#jwksManager.getSigningKey(decoded.header.kid);
146
- payload = jwt.verify(token, key);
147
- }
148
- else {
149
- payload = jwt.verify(token, this.#options.secret);
150
- }
209
+ payload = await this.#verifyAccessToken(token);
151
210
  }
152
211
  catch {
153
212
  throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
@@ -1,4 +1,4 @@
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';
@@ -41,8 +41,28 @@ export interface JwtUserProviderContract<RealUser> {
41
41
  export type BaseJwtContent = {
42
42
  userId: string | number | BigInt;
43
43
  };
44
+ /**
45
+ * Algorithms supported for asymmetric (RSA / ECDSA) JWT signing and verification.
46
+ * Use with `privateKey` + `publicKey` so access tokens can be verified elsewhere with only the public key.
47
+ */
48
+ export type JwtAsymmetricAlgorithm = 'RS256' | 'RS384' | 'RS512' | 'ES256' | 'ES384' | 'ES512';
44
49
  export type JwtGuardOptions<RealUser extends any = unknown> = {
45
- secret: string;
50
+ /**
51
+ * Symmetric signing secret (HMAC). Used when asymmetric keys are not set.
52
+ */
53
+ secret?: string;
54
+ /**
55
+ * PEM-encoded private key for signing access tokens (asymmetric mode).
56
+ */
57
+ privateKey?: string;
58
+ /**
59
+ * PEM-encoded public key for verifying access tokens (asymmetric mode).
60
+ */
61
+ publicKey?: string;
62
+ /**
63
+ * Required with `privateKey` and `publicKey`.
64
+ */
65
+ algorithm?: JwtAsymmetricAlgorithm;
46
66
  jwks?: Options;
47
67
  refreshTokenUserProvider?: AccessTokensUserProviderContract<RealUser>;
48
68
  tokenName?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@maximemrf/adonisjs-jwt",
3
3
  "description": "Adonisjs v6 package for authentication with jwt token",
4
- "version": "0.8.0",
4
+ "version": "0.9.0",
5
5
  "engines": {
6
6
  "node": ">=20.6.0"
7
7
  },
@@ -23,6 +23,7 @@
23
23
  "#database/*": "./database/*.js"
24
24
  },
25
25
  "scripts": {
26
+ "prepare": "tsc && npm run copy:templates",
26
27
  "clean": "del-cli build",
27
28
  "copy:templates": "copyfiles \"stubs/**/*.stub\" build",
28
29
  "typecheck": "tsc --noEmit",
@@ -39,11 +40,11 @@
39
40
  "prepublishOnly": "npm run build"
40
41
  },
41
42
  "devDependencies": {
42
- "@adonisjs/assembler": "^8.0.0",
43
+ "@adonisjs/assembler": "^8.2.0",
43
44
  "@adonisjs/auth": "^10.0.0",
44
- "@adonisjs/core": "^7.0.1",
45
+ "@adonisjs/core": "^7.1.1",
45
46
  "@adonisjs/eslint-config": "^3.0.0",
46
- "@adonisjs/lucid": "^22.0.0",
47
+ "@adonisjs/lucid": "^22.2.0",
47
48
  "@adonisjs/prettier-config": "^1.4.5",
48
49
  "@adonisjs/tsconfig": "^2.0.0",
49
50
  "@japa/assert": "^4.2.0",
@@ -52,15 +53,15 @@
52
53
  "@poppinss/ts-exec": "^1.4.4",
53
54
  "@types/nock": "^11.1.0",
54
55
  "@types/node": "^22.15.18",
55
- "c8": "^10.1.3",
56
+ "c8": "^11.0.0",
56
57
  "copyfiles": "^2.4.1",
57
58
  "del-cli": "^7.0.0",
58
- "eslint": "^9.39.3",
59
+ "eslint": "^9.39.4",
59
60
  "luxon": "^3.7.2",
60
61
  "nock": "^14.0.11",
61
62
  "np": "^11.0.2",
62
63
  "prettier": "^3.8.1",
63
- "sqlite3": "^5.1.7",
64
+ "sqlite3": "^6.0.1",
64
65
  "timekeeper": "^2.3.1",
65
66
  "typescript": "~5.9.3",
66
67
  "youch": "^4.1.0"
@@ -68,7 +69,7 @@
68
69
  "dependencies": {
69
70
  "@types/jsonwebtoken": "^9.0.10",
70
71
  "jsonwebtoken": "^9.0.3",
71
- "jwks-rsa": "^3.2.2",
72
+ "jwks-rsa": "^4.0.1",
72
73
  "ms": "^2.1.3"
73
74
  },
74
75
  "peerDependencies": {
@@ -76,6 +77,13 @@
76
77
  },
77
78
  "author": "MaximeMRF",
78
79
  "license": "MIT",
80
+ "repository": {
81
+ "type": "git",
82
+ "url": "git+https://github.com/MaximeMRF/adonisjs-jwt.git"
83
+ },
84
+ "bugs": {
85
+ "url": "https://github.com/MaximeMRF/adonisjs-jwt/issues"
86
+ },
79
87
  "keywords": [
80
88
  "adonisjs",
81
89
  "adonis",