@maximemrf/adonisjs-jwt 0.8.0 → 1.0.0-rc.1
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 +244 -23
- package/build/configure.d.ts +2 -0
- package/build/configure.js +240 -0
- package/build/index.d.ts +3 -0
- package/build/index.js +3 -0
- package/build/src/define_config.d.ts +14 -3
- package/build/src/define_config.js +51 -6
- 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} +3 -16
- package/build/src/{jwt.js → guard.js} +109 -60
- package/build/src/types.d.ts +90 -4
- 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 +37 -28
|
@@ -1,7 +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 {
|
|
4
|
+
import { validateGuardOptions } from './validation.js';
|
|
5
|
+
import { resolveDriver } from './driver_resolver.js';
|
|
5
6
|
export class JwtGuard {
|
|
6
7
|
#ctx;
|
|
7
8
|
#userProvider;
|
|
@@ -9,7 +10,7 @@ export class JwtGuard {
|
|
|
9
10
|
#options;
|
|
10
11
|
#tokenName;
|
|
11
12
|
#refreshTokenName = 'refreshToken';
|
|
12
|
-
#
|
|
13
|
+
#driver;
|
|
13
14
|
#cookieOptions;
|
|
14
15
|
constructor(ctx, userProvider, option) {
|
|
15
16
|
this.#ctx = ctx;
|
|
@@ -19,12 +20,45 @@ export class JwtGuard {
|
|
|
19
20
|
if (!this.#options.content)
|
|
20
21
|
this.#options.content = (user) => ({ userId: user.getId() });
|
|
21
22
|
this.#tokenName = this.#options.tokenName ?? 'token';
|
|
22
|
-
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;
|
|
23
28
|
this.#cookieOptions = {
|
|
24
29
|
httpOnly: true,
|
|
25
30
|
secure: true,
|
|
26
31
|
...this.#options.cookie,
|
|
27
32
|
};
|
|
33
|
+
validateGuardOptions(this.#options, 'JwtGuard');
|
|
34
|
+
this.#driver = resolveDriver(this.#options, 'JwtGuard');
|
|
35
|
+
}
|
|
36
|
+
#signAccessToken(payload) {
|
|
37
|
+
return this.#driver.sign(payload, { expiresIn: this.#options.tokenExpiresIn });
|
|
38
|
+
}
|
|
39
|
+
async #verifyAccessToken(token) {
|
|
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;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (this.#options.useCookies) {
|
|
56
|
+
const cookieToken = this.#ctx.request.cookie(this.#tokenName);
|
|
57
|
+
if (cookieToken) {
|
|
58
|
+
return cookieToken;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
28
62
|
}
|
|
29
63
|
/**
|
|
30
64
|
* A unique name for the guard driver
|
|
@@ -48,17 +82,14 @@ export class JwtGuard {
|
|
|
48
82
|
* Generate a JWT token for a given user.
|
|
49
83
|
*/
|
|
50
84
|
async generate(user) {
|
|
51
|
-
if (this.#
|
|
85
|
+
if (!this.#driver.canSign) {
|
|
52
86
|
throw new errors.E_UNAUTHORIZED_ACCESS("You can't use the auth.generate method with jwks", {
|
|
53
87
|
guardDriverName: this.driverName,
|
|
54
88
|
});
|
|
55
89
|
}
|
|
56
90
|
const providerUser = await this.#userProvider.createUserForGuard(user);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
expiresIn: this.#options.expiresIn,
|
|
60
|
-
}
|
|
61
|
-
: {});
|
|
91
|
+
const signPayload = this.#options.content(providerUser);
|
|
92
|
+
const token = this.#signAccessToken(signPayload);
|
|
62
93
|
let refreshToken;
|
|
63
94
|
if (this.#refreshTokenUserProvider) {
|
|
64
95
|
const generatedRefreshToken = await this.#refreshTokenUserProvider.createToken(user, this.#options.refreshTokenAbilities ?? [], this.#options.refreshTokenExpiresIn
|
|
@@ -71,7 +102,7 @@ export class JwtGuard {
|
|
|
71
102
|
if (this.#options.useCookies) {
|
|
72
103
|
this.#ctx.response.cookie(`${this.#tokenName}`, token, {
|
|
73
104
|
...this.#cookieOptions,
|
|
74
|
-
maxAge: this.#options.
|
|
105
|
+
maxAge: this.#options.tokenExpiresIn,
|
|
75
106
|
});
|
|
76
107
|
}
|
|
77
108
|
if (this.#options.useCookiesForRefreshToken && refreshToken) {
|
|
@@ -83,7 +114,7 @@ export class JwtGuard {
|
|
|
83
114
|
return {
|
|
84
115
|
type: 'bearer',
|
|
85
116
|
token: token,
|
|
86
|
-
expiresIn: this.#options.
|
|
117
|
+
expiresIn: this.#options.tokenExpiresIn,
|
|
87
118
|
refreshToken: refreshToken,
|
|
88
119
|
refreshTokenExpiresIn: this.#options.refreshTokenExpiresIn,
|
|
89
120
|
};
|
|
@@ -103,58 +134,39 @@ export class JwtGuard {
|
|
|
103
134
|
}
|
|
104
135
|
this.authenticationAttempted = true;
|
|
105
136
|
/**
|
|
106
|
-
*
|
|
107
|
-
*/
|
|
108
|
-
let token = this.#ctx.request.cookie(`${this.#tokenName}`);
|
|
109
|
-
/**
|
|
110
|
-
* 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
|
|
111
138
|
*/
|
|
139
|
+
const token = this.#extractToken();
|
|
112
140
|
if (!token) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const authHeader = this.#ctx.request.header('authorization');
|
|
117
|
-
if (!authHeader) {
|
|
118
|
-
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
119
|
-
guardDriverName: this.driverName,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Split the header value and read the token from it
|
|
124
|
-
*/
|
|
125
|
-
;
|
|
126
|
-
[, token] = authHeader.split('Bearer ');
|
|
127
|
-
if (!token) {
|
|
128
|
-
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
129
|
-
guardDriverName: this.driverName,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
141
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
142
|
+
guardDriverName: this.driverName,
|
|
143
|
+
});
|
|
132
144
|
}
|
|
133
145
|
/**
|
|
134
146
|
* Verify token
|
|
135
147
|
*/
|
|
136
148
|
let payload;
|
|
137
149
|
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
|
-
}
|
|
150
|
+
payload = await this.#verifyAccessToken(token);
|
|
151
151
|
}
|
|
152
152
|
catch {
|
|
153
153
|
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
154
154
|
guardDriverName: this.driverName,
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
|
-
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) {
|
|
158
170
|
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
159
171
|
guardDriverName: this.driverName,
|
|
160
172
|
});
|
|
@@ -162,7 +174,7 @@ export class JwtGuard {
|
|
|
162
174
|
/**
|
|
163
175
|
* Fetch the user by user ID and save a reference to it
|
|
164
176
|
*/
|
|
165
|
-
const providerUser = await this.#userProvider.findById(
|
|
177
|
+
const providerUser = await this.#userProvider.findById(userId);
|
|
166
178
|
if (!providerUser) {
|
|
167
179
|
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
168
180
|
guardDriverName: this.driverName,
|
|
@@ -175,7 +187,7 @@ export class JwtGuard {
|
|
|
175
187
|
}
|
|
176
188
|
async generateWithRefreshToken(refreshToken) {
|
|
177
189
|
this.authenticationAttempted = true;
|
|
178
|
-
if (this.#
|
|
190
|
+
if (!this.#driver.canSign) {
|
|
179
191
|
throw new errors.E_UNAUTHORIZED_ACCESS('JWKS is not supported for refresh token', {
|
|
180
192
|
guardDriverName: this.driverName,
|
|
181
193
|
});
|
|
@@ -194,6 +206,16 @@ export class JwtGuard {
|
|
|
194
206
|
guardDriverName: this.driverName,
|
|
195
207
|
});
|
|
196
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
|
+
}
|
|
197
219
|
/**
|
|
198
220
|
* Fetch the user by user ID
|
|
199
221
|
*/
|
|
@@ -203,10 +225,9 @@ export class JwtGuard {
|
|
|
203
225
|
guardDriverName: this.driverName,
|
|
204
226
|
});
|
|
205
227
|
}
|
|
206
|
-
this.isAuthenticated = true;
|
|
207
|
-
this.user = providerUser.getOriginal();
|
|
208
228
|
/**
|
|
209
|
-
* 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
|
|
210
231
|
*/
|
|
211
232
|
const isDeleted = await this.#refreshTokenUserProvider.invalidateToken(new Secret(refreshToken));
|
|
212
233
|
if (!isDeleted) {
|
|
@@ -214,19 +235,35 @@ export class JwtGuard {
|
|
|
214
235
|
guardDriverName: this.driverName,
|
|
215
236
|
});
|
|
216
237
|
}
|
|
217
|
-
|
|
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
|
+
return result;
|
|
218
248
|
}
|
|
219
249
|
async #findRefreshToken() {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
250
|
+
/**
|
|
251
|
+
* The cookie is the configured transport, so it takes precedence
|
|
252
|
+
*/
|
|
224
253
|
if (this.#options.useCookiesForRefreshToken) {
|
|
225
254
|
const cookieToken = this.#ctx.request.cookie(this.#refreshTokenName);
|
|
226
255
|
if (cookieToken) {
|
|
227
256
|
return cookieToken;
|
|
228
257
|
}
|
|
229
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Only read the request body: a refresh token passed in the
|
|
261
|
+
* query string would end up in access logs and Referer headers
|
|
262
|
+
*/
|
|
263
|
+
const bodyToken = this.#ctx.request.body()[this.#refreshTokenName];
|
|
264
|
+
if (typeof bodyToken === 'string' && bodyToken) {
|
|
265
|
+
return bodyToken;
|
|
266
|
+
}
|
|
230
267
|
const authHeader = this.#ctx.request.header('authorization');
|
|
231
268
|
if (authHeader) {
|
|
232
269
|
if (authHeader.toLowerCase().startsWith('bearer ')) {
|
|
@@ -249,6 +286,7 @@ export class JwtGuard {
|
|
|
249
286
|
guardDriverName: this.driverName,
|
|
250
287
|
});
|
|
251
288
|
}
|
|
289
|
+
this.#clearCookies();
|
|
252
290
|
if (!refreshToken) {
|
|
253
291
|
try {
|
|
254
292
|
refreshToken = await this.#findRefreshToken();
|
|
@@ -259,6 +297,17 @@ export class JwtGuard {
|
|
|
259
297
|
}
|
|
260
298
|
await this.#refreshTokenUserProvider.invalidateToken(new Secret(refreshToken));
|
|
261
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Remove the access and refresh token cookies set by the guard
|
|
302
|
+
*/
|
|
303
|
+
#clearCookies() {
|
|
304
|
+
if (this.#options.useCookies) {
|
|
305
|
+
this.#ctx.response.clearCookie(this.#tokenName, this.#cookieOptions);
|
|
306
|
+
}
|
|
307
|
+
if (this.#options.useCookiesForRefreshToken) {
|
|
308
|
+
this.#ctx.response.clearCookie(this.#refreshTokenName, this.#cookieOptions);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
262
311
|
/**
|
|
263
312
|
* Same as authenticate, but does not throw an exception
|
|
264
313
|
*/
|
package/build/src/types.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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';
|
|
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,16 +42,101 @@ 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"];
|
|
52
|
+
/**
|
|
53
|
+
* Algorithms supported for asymmetric (RSA / ECDSA) JWT signing and verification.
|
|
54
|
+
* Use with `privateKey` + `publicKey` so access tokens can be verified elsewhere with only the public key.
|
|
55
|
+
*/
|
|
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
|
+
export type JwtGenerateResult = {
|
|
60
|
+
type: string;
|
|
61
|
+
token: string;
|
|
62
|
+
expiresIn?: number | StringValue;
|
|
63
|
+
refreshToken?: string;
|
|
64
|
+
refreshTokenExpiresIn?: number | StringValue;
|
|
65
|
+
};
|
|
44
66
|
export type JwtGuardOptions<RealUser extends any = unknown> = {
|
|
45
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Driver resolved by `jwtGuard()`. Not part of the public API: custom
|
|
69
|
+
* drivers are not supported and the `JwtDriver` interface may change.
|
|
70
|
+
*/
|
|
71
|
+
driver?: JwtDriver;
|
|
72
|
+
/**
|
|
73
|
+
* Symmetric signing secret (HMAC). Used when asymmetric keys are not set.
|
|
74
|
+
*/
|
|
75
|
+
secret?: string;
|
|
76
|
+
/**
|
|
77
|
+
* PEM-encoded private key for signing access tokens (asymmetric mode).
|
|
78
|
+
*/
|
|
79
|
+
privateKey?: string;
|
|
80
|
+
/**
|
|
81
|
+
* PEM-encoded public key for verifying access tokens (asymmetric mode).
|
|
82
|
+
*/
|
|
83
|
+
publicKey?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Required with `privateKey` and `publicKey`.
|
|
86
|
+
*/
|
|
87
|
+
algorithm?: JwtAsymmetricAlgorithm;
|
|
46
88
|
jwks?: Options;
|
|
47
89
|
refreshTokenUserProvider?: AccessTokensUserProviderContract<RealUser>;
|
|
48
90
|
tokenName?: string;
|
|
49
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Name of the refresh token cookie and request body field.
|
|
93
|
+
* Defaults to `refreshToken`.
|
|
94
|
+
*/
|
|
95
|
+
refreshTokenName?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Lifetime of access tokens. Defaults to `DEFAULT_TOKEN_EXPIRES_IN`.
|
|
98
|
+
*/
|
|
99
|
+
tokenExpiresIn?: number | StringValue;
|
|
50
100
|
refreshTokenExpiresIn?: number | StringValue;
|
|
51
101
|
useCookies?: boolean;
|
|
52
102
|
useCookiesForRefreshToken?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Abilities given to refresh tokens. When set, `generateWithRefreshToken`
|
|
105
|
+
* rejects refresh tokens that don't have all of them.
|
|
106
|
+
*/
|
|
53
107
|
refreshTokenAbilities?: string[];
|
|
54
108
|
cookie?: JwtCookieOptions;
|
|
55
|
-
content?: (user: JwtGuardUser<RealUser>) => Record<string, any
|
|
109
|
+
content?: (user: JwtGuardUser<RealUser>) => Record<string, any> & BaseJwtContent;
|
|
110
|
+
/**
|
|
111
|
+
* Expected JWT issuer (iss). Validated during token verification.
|
|
112
|
+
* Required in JWKS mode.
|
|
113
|
+
*/
|
|
114
|
+
issuer?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Expected JWT audience (aud). Validated during token verification.
|
|
117
|
+
* Required in JWKS mode, unless `verifyPayload` is set.
|
|
118
|
+
*/
|
|
119
|
+
audience?: string | string[];
|
|
120
|
+
/**
|
|
121
|
+
* Algorithms accepted when verifying tokens in JWKS mode.
|
|
122
|
+
* Must be a non-empty subset of `ALLOWED_JWKS_ALGORITHMS` (default: all of them).
|
|
123
|
+
*/
|
|
124
|
+
algorithms?: JwtJwksAlgorithm[];
|
|
125
|
+
/**
|
|
126
|
+
* Number of seconds of clock skew tolerated when checking `exp` / `nbf`.
|
|
127
|
+
*/
|
|
128
|
+
clockTolerance?: number;
|
|
129
|
+
/**
|
|
130
|
+
* Extract the user id from a verified token payload.
|
|
131
|
+
* Defaults to `payload.userId`. Use e.g. `(payload) => payload.sub` for external providers.
|
|
132
|
+
*/
|
|
133
|
+
getUserId?: JwtGetUserId;
|
|
134
|
+
/**
|
|
135
|
+
* Extra check on a verified token payload, run before looking up the user.
|
|
136
|
+
* Return `false` to reject the token. Use it to check claims other than
|
|
137
|
+
* `iss` and `aud`, e.g. `client_id` and `token_use` for Amazon Cognito.
|
|
138
|
+
*/
|
|
139
|
+
verifyPayload?: JwtVerifyPayload;
|
|
56
140
|
};
|
|
141
|
+
export type JwtVerifyPayload = (payload: Record<string, any>) => boolean | Promise<boolean>;
|
|
142
|
+
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,28 +1,29 @@
|
|
|
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.1",
|
|
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
|
+
"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,43 +40,51 @@
|
|
|
39
40
|
"prepublishOnly": "npm run build"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@adonisjs/assembler": "^8.
|
|
43
|
-
"@adonisjs/auth": "^10.
|
|
44
|
-
"@adonisjs/core": "^7.
|
|
45
|
-
"@adonisjs/eslint-config": "^3.
|
|
46
|
-
"@adonisjs/lucid": "^22.
|
|
47
|
-
"@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",
|
|
48
49
|
"@adonisjs/tsconfig": "^2.0.0",
|
|
49
50
|
"@japa/assert": "^4.2.0",
|
|
50
51
|
"@japa/file-system": "^3.0.0",
|
|
51
52
|
"@japa/runner": "^5.3.0",
|
|
52
53
|
"@poppinss/ts-exec": "^1.4.4",
|
|
54
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
53
55
|
"@types/nock": "^11.1.0",
|
|
54
|
-
"@types/node": "^
|
|
55
|
-
"c8": "^
|
|
56
|
+
"@types/node": "^24.19.2",
|
|
57
|
+
"c8": "^11.0.0",
|
|
56
58
|
"copyfiles": "^2.4.1",
|
|
57
59
|
"del-cli": "^7.0.0",
|
|
58
|
-
"eslint": "^9.39.
|
|
60
|
+
"eslint": "^9.39.5",
|
|
59
61
|
"luxon": "^3.7.2",
|
|
60
|
-
"nock": "^14.0.
|
|
61
|
-
"np": "^11.
|
|
62
|
-
"prettier": "^3.
|
|
63
|
-
"sqlite3": "^
|
|
62
|
+
"nock": "^14.0.16",
|
|
63
|
+
"np": "^11.2.2",
|
|
64
|
+
"prettier": "^3.9.6",
|
|
65
|
+
"sqlite3": "^6.0.1",
|
|
64
66
|
"timekeeper": "^2.3.1",
|
|
65
|
-
"typescript": "~
|
|
66
|
-
"youch": "^4.1.
|
|
67
|
+
"typescript": "~6.0.3",
|
|
68
|
+
"youch": "^4.1.1"
|
|
67
69
|
},
|
|
68
70
|
"dependencies": {
|
|
69
|
-
"@types/jsonwebtoken": "^9.0.10",
|
|
70
71
|
"jsonwebtoken": "^9.0.3",
|
|
71
|
-
"jwks-rsa": "^
|
|
72
|
+
"jwks-rsa": "^4.1.0",
|
|
72
73
|
"ms": "^2.1.3"
|
|
73
74
|
},
|
|
74
75
|
"peerDependencies": {
|
|
76
|
+
"@adonisjs/auth": "^10.1.0",
|
|
75
77
|
"@adonisjs/core": "^7.0.1"
|
|
76
78
|
},
|
|
77
79
|
"author": "MaximeMRF",
|
|
78
80
|
"license": "MIT",
|
|
81
|
+
"repository": {
|
|
82
|
+
"type": "git",
|
|
83
|
+
"url": "git+https://github.com/MaximeMRF/adonisjs-jwt.git"
|
|
84
|
+
},
|
|
85
|
+
"bugs": {
|
|
86
|
+
"url": "https://github.com/MaximeMRF/adonisjs-jwt/issues"
|
|
87
|
+
},
|
|
79
88
|
"keywords": [
|
|
80
89
|
"adonisjs",
|
|
81
90
|
"adonis",
|
|
@@ -108,4 +117,4 @@
|
|
|
108
117
|
"tests/**"
|
|
109
118
|
]
|
|
110
119
|
}
|
|
111
|
-
}
|
|
120
|
+
}
|