@maximemrf/adonisjs-jwt 0.5.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 CHANGED
@@ -13,10 +13,10 @@
13
13
 
14
14
  ## Prerequisites
15
15
 
16
- You have to install the auth package from AdonisJS with the session guard because the jwt package use some components from the session guard.
16
+ You have to install the auth package from AdonisJS
17
17
 
18
18
  ```bash
19
- node ace add @adonisjs/auth --guard=session
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,26 @@ 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
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
+ })
131
218
  ```
132
219
 
133
220
  ## 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,
@@ -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
  */
@@ -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.5.0",
4
+ "version": "0.6.0",
5
5
  "engines": {
6
6
  "node": ">=20.6.0"
7
7
  },
@@ -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,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"