@maximemrf/adonisjs-jwt 0.8.0-alpha.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,10 +7,19 @@
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.
13
15
 
16
+ ## Compatibility & Versions
17
+
18
+ | Package Version | AdonisJS Version | Node.js Required |
19
+ | --- | --- | --- |
20
+ | `v0.7.x` | `AdonisJS v6` | `>= 20.6.0` |
21
+ | **`v0.8.x`** | **`AdonisJS v7`** | **`>= 24.0.0`** |
22
+
14
23
  ## Prerequisites
15
24
 
16
25
  You have to install the auth package from AdonisJS
@@ -19,7 +28,7 @@ You have to install the auth package from AdonisJS
19
28
  node ace add @adonisjs/auth
20
29
  ```
21
30
 
22
- ## Setup
31
+ ## Setup (AdonisJS v7)
23
32
 
24
33
  Install the package:
25
34
 
@@ -27,6 +36,14 @@ Install the package:
27
36
  npm i @maximemrf/adonisjs-jwt
28
37
  ```
29
38
 
39
+ ## Setup for AdonisJS v6
40
+
41
+ If you are using AdonisJS v6, you have to install the `v0.7.x` version of the package:
42
+
43
+ ```bash
44
+ npm i @maximemrf/adonisjs-jwt@0.7.1
45
+ ```
46
+
30
47
  ## Usage
31
48
 
32
49
  Go to `config/auth.ts` and add the following configuration:
@@ -134,6 +151,29 @@ cookie: {
134
151
  }
135
152
  ```
136
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
+
137
177
  ## JWKS
138
178
 
139
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', {
@@ -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-alpha.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",