@maximemrf/adonisjs-jwt 0.9.0 → 1.0.0-rc.2
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 +219 -23
- package/build/configure.d.ts +2 -0
- package/build/configure.js +246 -0
- package/build/index.d.ts +3 -0
- package/build/index.js +3 -0
- package/build/src/define_config.d.ts +14 -6
- package/build/src/define_config.js +51 -55
- package/build/src/driver_resolver.d.ts +16 -0
- package/build/src/driver_resolver.js +46 -0
- package/build/src/drivers/asymmetric.d.ts +19 -0
- package/build/src/drivers/asymmetric.js +57 -0
- package/build/src/drivers/jwks.d.ts +30 -0
- package/build/src/drivers/jwks.js +68 -0
- package/build/src/drivers/symmetric.d.ts +16 -0
- package/build/src/drivers/symmetric.js +38 -0
- package/build/src/drivers/types.d.ts +8 -0
- package/build/src/drivers/types.js +1 -0
- package/build/src/{jwt.d.ts → guard.d.ts} +4 -17
- package/build/src/{jwt.js → guard.js} +102 -109
- package/build/src/types.d.ts +75 -3
- package/build/src/types.js +19 -1
- package/build/src/validation.d.ts +2 -0
- package/build/src/validation.js +24 -0
- package/build/stubs/main.d.ts +4 -0
- package/build/stubs/main.js +5 -0
- package/build/stubs/migration.stub +35 -0
- package/package.json +27 -26
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { errors } from '@adonisjs/auth';
|
|
2
|
-
import
|
|
2
|
+
import { DEFAULT_TOKEN_EXPIRES_IN, } from './types.js';
|
|
3
3
|
import { Secret } from '@adonisjs/core/helpers';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { validateGuardOptions } from './validation.js';
|
|
5
|
+
import { resolveDriver } from './driver_resolver.js';
|
|
6
6
|
export class JwtGuard {
|
|
7
7
|
#ctx;
|
|
8
8
|
#userProvider;
|
|
@@ -10,7 +10,7 @@ export class JwtGuard {
|
|
|
10
10
|
#options;
|
|
11
11
|
#tokenName;
|
|
12
12
|
#refreshTokenName = 'refreshToken';
|
|
13
|
-
#
|
|
13
|
+
#driver;
|
|
14
14
|
#cookieOptions;
|
|
15
15
|
constructor(ctx, userProvider, option) {
|
|
16
16
|
this.#ctx = ctx;
|
|
@@ -20,85 +20,45 @@ export class JwtGuard {
|
|
|
20
20
|
if (!this.#options.content)
|
|
21
21
|
this.#options.content = (user) => ({ userId: user.getId() });
|
|
22
22
|
this.#tokenName = this.#options.tokenName ?? 'token';
|
|
23
|
-
this.#
|
|
23
|
+
this.#refreshTokenName = this.#options.refreshTokenName ?? 'refreshToken';
|
|
24
|
+
/**
|
|
25
|
+
* Never issue access tokens without an expiration
|
|
26
|
+
*/
|
|
27
|
+
this.#options.tokenExpiresIn = this.#options.tokenExpiresIn ?? DEFAULT_TOKEN_EXPIRES_IN;
|
|
24
28
|
this.#cookieOptions = {
|
|
25
29
|
httpOnly: true,
|
|
26
30
|
secure: true,
|
|
27
31
|
...this.#options.cookie,
|
|
28
32
|
};
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
}
|
|
33
|
+
validateGuardOptions(this.#options, 'JwtGuard');
|
|
34
|
+
this.#driver = resolveDriver(this.#options, 'JwtGuard');
|
|
74
35
|
}
|
|
75
36
|
#signAccessToken(payload) {
|
|
76
|
-
|
|
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);
|
|
37
|
+
return this.#driver.sign(payload, { expiresIn: this.#options.tokenExpiresIn });
|
|
84
38
|
}
|
|
85
39
|
async #verifyAccessToken(token) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
40
|
+
return this.#driver.verify(token);
|
|
41
|
+
}
|
|
42
|
+
#extractToken() {
|
|
43
|
+
/**
|
|
44
|
+
* The header is set explicitly by the client, so it takes precedence
|
|
45
|
+
* over the cookie, which the browser sends on every request (a stale
|
|
46
|
+
* cookie must not shadow a valid bearer token)
|
|
47
|
+
*/
|
|
48
|
+
const authHeader = this.#ctx.request.header('authorization');
|
|
49
|
+
if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
|
|
50
|
+
const headerToken = authHeader.slice(7).trim();
|
|
51
|
+
if (headerToken) {
|
|
52
|
+
return headerToken;
|
|
92
53
|
}
|
|
93
|
-
const key = await this.#jwksManager.getSigningKey(decoded.header.kid);
|
|
94
|
-
return jwt.verify(token, key);
|
|
95
54
|
}
|
|
96
|
-
if (this.#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
55
|
+
if (this.#options.useCookies) {
|
|
56
|
+
const cookieToken = this.#ctx.request.cookie(this.#tokenName);
|
|
57
|
+
if (cookieToken) {
|
|
58
|
+
return cookieToken;
|
|
59
|
+
}
|
|
100
60
|
}
|
|
101
|
-
return
|
|
61
|
+
return undefined;
|
|
102
62
|
}
|
|
103
63
|
/**
|
|
104
64
|
* A unique name for the guard driver
|
|
@@ -122,7 +82,7 @@ export class JwtGuard {
|
|
|
122
82
|
* Generate a JWT token for a given user.
|
|
123
83
|
*/
|
|
124
84
|
async generate(user) {
|
|
125
|
-
if (this.#
|
|
85
|
+
if (!this.#driver.canSign) {
|
|
126
86
|
throw new errors.E_UNAUTHORIZED_ACCESS("You can't use the auth.generate method with jwks", {
|
|
127
87
|
guardDriverName: this.driverName,
|
|
128
88
|
});
|
|
@@ -142,7 +102,7 @@ export class JwtGuard {
|
|
|
142
102
|
if (this.#options.useCookies) {
|
|
143
103
|
this.#ctx.response.cookie(`${this.#tokenName}`, token, {
|
|
144
104
|
...this.#cookieOptions,
|
|
145
|
-
maxAge: this.#options.
|
|
105
|
+
maxAge: this.#options.tokenExpiresIn,
|
|
146
106
|
});
|
|
147
107
|
}
|
|
148
108
|
if (this.#options.useCookiesForRefreshToken && refreshToken) {
|
|
@@ -154,7 +114,7 @@ export class JwtGuard {
|
|
|
154
114
|
return {
|
|
155
115
|
type: 'bearer',
|
|
156
116
|
token: token,
|
|
157
|
-
expiresIn: this.#options.
|
|
117
|
+
expiresIn: this.#options.tokenExpiresIn,
|
|
158
118
|
refreshToken: refreshToken,
|
|
159
119
|
refreshTokenExpiresIn: this.#options.refreshTokenExpiresIn,
|
|
160
120
|
};
|
|
@@ -174,32 +134,13 @@ export class JwtGuard {
|
|
|
174
134
|
}
|
|
175
135
|
this.authenticationAttempted = true;
|
|
176
136
|
/**
|
|
177
|
-
*
|
|
178
|
-
*/
|
|
179
|
-
let token = this.#ctx.request.cookie(`${this.#tokenName}`);
|
|
180
|
-
/**
|
|
181
|
-
* If token is missing on cookies, then try to read it from the header authorization
|
|
137
|
+
* Read the token from the Authorization header or the cookie
|
|
182
138
|
*/
|
|
139
|
+
const token = this.#extractToken();
|
|
183
140
|
if (!token) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const authHeader = this.#ctx.request.header('authorization');
|
|
188
|
-
if (!authHeader) {
|
|
189
|
-
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
190
|
-
guardDriverName: this.driverName,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Split the header value and read the token from it
|
|
195
|
-
*/
|
|
196
|
-
;
|
|
197
|
-
[, token] = authHeader.split('Bearer ');
|
|
198
|
-
if (!token) {
|
|
199
|
-
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
200
|
-
guardDriverName: this.driverName,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
141
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
142
|
+
guardDriverName: this.driverName,
|
|
143
|
+
});
|
|
203
144
|
}
|
|
204
145
|
/**
|
|
205
146
|
* Verify token
|
|
@@ -213,7 +154,19 @@ export class JwtGuard {
|
|
|
213
154
|
guardDriverName: this.driverName,
|
|
214
155
|
});
|
|
215
156
|
}
|
|
216
|
-
if (!payload || typeof payload !== 'object'
|
|
157
|
+
if (!payload || typeof payload !== 'object') {
|
|
158
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
159
|
+
guardDriverName: this.driverName,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (this.#options.verifyPayload && !(await this.#options.verifyPayload(payload))) {
|
|
163
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
164
|
+
guardDriverName: this.driverName,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const getUserId = this.#options.getUserId ?? ((claims) => claims.userId);
|
|
168
|
+
const userId = getUserId(payload);
|
|
169
|
+
if (userId === undefined || userId === null) {
|
|
217
170
|
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
218
171
|
guardDriverName: this.driverName,
|
|
219
172
|
});
|
|
@@ -221,7 +174,7 @@ export class JwtGuard {
|
|
|
221
174
|
/**
|
|
222
175
|
* Fetch the user by user ID and save a reference to it
|
|
223
176
|
*/
|
|
224
|
-
const providerUser = await this.#userProvider.findById(
|
|
177
|
+
const providerUser = await this.#userProvider.findById(userId);
|
|
225
178
|
if (!providerUser) {
|
|
226
179
|
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
227
180
|
guardDriverName: this.driverName,
|
|
@@ -234,7 +187,7 @@ export class JwtGuard {
|
|
|
234
187
|
}
|
|
235
188
|
async generateWithRefreshToken(refreshToken) {
|
|
236
189
|
this.authenticationAttempted = true;
|
|
237
|
-
if (this.#
|
|
190
|
+
if (!this.#driver.canSign) {
|
|
238
191
|
throw new errors.E_UNAUTHORIZED_ACCESS('JWKS is not supported for refresh token', {
|
|
239
192
|
guardDriverName: this.driverName,
|
|
240
193
|
});
|
|
@@ -253,6 +206,16 @@ export class JwtGuard {
|
|
|
253
206
|
guardDriverName: this.driverName,
|
|
254
207
|
});
|
|
255
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* When refresh token abilities are configured, reject tokens that
|
|
211
|
+
* don't have all of them (e.g. an API token from another provider)
|
|
212
|
+
*/
|
|
213
|
+
const requiredAbilities = this.#options.refreshTokenAbilities ?? [];
|
|
214
|
+
if (!requiredAbilities.every((ability) => accessToken.allows(ability))) {
|
|
215
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
216
|
+
guardDriverName: this.driverName,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
256
219
|
/**
|
|
257
220
|
* Fetch the user by user ID
|
|
258
221
|
*/
|
|
@@ -262,10 +225,9 @@ export class JwtGuard {
|
|
|
262
225
|
guardDriverName: this.driverName,
|
|
263
226
|
});
|
|
264
227
|
}
|
|
265
|
-
this.isAuthenticated = true;
|
|
266
|
-
this.user = providerUser.getOriginal();
|
|
267
228
|
/**
|
|
268
|
-
* Delete the refresh token from the database
|
|
229
|
+
* Delete the refresh token from the database. Only one of two concurrent
|
|
230
|
+
* requests using the same refresh token can delete it
|
|
269
231
|
*/
|
|
270
232
|
const isDeleted = await this.#refreshTokenUserProvider.invalidateToken(new Secret(refreshToken));
|
|
271
233
|
if (!isDeleted) {
|
|
@@ -273,19 +235,38 @@ export class JwtGuard {
|
|
|
273
235
|
guardDriverName: this.driverName,
|
|
274
236
|
});
|
|
275
237
|
}
|
|
276
|
-
|
|
238
|
+
const user = providerUser.getOriginal();
|
|
239
|
+
const result = await this.generate(user);
|
|
240
|
+
/**
|
|
241
|
+
* Like after `authenticate()`, `currentToken` holds the access token
|
|
242
|
+
* of the request, here the one that has just been issued
|
|
243
|
+
*/
|
|
244
|
+
this.isAuthenticated = true;
|
|
245
|
+
this.user = user;
|
|
246
|
+
this.user.currentToken = result.token;
|
|
247
|
+
/**
|
|
248
|
+
* The guard has a refresh token provider, so `generate()` issued a refresh token
|
|
249
|
+
*/
|
|
250
|
+
return result;
|
|
277
251
|
}
|
|
278
252
|
async #findRefreshToken() {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
253
|
+
/**
|
|
254
|
+
* The cookie is the configured transport, so it takes precedence
|
|
255
|
+
*/
|
|
283
256
|
if (this.#options.useCookiesForRefreshToken) {
|
|
284
257
|
const cookieToken = this.#ctx.request.cookie(this.#refreshTokenName);
|
|
285
258
|
if (cookieToken) {
|
|
286
259
|
return cookieToken;
|
|
287
260
|
}
|
|
288
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Only read the request body: a refresh token passed in the
|
|
264
|
+
* query string would end up in access logs and Referer headers
|
|
265
|
+
*/
|
|
266
|
+
const bodyToken = this.#ctx.request.body()[this.#refreshTokenName];
|
|
267
|
+
if (typeof bodyToken === 'string' && bodyToken) {
|
|
268
|
+
return bodyToken;
|
|
269
|
+
}
|
|
289
270
|
const authHeader = this.#ctx.request.header('authorization');
|
|
290
271
|
if (authHeader) {
|
|
291
272
|
if (authHeader.toLowerCase().startsWith('bearer ')) {
|
|
@@ -308,6 +289,7 @@ export class JwtGuard {
|
|
|
308
289
|
guardDriverName: this.driverName,
|
|
309
290
|
});
|
|
310
291
|
}
|
|
292
|
+
this.#clearCookies();
|
|
311
293
|
if (!refreshToken) {
|
|
312
294
|
try {
|
|
313
295
|
refreshToken = await this.#findRefreshToken();
|
|
@@ -318,6 +300,17 @@ export class JwtGuard {
|
|
|
318
300
|
}
|
|
319
301
|
await this.#refreshTokenUserProvider.invalidateToken(new Secret(refreshToken));
|
|
320
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Remove the access and refresh token cookies set by the guard
|
|
305
|
+
*/
|
|
306
|
+
#clearCookies() {
|
|
307
|
+
if (this.#options.useCookies) {
|
|
308
|
+
this.#ctx.response.clearCookie(this.#tokenName, this.#cookieOptions);
|
|
309
|
+
}
|
|
310
|
+
if (this.#options.useCookiesForRefreshToken) {
|
|
311
|
+
this.#ctx.response.clearCookie(this.#refreshTokenName, this.#cookieOptions);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
321
314
|
/**
|
|
322
315
|
* Same as authenticate, but does not throw an exception
|
|
323
316
|
*/
|
package/build/src/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { StringValue } from 'ms';
|
|
|
3
3
|
import type { AccessTokensUserProviderContract } from '@adonisjs/auth/types/access_tokens';
|
|
4
4
|
import type { Options } from 'jwks-rsa';
|
|
5
5
|
import type { CookieOptions } from '@adonisjs/core/types/http';
|
|
6
|
+
import type { JwtDriver } from './drivers/types.js';
|
|
6
7
|
export type JwtCookieOptions = Omit<Partial<CookieOptions>, 'maxAge' | 'expires'>;
|
|
7
8
|
/**
|
|
8
9
|
* The bridge between the User provider and the
|
|
@@ -41,12 +42,39 @@ export interface JwtUserProviderContract<RealUser> {
|
|
|
41
42
|
export type BaseJwtContent = {
|
|
42
43
|
userId: string | number | BigInt;
|
|
43
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Lifetime of access tokens when `tokenExpiresIn` is not configured
|
|
47
|
+
*/
|
|
48
|
+
export declare const DEFAULT_TOKEN_EXPIRES_IN = "1h";
|
|
49
|
+
export declare const ALLOWED_SYMMETRIC_ALGORITHMS: readonly ["HS256", "HS384", "HS512"];
|
|
50
|
+
export type JwtSymmetricAlgorithm = (typeof ALLOWED_SYMMETRIC_ALGORITHMS)[number];
|
|
51
|
+
export declare const ALLOWED_ASYMMETRIC_ALGORITHMS: readonly ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"];
|
|
44
52
|
/**
|
|
45
53
|
* Algorithms supported for asymmetric (RSA / ECDSA) JWT signing and verification.
|
|
46
54
|
* Use with `privateKey` + `publicKey` so access tokens can be verified elsewhere with only the public key.
|
|
47
55
|
*/
|
|
48
|
-
export type JwtAsymmetricAlgorithm =
|
|
56
|
+
export type JwtAsymmetricAlgorithm = (typeof ALLOWED_ASYMMETRIC_ALGORITHMS)[number];
|
|
57
|
+
export declare const ALLOWED_JWKS_ALGORITHMS: readonly ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"];
|
|
58
|
+
export type JwtJwksAlgorithm = (typeof ALLOWED_JWKS_ALGORITHMS)[number];
|
|
59
|
+
/**
|
|
60
|
+
* `refreshToken` is a string when the guard has a `refreshTokenUserProvider`
|
|
61
|
+
*/
|
|
62
|
+
export type JwtGenerateResult<WithRefreshToken extends boolean = boolean> = {
|
|
63
|
+
type: string;
|
|
64
|
+
token: string;
|
|
65
|
+
expiresIn?: number | StringValue;
|
|
66
|
+
refreshTokenExpiresIn?: number | StringValue;
|
|
67
|
+
} & ([WithRefreshToken] extends [true] ? {
|
|
68
|
+
refreshToken: string;
|
|
69
|
+
} : {
|
|
70
|
+
refreshToken?: string;
|
|
71
|
+
});
|
|
49
72
|
export type JwtGuardOptions<RealUser extends any = unknown> = {
|
|
73
|
+
/**
|
|
74
|
+
* Driver resolved by `jwtGuard()`. Not part of the public API: custom
|
|
75
|
+
* drivers are not supported and the `JwtDriver` interface may change.
|
|
76
|
+
*/
|
|
77
|
+
driver?: JwtDriver;
|
|
50
78
|
/**
|
|
51
79
|
* Symmetric signing secret (HMAC). Used when asymmetric keys are not set.
|
|
52
80
|
*/
|
|
@@ -66,11 +94,55 @@ export type JwtGuardOptions<RealUser extends any = unknown> = {
|
|
|
66
94
|
jwks?: Options;
|
|
67
95
|
refreshTokenUserProvider?: AccessTokensUserProviderContract<RealUser>;
|
|
68
96
|
tokenName?: string;
|
|
69
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Name of the refresh token cookie and request body field.
|
|
99
|
+
* Defaults to `refreshToken`.
|
|
100
|
+
*/
|
|
101
|
+
refreshTokenName?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Lifetime of access tokens. Defaults to `DEFAULT_TOKEN_EXPIRES_IN`.
|
|
104
|
+
*/
|
|
105
|
+
tokenExpiresIn?: number | StringValue;
|
|
70
106
|
refreshTokenExpiresIn?: number | StringValue;
|
|
71
107
|
useCookies?: boolean;
|
|
72
108
|
useCookiesForRefreshToken?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Abilities given to refresh tokens. When set, `generateWithRefreshToken`
|
|
111
|
+
* rejects refresh tokens that don't have all of them.
|
|
112
|
+
*/
|
|
73
113
|
refreshTokenAbilities?: string[];
|
|
74
114
|
cookie?: JwtCookieOptions;
|
|
75
|
-
content?: (user: JwtGuardUser<RealUser>) => Record<string, any
|
|
115
|
+
content?: (user: JwtGuardUser<RealUser>) => Record<string, any> & BaseJwtContent;
|
|
116
|
+
/**
|
|
117
|
+
* Expected JWT issuer (iss). Validated during token verification.
|
|
118
|
+
* Required in JWKS mode.
|
|
119
|
+
*/
|
|
120
|
+
issuer?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Expected JWT audience (aud). Validated during token verification.
|
|
123
|
+
* Required in JWKS mode, unless `verifyPayload` is set.
|
|
124
|
+
*/
|
|
125
|
+
audience?: string | string[];
|
|
126
|
+
/**
|
|
127
|
+
* Algorithms accepted when verifying tokens in JWKS mode.
|
|
128
|
+
* Must be a non-empty subset of `ALLOWED_JWKS_ALGORITHMS` (default: all of them).
|
|
129
|
+
*/
|
|
130
|
+
algorithms?: JwtJwksAlgorithm[];
|
|
131
|
+
/**
|
|
132
|
+
* Number of seconds of clock skew tolerated when checking `exp` / `nbf`.
|
|
133
|
+
*/
|
|
134
|
+
clockTolerance?: number;
|
|
135
|
+
/**
|
|
136
|
+
* Extract the user id from a verified token payload.
|
|
137
|
+
* Defaults to `payload.userId`. Use e.g. `(payload) => payload.sub` for external providers.
|
|
138
|
+
*/
|
|
139
|
+
getUserId?: JwtGetUserId;
|
|
140
|
+
/**
|
|
141
|
+
* Extra check on a verified token payload, run before looking up the user.
|
|
142
|
+
* Return `false` to reject the token. Use it to check claims other than
|
|
143
|
+
* `iss` and `aud`, e.g. `client_id` and `token_use` for Amazon Cognito.
|
|
144
|
+
*/
|
|
145
|
+
verifyPayload?: JwtVerifyPayload;
|
|
76
146
|
};
|
|
147
|
+
export type JwtVerifyPayload = (payload: Record<string, any>) => boolean | Promise<boolean>;
|
|
148
|
+
export type JwtGetUserId = (payload: Record<string, any>) => string | number | BigInt | undefined | null;
|
package/build/src/types.js
CHANGED
|
@@ -1 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Lifetime of access tokens when `tokenExpiresIn` is not configured
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_TOKEN_EXPIRES_IN = '1h';
|
|
5
|
+
export const ALLOWED_SYMMETRIC_ALGORITHMS = ['HS256', 'HS384', 'HS512'];
|
|
6
|
+
export const ALLOWED_ASYMMETRIC_ALGORITHMS = [
|
|
7
|
+
'RS256',
|
|
8
|
+
'RS384',
|
|
9
|
+
'RS512',
|
|
10
|
+
'ES256',
|
|
11
|
+
'ES384',
|
|
12
|
+
'ES512',
|
|
13
|
+
];
|
|
14
|
+
export const ALLOWED_JWKS_ALGORITHMS = [
|
|
15
|
+
...ALLOWED_ASYMMETRIC_ALGORITHMS,
|
|
16
|
+
'PS256',
|
|
17
|
+
'PS384',
|
|
18
|
+
'PS512',
|
|
19
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function validateGuardOptions(options, prefix = 'JwtGuard') {
|
|
2
|
+
if (typeof options.tokenExpiresIn === 'number' &&
|
|
3
|
+
!(Number.isFinite(options.tokenExpiresIn) && options.tokenExpiresIn > 0)) {
|
|
4
|
+
throw new Error(`${prefix} \`tokenExpiresIn\` must be a positive number of seconds`);
|
|
5
|
+
}
|
|
6
|
+
if (options.driver) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const asymmetricPartial = options.privateKey !== undefined ||
|
|
10
|
+
options.publicKey !== undefined ||
|
|
11
|
+
options.algorithm !== undefined;
|
|
12
|
+
const usesAsymmetric = options.privateKey !== undefined &&
|
|
13
|
+
options.publicKey !== undefined &&
|
|
14
|
+
options.algorithm !== undefined;
|
|
15
|
+
if (asymmetricPartial && !usesAsymmetric) {
|
|
16
|
+
throw new Error(`${prefix} asymmetric mode requires \`privateKey\`, \`publicKey\`, and \`algorithm\` to be set together`);
|
|
17
|
+
}
|
|
18
|
+
if (options.jwks && usesAsymmetric) {
|
|
19
|
+
throw new Error(`${prefix} cannot use \`jwks\` together with asymmetric \`privateKey\` / \`publicKey\``);
|
|
20
|
+
}
|
|
21
|
+
if (!options.jwks && !usesAsymmetric && !options.secret) {
|
|
22
|
+
throw new Error(`${prefix} requires \`secret\` (symmetric), or \`privateKey\` + \`publicKey\` + \`algorithm\` (asymmetric), or \`jwks\``);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({
|
|
3
|
+
to: app.makePath('database/migrations', entity.filename)
|
|
4
|
+
})
|
|
5
|
+
}}}
|
|
6
|
+
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
7
|
+
|
|
8
|
+
export default class extends BaseSchema {
|
|
9
|
+
protected tableName = 'jwt_refresh_tokens'
|
|
10
|
+
|
|
11
|
+
async up() {
|
|
12
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
13
|
+
table.increments()
|
|
14
|
+
table
|
|
15
|
+
.integer('tokenable_id')
|
|
16
|
+
.notNullable()
|
|
17
|
+
.unsigned()
|
|
18
|
+
.references('id')
|
|
19
|
+
.inTable('users')
|
|
20
|
+
.onDelete('CASCADE')
|
|
21
|
+
table.string('type').notNullable()
|
|
22
|
+
table.string('name').nullable()
|
|
23
|
+
table.string('hash', 80).notNullable()
|
|
24
|
+
table.text('abilities').notNullable()
|
|
25
|
+
table.timestamp('created_at', { precision: 6, useTz: true }).notNullable()
|
|
26
|
+
table.timestamp('updated_at', { precision: 6, useTz: true }).notNullable()
|
|
27
|
+
table.timestamp('expires_at', { precision: 6, useTz: true }).nullable()
|
|
28
|
+
table.timestamp('last_used_at', { precision: 6, useTz: true }).nullable()
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async down() {
|
|
33
|
+
this.schema.dropTable(this.tableName)
|
|
34
|
+
}
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maximemrf/adonisjs-jwt",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "AdonisJS package for authentication with JWT tokens",
|
|
4
|
+
"version": "1.0.0-rc.2",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=
|
|
6
|
+
"node": ">=24.0.0"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
|
+
"main": "./build/index.js",
|
|
10
|
+
"types": "./build/index.d.ts",
|
|
9
11
|
"files": [
|
|
10
12
|
"build/src",
|
|
11
13
|
"build/stubs",
|
|
12
14
|
"build/index.d.ts",
|
|
13
|
-
"build/index.js"
|
|
15
|
+
"build/index.js",
|
|
16
|
+
"build/configure.d.ts",
|
|
17
|
+
"build/configure.js"
|
|
14
18
|
],
|
|
15
19
|
"exports": {
|
|
16
20
|
".": "./build/index.js",
|
|
17
21
|
"./types": "./build/src/types.js",
|
|
18
|
-
"./jwt_config": "./build/src/define_config.js"
|
|
19
|
-
|
|
20
|
-
"imports": {
|
|
21
|
-
"#generated/*": "./.adonisjs/server/*.js",
|
|
22
|
-
"#transformers/*": "./app/transformers/*.js",
|
|
23
|
-
"#database/*": "./database/*.js"
|
|
22
|
+
"./jwt_config": "./build/src/define_config.js",
|
|
23
|
+
"./stubs": "./build/stubs/main.js"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"prepare": "tsc && npm run copy:templates",
|
|
@@ -40,39 +40,40 @@
|
|
|
40
40
|
"prepublishOnly": "npm run build"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@adonisjs/assembler": "^8.
|
|
44
|
-
"@adonisjs/auth": "^10.
|
|
45
|
-
"@adonisjs/core": "^7.
|
|
46
|
-
"@adonisjs/eslint-config": "^3.
|
|
47
|
-
"@adonisjs/lucid": "^22.2
|
|
48
|
-
"@adonisjs/prettier-config": "^1.
|
|
43
|
+
"@adonisjs/assembler": "^8.4.0",
|
|
44
|
+
"@adonisjs/auth": "^10.1.0",
|
|
45
|
+
"@adonisjs/core": "^7.3.5",
|
|
46
|
+
"@adonisjs/eslint-config": "^3.1.0",
|
|
47
|
+
"@adonisjs/lucid": "^22.4.2",
|
|
48
|
+
"@adonisjs/prettier-config": "^1.5.0",
|
|
49
49
|
"@adonisjs/tsconfig": "^2.0.0",
|
|
50
50
|
"@japa/assert": "^4.2.0",
|
|
51
51
|
"@japa/file-system": "^3.0.0",
|
|
52
52
|
"@japa/runner": "^5.3.0",
|
|
53
53
|
"@poppinss/ts-exec": "^1.4.4",
|
|
54
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
54
55
|
"@types/nock": "^11.1.0",
|
|
55
|
-
"@types/node": "^
|
|
56
|
+
"@types/node": "^24.19.2",
|
|
56
57
|
"c8": "^11.0.0",
|
|
57
58
|
"copyfiles": "^2.4.1",
|
|
58
59
|
"del-cli": "^7.0.0",
|
|
59
|
-
"eslint": "^9.39.
|
|
60
|
+
"eslint": "^9.39.5",
|
|
60
61
|
"luxon": "^3.7.2",
|
|
61
|
-
"nock": "^14.0.
|
|
62
|
-
"np": "^11.
|
|
63
|
-
"prettier": "^3.
|
|
62
|
+
"nock": "^14.0.16",
|
|
63
|
+
"np": "^11.2.2",
|
|
64
|
+
"prettier": "^3.9.6",
|
|
64
65
|
"sqlite3": "^6.0.1",
|
|
65
66
|
"timekeeper": "^2.3.1",
|
|
66
|
-
"typescript": "~
|
|
67
|
-
"youch": "^4.1.
|
|
67
|
+
"typescript": "~6.0.3",
|
|
68
|
+
"youch": "^4.1.1"
|
|
68
69
|
},
|
|
69
70
|
"dependencies": {
|
|
70
|
-
"@types/jsonwebtoken": "^9.0.10",
|
|
71
71
|
"jsonwebtoken": "^9.0.3",
|
|
72
|
-
"jwks-rsa": "^4.0
|
|
72
|
+
"jwks-rsa": "^4.1.0",
|
|
73
73
|
"ms": "^2.1.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
+
"@adonisjs/auth": "^10.1.0",
|
|
76
77
|
"@adonisjs/core": "^7.0.1"
|
|
77
78
|
},
|
|
78
79
|
"author": "MaximeMRF",
|
|
@@ -116,4 +117,4 @@
|
|
|
116
117
|
"tests/**"
|
|
117
118
|
]
|
|
118
119
|
}
|
|
119
|
-
}
|
|
120
|
+
}
|