@maximemrf/adonisjs-jwt 0.4.0 → 0.6.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 +97 -5
- package/build/src/define_config.d.ts +3 -0
- package/build/src/define_config.js +2 -1
- package/build/src/jwt.d.ts +3 -0
- package/build/src/jwt.js +73 -0
- package/build/src/types.d.ts +2 -0
- package/package.json +5 -3
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,9 +35,11 @@ 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'
|
|
42
|
+
import env from '#start/env'
|
|
41
43
|
|
|
42
44
|
interface JwtContent extends BaseJwtContent {
|
|
43
45
|
email: string
|
|
@@ -61,9 +63,18 @@ const authConfig = defineConfig({
|
|
|
61
63
|
tokenExpiresIn: '1h',
|
|
62
64
|
// if you want to use cookies for the authentication instead of the bearer token (optional)
|
|
63
65
|
useCookies: true,
|
|
66
|
+
// secret is the secret used to sign the token, it can be optional, by default it uses the application key
|
|
67
|
+
// you can use a env variable like JWT_SECRET or set it directly with a string
|
|
68
|
+
// if you don't have specific needs, please discard this option
|
|
69
|
+
secret: env.get('JWT_SECRET'),
|
|
64
70
|
provider: sessionUserProvider({
|
|
65
71
|
model: () => import('#models/user'),
|
|
66
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
|
+
}),
|
|
67
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
|
|
68
79
|
content: <T>(user: JwtGuardUser<T>): JwtContent => {
|
|
69
80
|
return {
|
|
@@ -76,13 +87,13 @@ const authConfig = defineConfig({
|
|
|
76
87
|
})
|
|
77
88
|
```
|
|
78
89
|
|
|
79
|
-
`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`.
|
|
80
91
|
|
|
81
92
|
```typescript
|
|
82
93
|
tokenName: 'custom-name'
|
|
83
94
|
```
|
|
84
95
|
|
|
85
|
-
`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.
|
|
86
97
|
|
|
87
98
|
```typescript
|
|
88
99
|
// string
|
|
@@ -99,7 +110,68 @@ useCookies: true
|
|
|
99
110
|
|
|
100
111
|
If you just want to use jwt with the bearer token no need to set `useCookies` to `false` you can just remove it.
|
|
101
112
|
|
|
102
|
-
##
|
|
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
|
+
|
|
174
|
+
## Authentication
|
|
103
175
|
|
|
104
176
|
To make a protected route, you have to use the `auth` middleware with the `jwt` guard.
|
|
105
177
|
|
|
@@ -123,6 +195,26 @@ router.get('/', async ({ auth }) => {
|
|
|
123
195
|
return auth.use('jwt').getUserOrFail()
|
|
124
196
|
})
|
|
125
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
|
|
208
|
+
const user = await auth.use('jwt').authenticateWithRefreshToken()
|
|
209
|
+
const newRefreshToken = user.currentToken
|
|
210
|
+
const newToken = await auth.use('jwt').generate(user)
|
|
211
|
+
|
|
212
|
+
return response.ok({
|
|
213
|
+
token: newToken,
|
|
214
|
+
refreshToken: newRefreshToken,
|
|
215
|
+
...user.serialize(),
|
|
216
|
+
})
|
|
217
|
+
})
|
|
126
218
|
```
|
|
127
219
|
|
|
128
220
|
## Security
|
|
@@ -3,10 +3,13 @@ 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;
|
|
13
|
+
secret?: string;
|
|
11
14
|
content: <T>(user: JwtGuardUser<T>) => Record<string | number, any>;
|
|
12
15
|
}): GuardConfigProvider<(ctx: HttpContext) => JwtGuard<UserProvider>>;
|
|
@@ -4,7 +4,8 @@ export function jwtGuard(config) {
|
|
|
4
4
|
async resolver(_, app) {
|
|
5
5
|
const appKey = app.config.get('app.appKey').release();
|
|
6
6
|
const options = {
|
|
7
|
-
secret: appKey,
|
|
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(): 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,76 @@ export class JwtGuard {
|
|
|
132
135
|
this.user.currentToken = token;
|
|
133
136
|
return this.getUserOrFail();
|
|
134
137
|
}
|
|
138
|
+
async authenticateWithRefreshToken() {
|
|
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
|
+
* Delete the refresh token from the database
|
|
192
|
+
*/
|
|
193
|
+
const isDeleted = await this.#refreshTokenUserProvider.invalidateToken(new Secret(refreshToken));
|
|
194
|
+
if (!isDeleted) {
|
|
195
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
196
|
+
guardDriverName: this.driverName,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const newRefreshToken = await this.#refreshTokenUserProvider.createToken(this.user);
|
|
200
|
+
if (!newRefreshToken.value) {
|
|
201
|
+
throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
|
|
202
|
+
guardDriverName: this.driverName,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
this.user.currentToken = newRefreshToken.value?.release();
|
|
206
|
+
return this.getUserOrFail();
|
|
207
|
+
}
|
|
135
208
|
/**
|
|
136
209
|
* Same as authenticate, but does not throw an exception
|
|
137
210
|
*/
|
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.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.6.0"
|
|
7
7
|
},
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"typecheck": "tsc --noEmit",
|
|
24
24
|
"lint": "eslint .",
|
|
25
25
|
"format": "prettier --write .",
|
|
26
|
-
"quick:test": "node --import
|
|
26
|
+
"quick:test": "node --import=ts-node-maintained/register/esm --enable-source-maps --experimental-import-meta-resolve bin/test.ts --force-exit",
|
|
27
27
|
"pretest": "npm run lint",
|
|
28
28
|
"test": "c8 npm run quick:test",
|
|
29
29
|
"prebuild": "npm run lint && npm run clean",
|
|
@@ -42,6 +42,7 @@
|
|
|
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
47
|
"@swc/core": "1.11.24",
|
|
47
48
|
"@types/node": "^22.15.18",
|
|
@@ -52,8 +53,9 @@
|
|
|
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
|
-
"ts-node": "^10.9.
|
|
58
|
+
"ts-node-maintained": "^10.9.5",
|
|
57
59
|
"typescript": "~5.8"
|
|
58
60
|
},
|
|
59
61
|
"dependencies": {
|