@maximemrf/adonisjs-jwt 0.5.0 → 0.6.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 +92 -4
- package/build/src/define_config.d.ts +2 -0
- package/build/src/define_config.js +1 -0
- package/build/src/jwt.d.ts +3 -0
- package/build/src/jwt.js +79 -0
- package/build/src/types.d.ts +2 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
## Prerequisites
|
|
15
15
|
|
|
16
|
-
You have to install the auth package from AdonisJS
|
|
16
|
+
You have to install the auth package from AdonisJS
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
node ace add @adonisjs/auth
|
|
19
|
+
node ace add @adonisjs/auth
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Setup
|
|
@@ -35,6 +35,7 @@ Go to `config/auth.ts` and add the following configuration:
|
|
|
35
35
|
import { defineConfig } from '@adonisjs/auth'
|
|
36
36
|
import { InferAuthEvents, Authenticators } from '@adonisjs/auth/types'
|
|
37
37
|
import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session'
|
|
38
|
+
import { tokensUserProvider } from '@adonisjs/auth/access_tokens'
|
|
38
39
|
import { jwtGuard } from '@maximemrf/adonisjs-jwt/jwt_config'
|
|
39
40
|
import { JwtGuardUser, BaseJwtContent } from '@maximemrf/adonisjs-jwt/types'
|
|
40
41
|
import User from '#models/user'
|
|
@@ -69,6 +70,11 @@ const authConfig = defineConfig({
|
|
|
69
70
|
provider: sessionUserProvider({
|
|
70
71
|
model: () => import('#models/user'),
|
|
71
72
|
}),
|
|
73
|
+
// if you want to use refresh tokens, you have to set the refreshTokenUserProvider
|
|
74
|
+
refreshTokenUserProvider: tokensUserProvider({
|
|
75
|
+
tokens: 'refreshTokens',
|
|
76
|
+
model: () => import('#models/user'),
|
|
77
|
+
}),
|
|
72
78
|
// 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
|
|
73
79
|
content: <T>(user: JwtGuardUser<T>): JwtContent => {
|
|
74
80
|
return {
|
|
@@ -81,13 +87,13 @@ const authConfig = defineConfig({
|
|
|
81
87
|
})
|
|
82
88
|
```
|
|
83
89
|
|
|
84
|
-
`tokenName` is the name of the token passed as a cookie, it can be optional, by default it is `token`.
|
|
90
|
+
`tokenName` is the name of the jwt token passed as a cookie, it can be optional, by default it is `token`.
|
|
85
91
|
|
|
86
92
|
```typescript
|
|
87
93
|
tokenName: 'custom-name'
|
|
88
94
|
```
|
|
89
95
|
|
|
90
|
-
`tokenExpiresIn` is the time before the token expires it can be a string or a number and it can be optional.
|
|
96
|
+
`tokenExpiresIn` is the time before the jwt token expires it can be a string or a number and it can be optional.
|
|
91
97
|
|
|
92
98
|
```typescript
|
|
93
99
|
// string
|
|
@@ -104,6 +110,67 @@ useCookies: true
|
|
|
104
110
|
|
|
105
111
|
If you just want to use jwt with the bearer token no need to set `useCookies` to `false` you can just remove it.
|
|
106
112
|
|
|
113
|
+
## Refresh Tokens
|
|
114
|
+
|
|
115
|
+
To use refresh tokens, you have to set the `refreshTokenUserProvider` in the guard configuration, see the example above.
|
|
116
|
+
|
|
117
|
+
Create a new AdonisJS migration file and run it to create the `jwt_refresh_tokens` table:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
121
|
+
|
|
122
|
+
export default class extends BaseSchema {
|
|
123
|
+
protected tableName = 'jwt_refresh_tokens'
|
|
124
|
+
|
|
125
|
+
async up() {
|
|
126
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
127
|
+
table.increments()
|
|
128
|
+
table.integer('tokenable_id').notNullable().unsigned()
|
|
129
|
+
table.string('type').notNullable()
|
|
130
|
+
table.string('name').nullable()
|
|
131
|
+
table.string('hash', 80).notNullable()
|
|
132
|
+
table.text('abilities').notNullable()
|
|
133
|
+
table.timestamp('created_at', { precision: 6, useTz: true }).notNullable()
|
|
134
|
+
table.timestamp('updated_at', { precision: 6, useTz: true }).notNullable()
|
|
135
|
+
table.timestamp('expires_at', { precision: 6, useTz: true }).nullable()
|
|
136
|
+
table.timestamp('last_used_at', { precision: 6, useTz: true }).nullable()
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async down() {
|
|
141
|
+
this.schema.dropTable(this.tableName)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
And add the `refreshTokens` property to your User model:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { column, BaseModel } from '@adonisjs/lucid/orm'
|
|
150
|
+
import { DbAccessTokensProvider } from '@adonisjs/auth/access_tokens'
|
|
151
|
+
|
|
152
|
+
export default class User extends BaseModel {
|
|
153
|
+
@column({ isPrimary: true })
|
|
154
|
+
declare id: number
|
|
155
|
+
|
|
156
|
+
@column()
|
|
157
|
+
declare username: string
|
|
158
|
+
|
|
159
|
+
@column()
|
|
160
|
+
declare email: string
|
|
161
|
+
|
|
162
|
+
@column()
|
|
163
|
+
declare password: string
|
|
164
|
+
|
|
165
|
+
static refreshTokens = DbAccessTokensProvider.forModel(User, {
|
|
166
|
+
prefix: 'rt_',
|
|
167
|
+
table: 'jwt_refresh_tokens',
|
|
168
|
+
type: 'jwt_refresh_token',
|
|
169
|
+
tokenSecretLength: 40,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
107
174
|
## Authentication
|
|
108
175
|
|
|
109
176
|
To make a protected route, you have to use the `auth` middleware with the `jwt` guard.
|
|
@@ -128,6 +195,27 @@ router.get('/', async ({ auth }) => {
|
|
|
128
195
|
return auth.use('jwt').getUserOrFail()
|
|
129
196
|
})
|
|
130
197
|
.use(middleware.auth({ guards: ['jwt'] }))
|
|
198
|
+
|
|
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
|
+
// if you use the refresh token
|
|
205
|
+
router.post('jwt/refresh', async ({ auth }) => {
|
|
206
|
+
// this will authenticate the user using the refresh token
|
|
207
|
+
// it will delete the old refresh token and generate a new one with the same abilities
|
|
208
|
+
// You could pass a name for the new token as well
|
|
209
|
+
const user = await auth.use('jwt').authenticateWithRefreshToken('optional_name')
|
|
210
|
+
const newRefreshToken = user.currentToken
|
|
211
|
+
const newToken = await auth.use('jwt').generate(user)
|
|
212
|
+
|
|
213
|
+
return response.ok({
|
|
214
|
+
token: newToken,
|
|
215
|
+
refreshToken: newRefreshToken,
|
|
216
|
+
...user.serialize(),
|
|
217
|
+
})
|
|
218
|
+
})
|
|
131
219
|
```
|
|
132
220
|
|
|
133
221
|
## Security
|
|
@@ -3,8 +3,10 @@ import type { HttpContext } from '@adonisjs/core/http';
|
|
|
3
3
|
import { JwtGuardUser, JwtUserProviderContract } from './types.js';
|
|
4
4
|
import { JwtGuard } from './jwt.js';
|
|
5
5
|
import type { StringValue } from 'ms';
|
|
6
|
+
import { AccessTokensUserProviderContract } from '@adonisjs/auth/types/access_tokens';
|
|
6
7
|
export declare function jwtGuard<UserProvider extends JwtUserProviderContract<unknown>>(config: {
|
|
7
8
|
provider: UserProvider;
|
|
9
|
+
refreshTokenUserProvider?: AccessTokensUserProviderContract<unknown>;
|
|
8
10
|
tokenName?: string;
|
|
9
11
|
tokenExpiresIn?: number | StringValue;
|
|
10
12
|
useCookies?: boolean;
|
|
@@ -5,6 +5,7 @@ export function jwtGuard(config) {
|
|
|
5
5
|
const appKey = app.config.get('app.appKey').release();
|
|
6
6
|
const options = {
|
|
7
7
|
secret: config.secret ?? appKey,
|
|
8
|
+
refreshTokenUserProvider: config.refreshTokenUserProvider,
|
|
8
9
|
tokenName: config.tokenName,
|
|
9
10
|
expiresIn: config.tokenExpiresIn,
|
|
10
11
|
useCookies: config.useCookies,
|
package/build/src/jwt.d.ts
CHANGED
|
@@ -46,6 +46,9 @@ export declare class JwtGuard<UserProvider extends JwtUserProviderContract<unkno
|
|
|
46
46
|
authenticate(): Promise<UserProvider[typeof symbols.PROVIDER_REAL_USER] & {
|
|
47
47
|
currentToken: string;
|
|
48
48
|
}>;
|
|
49
|
+
authenticateWithRefreshToken(name?: string): Promise<UserProvider[typeof symbols.PROVIDER_REAL_USER] & {
|
|
50
|
+
currentToken: string;
|
|
51
|
+
}>;
|
|
49
52
|
/**
|
|
50
53
|
* Same as authenticate, but does not throw an exception
|
|
51
54
|
*/
|
package/build/src/jwt.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { errors } from '@adonisjs/auth';
|
|
2
2
|
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { Secret } from '@adonisjs/core/helpers';
|
|
3
4
|
export class JwtGuard {
|
|
4
5
|
#ctx;
|
|
5
6
|
#userProvider;
|
|
7
|
+
#refreshTokenUserProvider;
|
|
6
8
|
#options;
|
|
7
9
|
#tokenName;
|
|
8
10
|
constructor(ctx, userProvider, option) {
|
|
9
11
|
this.#ctx = ctx;
|
|
10
12
|
this.#userProvider = userProvider;
|
|
11
13
|
this.#options = option;
|
|
14
|
+
this.#refreshTokenUserProvider = this.#options.refreshTokenUserProvider;
|
|
12
15
|
if (!this.#options.content)
|
|
13
16
|
this.#options.content = (user) => ({ userId: user.getId() });
|
|
14
17
|
this.#tokenName = this.#options.tokenName ?? 'token';
|
|
@@ -132,6 +135,82 @@ export class JwtGuard {
|
|
|
132
135
|
this.user.currentToken = token;
|
|
133
136
|
return this.getUserOrFail();
|
|
134
137
|
}
|
|
138
|
+
async authenticateWithRefreshToken(name) {
|
|
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
|
+
}
|
|
146
|
+
this.authenticationAttempted = true;
|
|
147
|
+
/**
|
|
148
|
+
* Ensure the refresh token user provider is defined
|
|
149
|
+
*/
|
|
150
|
+
if (!this.#refreshTokenUserProvider) {
|
|
151
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
152
|
+
guardDriverName: this.driverName,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
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
|
+
if (!refreshToken) {
|
|
169
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
170
|
+
guardDriverName: this.driverName,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const accessToken = await this.#refreshTokenUserProvider.verifyToken(new Secret(refreshToken));
|
|
174
|
+
if (!accessToken) {
|
|
175
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
176
|
+
guardDriverName: this.driverName,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Fetch the user by user ID
|
|
181
|
+
*/
|
|
182
|
+
const providerUser = await this.#refreshTokenUserProvider.findById(accessToken.tokenableId);
|
|
183
|
+
if (!providerUser) {
|
|
184
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
185
|
+
guardDriverName: this.driverName,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
this.isAuthenticated = true;
|
|
189
|
+
this.user = providerUser.getOriginal();
|
|
190
|
+
/**
|
|
191
|
+
* Get the same abilities for the new refresh token
|
|
192
|
+
*/
|
|
193
|
+
const abilities = accessToken.abilities;
|
|
194
|
+
/**
|
|
195
|
+
* Delete the refresh token from the database
|
|
196
|
+
*/
|
|
197
|
+
const isDeleted = await this.#refreshTokenUserProvider.invalidateToken(new Secret(refreshToken));
|
|
198
|
+
if (!isDeleted) {
|
|
199
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
200
|
+
guardDriverName: this.driverName,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const newRefreshToken = await this.#refreshTokenUserProvider.createToken(this.user, abilities, {
|
|
204
|
+
name,
|
|
205
|
+
});
|
|
206
|
+
if (!newRefreshToken.value) {
|
|
207
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
208
|
+
guardDriverName: this.driverName,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
this.user.currentToken = newRefreshToken.value?.release();
|
|
212
|
+
return this.getUserOrFail();
|
|
213
|
+
}
|
|
135
214
|
/**
|
|
136
215
|
* Same as authenticate, but does not throw an exception
|
|
137
216
|
*/
|
package/build/src/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { symbols } from '@adonisjs/auth';
|
|
2
2
|
import type { StringValue } from 'ms';
|
|
3
|
+
import { AccessTokensUserProviderContract } from '@adonisjs/auth/types/access_tokens';
|
|
3
4
|
/**
|
|
4
5
|
* The bridge between the User provider and the
|
|
5
6
|
* Guard
|
|
@@ -39,6 +40,7 @@ export type BaseJwtContent = {
|
|
|
39
40
|
};
|
|
40
41
|
export type JwtGuardOptions<RealUser extends any = unknown> = {
|
|
41
42
|
secret: string;
|
|
43
|
+
refreshTokenUserProvider?: AccessTokensUserProviderContract<RealUser>;
|
|
42
44
|
tokenName?: string;
|
|
43
45
|
expiresIn?: number | StringValue;
|
|
44
46
|
useCookies?: boolean;
|
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.6.1",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.6.0"
|
|
7
7
|
},
|
|
@@ -42,8 +42,9 @@
|
|
|
42
42
|
"@adonisjs/prettier-config": "^1.4.4",
|
|
43
43
|
"@adonisjs/tsconfig": "^1.4.0",
|
|
44
44
|
"@japa/assert": "^4.0.1",
|
|
45
|
+
"@japa/file-system": "^2.3.2",
|
|
45
46
|
"@japa/runner": "^4.2.0",
|
|
46
|
-
"@swc/core": "1.
|
|
47
|
+
"@swc/core": "^1.12.5",
|
|
47
48
|
"@types/node": "^22.15.18",
|
|
48
49
|
"c8": "^9.1.0",
|
|
49
50
|
"copyfiles": "^2.4.1",
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
"luxon": "^3.4.4",
|
|
53
54
|
"np": "^10.0.0",
|
|
54
55
|
"prettier": "^3.5.3",
|
|
56
|
+
"sqlite3": "^5.1.7",
|
|
55
57
|
"timekeeper": "^2.3.1",
|
|
56
58
|
"ts-node-maintained": "^10.9.5",
|
|
57
59
|
"typescript": "~5.8"
|