@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 +41 -1
- package/build/src/define_config.d.ts +4 -1
- package/build/src/define_config.js +50 -1
- package/build/src/jwt.js +77 -18
- package/build/src/types.d.ts +21 -1
- package/package.json +16 -8
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
|
-
|
|
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
|
|
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
|
-
|
|
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', {
|
package/build/src/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
43
|
+
"@adonisjs/assembler": "^8.2.0",
|
|
43
44
|
"@adonisjs/auth": "^10.0.0",
|
|
44
|
-
"@adonisjs/core": "^7.
|
|
45
|
+
"@adonisjs/core": "^7.1.1",
|
|
45
46
|
"@adonisjs/eslint-config": "^3.0.0",
|
|
46
|
-
"@adonisjs/lucid": "^22.
|
|
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": "^
|
|
56
|
+
"c8": "^11.0.0",
|
|
56
57
|
"copyfiles": "^2.4.1",
|
|
57
58
|
"del-cli": "^7.0.0",
|
|
58
|
-
"eslint": "^9.39.
|
|
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": "^
|
|
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": "^
|
|
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",
|