@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 +25 -0
- 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 +22 -2
- package/package.json +16 -8
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
|
-
|
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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
|
-
|
|
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",
|