@maximemrf/adonisjs-jwt 0.6.1 → 0.7.0-alpha.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 +28 -18
- package/build/src/define_config.d.ts +3 -0
- package/build/src/define_config.js +3 -0
- package/build/src/jwt.d.ts +16 -5
- package/build/src/jwt.js +59 -37
- package/build/src/types.d.ts +3 -0
- package/package.json +21 -21
package/README.md
CHANGED
|
@@ -75,6 +75,12 @@ const authConfig = defineConfig({
|
|
|
75
75
|
tokens: 'refreshTokens',
|
|
76
76
|
model: () => import('#models/user'),
|
|
77
77
|
}),
|
|
78
|
+
// optionally set the expiry for the refresh token
|
|
79
|
+
refreshTokenExpiresIn: '7d',
|
|
80
|
+
// ability to separate cookie usage for refresh token
|
|
81
|
+
useCookiesForRefreshToken: true,
|
|
82
|
+
// limit the abilities of the refresh token
|
|
83
|
+
refreshTokenAbilities: ['refresh_token'],
|
|
78
84
|
// content is a function that takes the user and returns the content of the token, it can be optional, by default it returns only the user id
|
|
79
85
|
content: <T>(user: JwtGuardUser<T>): JwtContent => {
|
|
80
86
|
return {
|
|
@@ -125,7 +131,13 @@ export default class extends BaseSchema {
|
|
|
125
131
|
async up() {
|
|
126
132
|
this.schema.createTable(this.tableName, (table) => {
|
|
127
133
|
table.increments()
|
|
128
|
-
table
|
|
134
|
+
table
|
|
135
|
+
.integer('tokenable_id')
|
|
136
|
+
.notNullable()
|
|
137
|
+
.unsigned()
|
|
138
|
+
.references('id')
|
|
139
|
+
.inTable('users')
|
|
140
|
+
.onDelete('CASCADE')
|
|
129
141
|
table.string('type').notNullable()
|
|
130
142
|
table.string('name').nullable()
|
|
131
143
|
table.string('hash', 80).notNullable()
|
|
@@ -180,7 +192,9 @@ router.post('login', async ({ request, auth }) => {
|
|
|
180
192
|
const { email, password } = request.all()
|
|
181
193
|
const user = await User.verifyCredentials(email, password)
|
|
182
194
|
|
|
183
|
-
// to generate a token
|
|
195
|
+
// to generate a token (and refresh token if configured)
|
|
196
|
+
// this returns { type, token, expiresIn, refreshToken, refreshTokenExpiresIn }
|
|
197
|
+
// if useCookies is true, it sets cookies on the response instead
|
|
184
198
|
return await auth.use('jwt').generate(user)
|
|
185
199
|
})
|
|
186
200
|
|
|
@@ -196,25 +210,21 @@ router.get('/', async ({ auth }) => {
|
|
|
196
210
|
})
|
|
197
211
|
.use(middleware.auth({ guards: ['jwt'] }))
|
|
198
212
|
|
|
199
|
-
// to create a refresh token to a given user
|
|
200
|
-
import User from '#models/user'
|
|
201
|
-
const user = auth.getUserOrFail()
|
|
202
|
-
const refreshToken = await User.refreshTokens.create(user)
|
|
203
|
-
|
|
204
213
|
// if you use the refresh token
|
|
205
214
|
router.post('jwt/refresh', async ({ auth }) => {
|
|
206
215
|
// this will authenticate the user using the refresh token
|
|
207
|
-
// it will delete the old refresh token and generate a new one
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
// it will delete the old refresh token and generate a new one
|
|
217
|
+
// it accepts an optional refresh token, otherwise it looks in:
|
|
218
|
+
// 1. request body 'refreshToken'
|
|
219
|
+
// 2. cookies (if enabled)
|
|
220
|
+
// 3. Authorization header
|
|
221
|
+
return await auth.use('jwt').generateWithRefreshToken()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// to logout (revoke refresh token)
|
|
225
|
+
router.post('logout', async ({ auth }) => {
|
|
226
|
+
await auth.use('jwt').revoke()
|
|
227
|
+
return { message: 'Logged out' }
|
|
218
228
|
})
|
|
219
229
|
```
|
|
220
230
|
|
|
@@ -9,7 +9,10 @@ export declare function jwtGuard<UserProvider extends JwtUserProviderContract<un
|
|
|
9
9
|
refreshTokenUserProvider?: AccessTokensUserProviderContract<unknown>;
|
|
10
10
|
tokenName?: string;
|
|
11
11
|
tokenExpiresIn?: number | StringValue;
|
|
12
|
+
refreshTokenExpiresIn?: number | StringValue;
|
|
12
13
|
useCookies?: boolean;
|
|
14
|
+
useCookiesForRefreshToken?: boolean;
|
|
15
|
+
refreshTokenAbilities?: string[];
|
|
13
16
|
secret?: string;
|
|
14
17
|
content: <T>(user: JwtGuardUser<T>) => Record<string | number, any>;
|
|
15
18
|
}): GuardConfigProvider<(ctx: HttpContext) => JwtGuard<UserProvider>>;
|
|
@@ -8,7 +8,10 @@ export function jwtGuard(config) {
|
|
|
8
8
|
refreshTokenUserProvider: config.refreshTokenUserProvider,
|
|
9
9
|
tokenName: config.tokenName,
|
|
10
10
|
expiresIn: config.tokenExpiresIn,
|
|
11
|
+
refreshTokenExpiresIn: config.refreshTokenExpiresIn,
|
|
11
12
|
useCookies: config.useCookies,
|
|
13
|
+
useCookiesForRefreshToken: config.useCookiesForRefreshToken,
|
|
14
|
+
refreshTokenAbilities: config.refreshTokenAbilities,
|
|
12
15
|
content: config.content,
|
|
13
16
|
};
|
|
14
17
|
return (ctx) => new JwtGuard(ctx, config.provider, options);
|
package/build/src/jwt.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { symbols } from '@adonisjs/auth';
|
|
2
2
|
import { AuthClientResponse, GuardContract } from '@adonisjs/auth/types';
|
|
3
3
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
4
|
+
import type { StringValue } from 'ms';
|
|
4
5
|
import { JwtUserProviderContract, JwtGuardOptions } from './types.js';
|
|
5
6
|
export declare class JwtGuard<UserProvider extends JwtUserProviderContract<unknown>> implements GuardContract<UserProvider[typeof symbols.PROVIDER_REAL_USER]> {
|
|
6
7
|
#private;
|
|
@@ -33,10 +34,12 @@ export declare class JwtGuard<UserProvider extends JwtUserProviderContract<unkno
|
|
|
33
34
|
/**
|
|
34
35
|
* Generate a JWT token for a given user.
|
|
35
36
|
*/
|
|
36
|
-
generate(user: UserProvider[typeof symbols.PROVIDER_REAL_USER]): Promise<
|
|
37
|
+
generate(user: UserProvider[typeof symbols.PROVIDER_REAL_USER]): Promise<{
|
|
37
38
|
type: string;
|
|
38
39
|
token: string;
|
|
39
|
-
expiresIn: number |
|
|
40
|
+
expiresIn: number | StringValue | undefined;
|
|
41
|
+
refreshToken: string | undefined;
|
|
42
|
+
refreshTokenExpiresIn: number | StringValue | undefined;
|
|
40
43
|
}>;
|
|
41
44
|
/**
|
|
42
45
|
* Authenticate the current HTTP request and return
|
|
@@ -46,9 +49,17 @@ export declare class JwtGuard<UserProvider extends JwtUserProviderContract<unkno
|
|
|
46
49
|
authenticate(): Promise<UserProvider[typeof symbols.PROVIDER_REAL_USER] & {
|
|
47
50
|
currentToken: string;
|
|
48
51
|
}>;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
generateWithRefreshToken(refreshToken?: string): Promise<{
|
|
53
|
+
type: string;
|
|
54
|
+
token: string;
|
|
55
|
+
expiresIn: number | StringValue | undefined;
|
|
56
|
+
refreshToken: string | undefined;
|
|
57
|
+
refreshTokenExpiresIn: number | StringValue | undefined;
|
|
58
|
+
} | undefined>;
|
|
59
|
+
/**
|
|
60
|
+
* Revoke the refresh token
|
|
61
|
+
*/
|
|
62
|
+
revoke(refreshToken?: string): Promise<void>;
|
|
52
63
|
/**
|
|
53
64
|
* Same as authenticate, but does not throw an exception
|
|
54
65
|
*/
|
package/build/src/jwt.js
CHANGED
|
@@ -7,6 +7,7 @@ export class JwtGuard {
|
|
|
7
7
|
#refreshTokenUserProvider;
|
|
8
8
|
#options;
|
|
9
9
|
#tokenName;
|
|
10
|
+
#refreshTokenName = 'refreshToken';
|
|
10
11
|
constructor(ctx, userProvider, option) {
|
|
11
12
|
this.#ctx = ctx;
|
|
12
13
|
this.#userProvider = userProvider;
|
|
@@ -44,8 +45,22 @@ export class JwtGuard {
|
|
|
44
45
|
expiresIn: this.#options.expiresIn,
|
|
45
46
|
}
|
|
46
47
|
: {});
|
|
48
|
+
let refreshToken;
|
|
49
|
+
if (this.#refreshTokenUserProvider) {
|
|
50
|
+
const generatedRefreshToken = await this.#refreshTokenUserProvider.createToken(user, this.#options.refreshTokenAbilities ?? [], this.#options.refreshTokenExpiresIn
|
|
51
|
+
? {
|
|
52
|
+
expiresIn: this.#options.refreshTokenExpiresIn,
|
|
53
|
+
}
|
|
54
|
+
: undefined);
|
|
55
|
+
refreshToken = generatedRefreshToken.value.release();
|
|
56
|
+
}
|
|
47
57
|
if (this.#options.useCookies) {
|
|
48
|
-
|
|
58
|
+
this.#ctx.response.cookie(`${this.#tokenName}`, token, {
|
|
59
|
+
httpOnly: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (this.#options.useCookiesForRefreshToken && refreshToken) {
|
|
63
|
+
this.#ctx.response.cookie(`${this.#refreshTokenName}`, refreshToken, {
|
|
49
64
|
httpOnly: true,
|
|
50
65
|
});
|
|
51
66
|
}
|
|
@@ -53,6 +68,8 @@ export class JwtGuard {
|
|
|
53
68
|
type: 'bearer',
|
|
54
69
|
token: token,
|
|
55
70
|
expiresIn: this.#options.expiresIn,
|
|
71
|
+
refreshToken: refreshToken,
|
|
72
|
+
refreshTokenExpiresIn: this.#options.refreshTokenExpiresIn,
|
|
56
73
|
};
|
|
57
74
|
}
|
|
58
75
|
/**
|
|
@@ -135,40 +152,15 @@ export class JwtGuard {
|
|
|
135
152
|
this.user.currentToken = token;
|
|
136
153
|
return this.getUserOrFail();
|
|
137
154
|
}
|
|
138
|
-
async
|
|
139
|
-
/**
|
|
140
|
-
* Avoid re-authentication when it has been done already
|
|
141
|
-
* for the given request
|
|
142
|
-
*/
|
|
143
|
-
if (this.authenticationAttempted) {
|
|
144
|
-
return this.getUserOrFail();
|
|
145
|
-
}
|
|
155
|
+
async generateWithRefreshToken(refreshToken) {
|
|
146
156
|
this.authenticationAttempted = true;
|
|
147
|
-
/**
|
|
148
|
-
* Ensure the refresh token user provider is defined
|
|
149
|
-
*/
|
|
150
157
|
if (!this.#refreshTokenUserProvider) {
|
|
151
158
|
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
152
159
|
guardDriverName: this.driverName,
|
|
153
160
|
});
|
|
154
161
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Ensure the auth header exists
|
|
157
|
-
*/
|
|
158
|
-
const authHeader = this.#ctx.request.header('authorization');
|
|
159
|
-
if (!authHeader) {
|
|
160
|
-
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
161
|
-
guardDriverName: this.driverName,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Split the header value and read the token from it
|
|
166
|
-
*/
|
|
167
|
-
const [, refreshToken] = authHeader.split('Bearer ');
|
|
168
162
|
if (!refreshToken) {
|
|
169
|
-
|
|
170
|
-
guardDriverName: this.driverName,
|
|
171
|
-
});
|
|
163
|
+
refreshToken = await this.#findRefreshToken();
|
|
172
164
|
}
|
|
173
165
|
const accessToken = await this.#refreshTokenUserProvider.verifyToken(new Secret(refreshToken));
|
|
174
166
|
if (!accessToken) {
|
|
@@ -187,10 +179,6 @@ export class JwtGuard {
|
|
|
187
179
|
}
|
|
188
180
|
this.isAuthenticated = true;
|
|
189
181
|
this.user = providerUser.getOriginal();
|
|
190
|
-
/**
|
|
191
|
-
* Get the same abilities for the new refresh token
|
|
192
|
-
*/
|
|
193
|
-
const abilities = accessToken.abilities;
|
|
194
182
|
/**
|
|
195
183
|
* Delete the refresh token from the database
|
|
196
184
|
*/
|
|
@@ -200,16 +188,50 @@ export class JwtGuard {
|
|
|
200
188
|
guardDriverName: this.driverName,
|
|
201
189
|
});
|
|
202
190
|
}
|
|
203
|
-
|
|
204
|
-
|
|
191
|
+
return this.generate(this.user);
|
|
192
|
+
}
|
|
193
|
+
async #findRefreshToken() {
|
|
194
|
+
const bodyToken = this.#ctx.request.input('refreshToken');
|
|
195
|
+
if (bodyToken) {
|
|
196
|
+
return bodyToken;
|
|
197
|
+
}
|
|
198
|
+
if (this.#options.useCookiesForRefreshToken) {
|
|
199
|
+
const cookieToken = this.#ctx.request.cookie(this.#refreshTokenName);
|
|
200
|
+
if (cookieToken) {
|
|
201
|
+
return cookieToken;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const authHeader = this.#ctx.request.header('authorization');
|
|
205
|
+
if (authHeader) {
|
|
206
|
+
if (authHeader.toLowerCase().startsWith('bearer ')) {
|
|
207
|
+
const token = authHeader.slice(7).trim();
|
|
208
|
+
if (token) {
|
|
209
|
+
return token;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
214
|
+
guardDriverName: this.driverName,
|
|
205
215
|
});
|
|
206
|
-
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Revoke the refresh token
|
|
219
|
+
*/
|
|
220
|
+
async revoke(refreshToken) {
|
|
221
|
+
if (!this.#refreshTokenUserProvider) {
|
|
207
222
|
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
208
223
|
guardDriverName: this.driverName,
|
|
209
224
|
});
|
|
210
225
|
}
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
if (!refreshToken) {
|
|
227
|
+
try {
|
|
228
|
+
refreshToken = await this.#findRefreshToken();
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
await this.#refreshTokenUserProvider.invalidateToken(new Secret(refreshToken));
|
|
213
235
|
}
|
|
214
236
|
/**
|
|
215
237
|
* Same as authenticate, but does not throw an exception
|
package/build/src/types.d.ts
CHANGED
|
@@ -43,6 +43,9 @@ export type JwtGuardOptions<RealUser extends any = unknown> = {
|
|
|
43
43
|
refreshTokenUserProvider?: AccessTokensUserProviderContract<RealUser>;
|
|
44
44
|
tokenName?: string;
|
|
45
45
|
expiresIn?: number | StringValue;
|
|
46
|
+
refreshTokenExpiresIn?: number | StringValue;
|
|
46
47
|
useCookies?: boolean;
|
|
48
|
+
useCookiesForRefreshToken?: boolean;
|
|
49
|
+
refreshTokenAbilities?: string[];
|
|
47
50
|
content?: (user: JwtGuardUser<RealUser>) => Record<string, any>;
|
|
48
51
|
};
|
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.7.0-alpha.1",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.6.0"
|
|
7
7
|
},
|
|
@@ -35,32 +35,32 @@
|
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@adonisjs/assembler": "^7.8.2",
|
|
38
|
-
"@adonisjs/auth": "^9.
|
|
39
|
-
"@adonisjs/core": "^6.
|
|
40
|
-
"@adonisjs/eslint-config": "^2.
|
|
41
|
-
"@adonisjs/lucid": "^21.
|
|
42
|
-
"@adonisjs/prettier-config": "^1.4.
|
|
43
|
-
"@adonisjs/tsconfig": "^1.4.
|
|
44
|
-
"@japa/assert": "^4.0
|
|
45
|
-
"@japa/file-system": "^
|
|
46
|
-
"@japa/runner": "^
|
|
47
|
-
"@swc/core": "^1.
|
|
38
|
+
"@adonisjs/auth": "^9.5.1",
|
|
39
|
+
"@adonisjs/core": "^6.19.3",
|
|
40
|
+
"@adonisjs/eslint-config": "^2.1.2",
|
|
41
|
+
"@adonisjs/lucid": "^21.8.2",
|
|
42
|
+
"@adonisjs/prettier-config": "^1.4.5",
|
|
43
|
+
"@adonisjs/tsconfig": "^1.4.1",
|
|
44
|
+
"@japa/assert": "^4.2.0",
|
|
45
|
+
"@japa/file-system": "^3.0.0",
|
|
46
|
+
"@japa/runner": "^5.3.0",
|
|
47
|
+
"@swc/core": "^1.15.8",
|
|
48
48
|
"@types/node": "^22.15.18",
|
|
49
|
-
"c8": "^
|
|
49
|
+
"c8": "^10.1.3",
|
|
50
50
|
"copyfiles": "^2.4.1",
|
|
51
|
-
"del-cli": "^
|
|
52
|
-
"eslint": "^9.
|
|
53
|
-
"luxon": "^3.
|
|
54
|
-
"np": "^10.
|
|
55
|
-
"prettier": "^3.
|
|
51
|
+
"del-cli": "^7.0.0",
|
|
52
|
+
"eslint": "^9.39.2",
|
|
53
|
+
"luxon": "^3.7.2",
|
|
54
|
+
"np": "^10.3.0",
|
|
55
|
+
"prettier": "^3.8.0",
|
|
56
56
|
"sqlite3": "^5.1.7",
|
|
57
57
|
"timekeeper": "^2.3.1",
|
|
58
|
-
"ts-node-maintained": "^10.9.
|
|
59
|
-
"typescript": "~5.
|
|
58
|
+
"ts-node-maintained": "^10.9.6",
|
|
59
|
+
"typescript": "~5.9.3"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@types/jsonwebtoken": "^9.0.
|
|
63
|
-
"jsonwebtoken": "^9.0.
|
|
62
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
63
|
+
"jsonwebtoken": "^9.0.3",
|
|
64
64
|
"ms": "^2.1.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|