@maximemrf/adonisjs-jwt 0.6.1 → 0.7.0-alpha.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
@@ -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 {
@@ -180,7 +186,9 @@ router.post('login', async ({ request, auth }) => {
180
186
  const { email, password } = request.all()
181
187
  const user = await User.verifyCredentials(email, password)
182
188
 
183
- // to generate a token
189
+ // to generate a token (and refresh token if configured)
190
+ // this returns { type, token, expiresIn, refreshToken, refreshTokenExpiresIn }
191
+ // if useCookies is true, it sets cookies on the response instead
184
192
  return await auth.use('jwt').generate(user)
185
193
  })
186
194
 
@@ -196,25 +204,21 @@ router.get('/', async ({ auth }) => {
196
204
  })
197
205
  .use(middleware.auth({ guards: ['jwt'] }))
198
206
 
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
207
  // if you use the refresh token
205
208
  router.post('jwt/refresh', async ({ auth }) => {
206
209
  // 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
- })
210
+ // it will delete the old refresh token and generate a new one
211
+ // it accepts an optional refresh token, otherwise it looks in:
212
+ // 1. request body 'refreshToken'
213
+ // 2. cookies (if enabled)
214
+ // 3. Authorization header
215
+ return await auth.use('jwt').generateWithRefreshToken()
216
+ })
217
+
218
+ // to logout (revoke refresh token)
219
+ router.post('logout', async ({ auth }) => {
220
+ await auth.use('jwt').revoke()
221
+ return { message: 'Logged out' }
218
222
  })
219
223
  ```
220
224
 
@@ -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);
@@ -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<import("@adonisjs/core/http").Response | {
37
+ generate(user: UserProvider[typeof symbols.PROVIDER_REAL_USER]): Promise<{
37
38
  type: string;
38
39
  token: string;
39
- expiresIn: number | import("ms").StringValue | undefined;
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
- authenticateWithRefreshToken(name?: string): Promise<UserProvider[typeof symbols.PROVIDER_REAL_USER] & {
50
- currentToken: string;
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
- return this.#ctx.response.cookie(`${this.#tokenName}`, token, {
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 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
- }
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
- throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
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
- const newRefreshToken = await this.#refreshTokenUserProvider.createToken(this.user, abilities, {
204
- name,
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
- if (!newRefreshToken.value) {
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
- this.user.currentToken = newRefreshToken.value?.release();
212
- return this.getUserOrFail();
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
@@ -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.6.1",
4
+ "version": "0.7.0-alpha.0",
5
5
  "engines": {
6
6
  "node": ">=20.6.0"
7
7
  },
@@ -35,31 +35,31 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@adonisjs/assembler": "^7.8.2",
38
- "@adonisjs/auth": "^9.4.0",
39
- "@adonisjs/core": "^6.17.2",
40
- "@adonisjs/eslint-config": "^2.0.0",
41
- "@adonisjs/lucid": "^21.0.0",
42
- "@adonisjs/prettier-config": "^1.4.4",
43
- "@adonisjs/tsconfig": "^1.4.0",
44
- "@japa/assert": "^4.0.1",
38
+ "@adonisjs/auth": "^9.5.0",
39
+ "@adonisjs/core": "^6.19.0",
40
+ "@adonisjs/eslint-config": "^2.1.2",
41
+ "@adonisjs/lucid": "^21.8.0",
42
+ "@adonisjs/prettier-config": "^1.4.5",
43
+ "@adonisjs/tsconfig": "^1.4.1",
44
+ "@japa/assert": "^4.1.1",
45
45
  "@japa/file-system": "^2.3.2",
46
- "@japa/runner": "^4.2.0",
47
- "@swc/core": "^1.12.5",
46
+ "@japa/runner": "^4.4.0",
47
+ "@swc/core": "^1.13.5",
48
48
  "@types/node": "^22.15.18",
49
- "c8": "^9.1.0",
49
+ "c8": "^10.1.3",
50
50
  "copyfiles": "^2.4.1",
51
- "del-cli": "^5.1.0",
52
- "eslint": "^9.26.0",
53
- "luxon": "^3.4.4",
54
- "np": "^10.0.0",
55
- "prettier": "^3.5.3",
51
+ "del-cli": "^7.0.0",
52
+ "eslint": "^9.37.0",
53
+ "luxon": "^3.7.2",
54
+ "np": "^10.2.0",
55
+ "prettier": "^3.6.2",
56
56
  "sqlite3": "^5.1.7",
57
57
  "timekeeper": "^2.3.1",
58
- "ts-node-maintained": "^10.9.5",
59
- "typescript": "~5.8"
58
+ "ts-node-maintained": "^10.9.6",
59
+ "typescript": "~5.9.3"
60
60
  },
61
61
  "dependencies": {
62
- "@types/jsonwebtoken": "^9.0.6",
62
+ "@types/jsonwebtoken": "^9.0.10",
63
63
  "jsonwebtoken": "^9.0.2",
64
64
  "ms": "^2.1.3"
65
65
  },