@lenne.tech/nest-server 9.2.1 → 9.2.2

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.
Files changed (66) hide show
  1. package/README.md +1 -2
  2. package/dist/config.env.js +3 -0
  3. package/dist/config.env.js.map +1 -1
  4. package/dist/core/common/helpers/context.helper.d.ts +5 -2
  5. package/dist/core/common/helpers/context.helper.js +14 -8
  6. package/dist/core/common/helpers/context.helper.js.map +1 -1
  7. package/dist/core/common/interfaces/server-options.interface.d.ts +3 -1
  8. package/dist/core/common/pipes/check-input.pipe.js +2 -2
  9. package/dist/core/common/pipes/check-input.pipe.js.map +1 -1
  10. package/dist/core/modules/auth/core-auth.module.js +5 -1
  11. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  12. package/dist/core/modules/auth/core-auth.resolver.d.ts +6 -6
  13. package/dist/core/modules/auth/core-auth.resolver.js +25 -22
  14. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  15. package/dist/core/modules/auth/guards/auth.guard.js +9 -2
  16. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  17. package/dist/core/modules/auth/inputs/core-auth-sign-in.input.d.ts +1 -0
  18. package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js +6 -1
  19. package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js.map +1 -1
  20. package/dist/core/modules/auth/inputs/core-auth-sign-up.input.d.ts +2 -5
  21. package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js +2 -23
  22. package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js.map +1 -1
  23. package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +2 -2
  24. package/dist/core/modules/auth/interfaces/core-token-data.interface.d.ts +5 -0
  25. package/dist/core/modules/auth/interfaces/core-token-data.interface.js +3 -0
  26. package/dist/core/modules/auth/interfaces/core-token-data.interface.js.map +1 -0
  27. package/dist/core/modules/auth/interfaces/jwt-payload.interface.d.ts +3 -0
  28. package/dist/core/modules/auth/services/core-auth.service.d.ts +11 -9
  29. package/dist/core/modules/auth/services/core-auth.service.js +62 -50
  30. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  31. package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js +1 -10
  32. package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js.map +1 -1
  33. package/dist/core/modules/auth/strategies/jwt.strategy.js +1 -1
  34. package/dist/core/modules/auth/strategies/jwt.strategy.js.map +1 -1
  35. package/dist/core/modules/auth/tokens.decorator.d.ts +1 -0
  36. package/dist/core/modules/auth/tokens.decorator.js +20 -0
  37. package/dist/core/modules/auth/tokens.decorator.js.map +1 -0
  38. package/dist/core/modules/user/core-user.model.d.ts +2 -2
  39. package/dist/core/modules/user/core-user.model.js +0 -6
  40. package/dist/core/modules/user/core-user.model.js.map +1 -1
  41. package/dist/core.module.js +1 -1
  42. package/dist/core.module.js.map +1 -1
  43. package/dist/index.d.ts +4 -2
  44. package/dist/index.js +4 -2
  45. package/dist/index.js.map +1 -1
  46. package/dist/tsconfig.build.tsbuildinfo +1 -1
  47. package/package.json +1 -1
  48. package/src/config.env.ts +3 -0
  49. package/src/core/common/helpers/context.helper.ts +25 -10
  50. package/src/core/common/interfaces/server-options.interface.ts +11 -1
  51. package/src/core/common/pipes/check-input.pipe.ts +2 -2
  52. package/src/core/modules/auth/core-auth.module.ts +5 -1
  53. package/src/core/modules/auth/core-auth.resolver.ts +22 -19
  54. package/src/core/modules/auth/guards/auth.guard.ts +9 -2
  55. package/src/core/modules/auth/inputs/core-auth-sign-in.input.ts +4 -1
  56. package/src/core/modules/auth/inputs/core-auth-sign-up.input.ts +3 -16
  57. package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +3 -6
  58. package/src/core/modules/auth/interfaces/core-token-data.interface.ts +19 -0
  59. package/src/core/modules/auth/interfaces/jwt-payload.interface.ts +3 -0
  60. package/src/core/modules/auth/services/core-auth.service.ts +93 -81
  61. package/src/core/modules/auth/strategies/jwt-refresh.strategy.ts +1 -11
  62. package/src/core/modules/auth/strategies/jwt.strategy.ts +1 -1
  63. package/src/core/modules/auth/tokens.decorator.ts +36 -0
  64. package/src/core/modules/user/core-user.model.ts +5 -11
  65. package/src/core.module.ts +1 -2
  66. package/src/index.ts +4 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "9.2.1",
3
+ "version": "9.2.2",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
package/src/config.env.ts CHANGED
@@ -62,6 +62,7 @@ const config: { [env: string]: IServerOptions } = {
62
62
  expiresIn: '15m',
63
63
  },
64
64
  refresh: {
65
+ renewal: true,
65
66
  secret: 'SECRET_OR_PRIVATE_KEY_LOCAL_REFRESH',
66
67
  signInOptions: {
67
68
  expiresIn: '7d',
@@ -134,6 +135,7 @@ const config: { [env: string]: IServerOptions } = {
134
135
  expiresIn: '15m',
135
136
  },
136
137
  refresh: {
138
+ renewal: true,
137
139
  secret: 'SECRET_OR_PRIVATE_KEY_DEV_REFRESH',
138
140
  signInOptions: {
139
141
  expiresIn: '7d',
@@ -206,6 +208,7 @@ const config: { [env: string]: IServerOptions } = {
206
208
  expiresIn: '15m',
207
209
  },
208
210
  refresh: {
211
+ renewal: true,
209
212
  secret: 'SECRET_OR_PRIVATE_KEY_PROD_REFRESH',
210
213
  signInOptions: {
211
214
  expiresIn: '7d',
@@ -1,5 +1,6 @@
1
1
  import { ExecutionContext } from '@nestjs/common';
2
2
  import { GqlExecutionContext } from '@nestjs/graphql';
3
+ import { Request as RequestType } from 'express';
3
4
 
4
5
  /**
5
6
  * Helper for context processing
@@ -10,7 +11,11 @@ export default class Context {
10
11
  * Get data from Context
11
12
  * @deprecated use getContextData function
12
13
  */
13
- public static getData(context: ExecutionContext): { currentUser: { [key: string]: any }; args: any } {
14
+ public static getData(context: ExecutionContext): {
15
+ args: any;
16
+ currentUser: { [key: string]: any };
17
+ request: RequestType;
18
+ } {
14
19
  return getContextData(context);
15
20
  }
16
21
  }
@@ -18,26 +23,36 @@ export default class Context {
18
23
  /**
19
24
  * Get data from Context
20
25
  */
21
- export function getContextData(context: ExecutionContext): { currentUser: { [key: string]: any }; args: any } {
26
+ export function getContextData(context: ExecutionContext): {
27
+ args: any;
28
+ currentUser: { [key: string]: any };
29
+ request: RequestType;
30
+ } {
22
31
  // Check context
23
32
  if (!context) {
24
- return { currentUser: null, args: null };
33
+ return { currentUser: null, args: null, request: null };
25
34
  }
26
35
 
27
36
  // Init data
28
37
  let user: { [key: string]: any };
38
+ let rawContext: any = null;
29
39
  let ctx: any = null;
40
+ let request: any;
30
41
  try {
31
- ctx = GqlExecutionContext.create(context)?.getContext();
42
+ rawContext = GqlExecutionContext.create(context);
43
+ ctx = rawContext?.getContext();
44
+ request = ctx.req;
32
45
  } catch (e) {
33
46
  // console.info(e);
34
47
  }
35
48
 
36
49
  let args: any;
37
- try {
38
- args = GqlExecutionContext.create(context)?.getArgs();
39
- } catch (e) {
40
- // console.info(e);
50
+ if (rawContext) {
51
+ try {
52
+ args = rawContext.getArgs();
53
+ } catch (e) {
54
+ // console.info(e);
55
+ }
41
56
  }
42
57
 
43
58
  // Get data
@@ -45,7 +60,7 @@ export function getContextData(context: ExecutionContext): { currentUser: { [key
45
60
  // User from GraphQL context
46
61
  user = ctx?.user || ctx?.req?.user;
47
62
  } else {
48
- const request = context?.switchToHttp ? context.switchToHttp()?.getRequest() : null;
63
+ request = context?.switchToHttp ? context.switchToHttp()?.getRequest() : null;
49
64
  if (request) {
50
65
  args = request.body;
51
66
 
@@ -55,5 +70,5 @@ export function getContextData(context: ExecutionContext): { currentUser: { [key
55
70
  }
56
71
 
57
72
  // Return data
58
- return { currentUser: user, args };
73
+ return { args, currentUser: user, request };
59
74
  }
@@ -182,7 +182,17 @@ export interface IServerOptions {
182
182
  * Configuration of JavaScript Web Token (JWT) module
183
183
  */
184
184
  jwt?: {
185
- refresh?: IJwt;
185
+ /**
186
+ * Configuration for refresh Token (JWT)
187
+ */
188
+ refresh?: {
189
+ /**
190
+ * Whether renewal of the refresh token is permitted
191
+ * If falsy (default): during refresh only a new token, the refresh token retains its original term
192
+ * If true: during refresh not only a new token but also a new refresh token is created
193
+ */
194
+ renewal?: boolean;
195
+ } & IJwt;
186
196
  } & IJwt &
187
197
  JwtModuleOptions;
188
198
 
@@ -25,9 +25,9 @@ export class CheckInputPipe implements PipeTransform {
25
25
  const metatype = metadata?.metatype;
26
26
 
27
27
  // Get user
28
- const { user }: any = getContextData(this.context);
28
+ const { currentUser }: any = getContextData(this.context);
29
29
 
30
30
  // Check and return
31
- return check(value, user, { metatype });
31
+ return check(value, currentUser, { metatype });
32
32
  }
33
33
  }
@@ -30,7 +30,11 @@ export class CoreAuthModule {
30
30
  }
31
31
  ): DynamicModule {
32
32
  // Process imports
33
- let imports: any[] = [UserModule, PassportModule.register({ defaultStrategy: 'jwt' }), JwtModule.register(options)];
33
+ let imports: any[] = [
34
+ UserModule,
35
+ PassportModule.register({ defaultStrategy: ['jwt', 'jwt-refresh'] }),
36
+ JwtModule.register(options),
37
+ ];
34
38
  if (Array.isArray(options?.imports)) {
35
39
  imports = imports.concat(options.imports);
36
40
  }
@@ -10,6 +10,7 @@ import { CoreAuthSignInInput } from './inputs/core-auth-sign-in.input';
10
10
  import { CoreAuthSignUpInput } from './inputs/core-auth-sign-up.input';
11
11
  import { ICoreAuthUser } from './interfaces/core-auth-user.interface';
12
12
  import { CoreAuthService } from './services/core-auth.service';
13
+ import { Tokens } from './tokens.decorator';
13
14
 
14
15
  /**
15
16
  * Authentication resolver for the sign in
@@ -25,31 +26,18 @@ export class CoreAuthResolver {
25
26
  // Mutations
26
27
  // ===========================================================================
27
28
 
28
- /**
29
- * Sign in user via email and password (on specific device)
30
- */
31
- @Mutation((returns) => CoreAuthModel, {
32
- description: 'Sign in user via email and password and get JWT tokens (for specific device)',
33
- })
34
- async signIn(
35
- @Info() info: GraphQLResolveInfo,
36
- @Context() ctx: { res: ResponseType },
37
- @Args('input') input: CoreAuthSignInInput
38
- ): Promise<CoreAuthModel> {
39
- const result = await this.authService.signIn(input, { fieldSelection: { info, select: 'signIn' } });
40
- return this.processCookies(ctx, result);
41
- }
42
-
43
29
  /**
44
30
  * Logout user (from specific device)
45
31
  */
32
+ @UseGuards(AuthGuard('jwt'))
46
33
  @Mutation((returns) => CoreAuthModel, { description: 'Logout user (from specific device)' })
47
34
  async logout(
48
35
  @GraphQLUser() currentUser: ICoreAuthUser,
49
36
  @Context() ctx: { res: ResponseType },
50
- @Args('deviceId', { nullable: true }) deviceId?: string
37
+ @Tokens('token') token: string,
38
+ @Args('allDevices', { nullable: true }) allDevices?: boolean
51
39
  ): Promise<boolean> {
52
- const result = await this.authService.logout({ currentUser, deviceId });
40
+ const result = await this.authService.logout(token, { currentUser, allDevices });
53
41
  return this.processCookies(ctx, result);
54
42
  }
55
43
 
@@ -60,10 +48,25 @@ export class CoreAuthResolver {
60
48
  @Mutation((returns) => CoreAuthModel, { description: 'Refresh tokens (for specific device)' })
61
49
  async refreshToken(
62
50
  @GraphQLUser() user: ICoreAuthUser,
51
+ @Tokens('refreshToken') refreshToken: string,
52
+ @Context() ctx: { res: ResponseType }
53
+ ): Promise<CoreAuthModel> {
54
+ const result = await this.authService.refreshTokens(user, refreshToken);
55
+ return this.processCookies(ctx, result);
56
+ }
57
+
58
+ /**
59
+ * Sign in user via email and password (on specific device)
60
+ */
61
+ @Mutation((returns) => CoreAuthModel, {
62
+ description: 'Sign in user via email and password and get JWT tokens (for specific device)',
63
+ })
64
+ async signIn(
65
+ @Info() info: GraphQLResolveInfo,
63
66
  @Context() ctx: { res: ResponseType },
64
- @Args('deviceId', { nullable: true }) deviceId?: string
67
+ @Args('input') input: CoreAuthSignInInput
65
68
  ): Promise<CoreAuthModel> {
66
- const result = await this.authService.refreshTokens(user, deviceId);
69
+ const result = await this.authService.signIn(input, { fieldSelection: { info, select: 'signIn' } });
67
70
  return this.processCookies(ctx, result);
68
71
  }
69
72
 
@@ -3,6 +3,7 @@ import { GqlExecutionContext } from '@nestjs/graphql';
3
3
  import { AuthModuleOptions, Type } from '@nestjs/passport';
4
4
  import { defaultOptions } from '@nestjs/passport/dist/options';
5
5
  import { memoize } from '@nestjs/passport/dist/utils/memoize.util';
6
+ import * as jwt from 'jsonwebtoken';
6
7
  import * as passport from 'passport';
7
8
 
8
9
  /**
@@ -103,8 +104,14 @@ function createAuthGuard(type?: string): Type<CanActivate> {
103
104
  * Process request
104
105
  */
105
106
  handleRequest(err, user, info, context): TUser {
106
- if (err || !user) {
107
- throw err || new UnauthorizedException();
107
+ if (err) {
108
+ if (err instanceof jwt.JsonWebTokenError) {
109
+ throw new UnauthorizedException('Invalid token');
110
+ }
111
+ throw err;
112
+ }
113
+ if (!user) {
114
+ throw new UnauthorizedException('Invalid token');
108
115
  }
109
116
  return user;
110
117
  }
@@ -10,9 +10,12 @@ export class CoreAuthSignInInput extends CoreInput {
10
10
  // Properties
11
11
  // ===================================================================================================================
12
12
 
13
- @Field({ description: 'Device ID', nullable: true })
13
+ @Field({ description: 'Device ID (is created automatically if it is not set)', nullable: true })
14
14
  deviceId?: string = undefined;
15
15
 
16
+ @Field({ description: 'Device description', nullable: true })
17
+ deviceDescription?: string = undefined;
18
+
16
19
  @Field({ description: 'Email', nullable: false })
17
20
  email: string = undefined;
18
21
 
@@ -1,21 +1,8 @@
1
- import { Field, InputType } from '@nestjs/graphql';
2
- import { CoreInput } from '../../../common/inputs/core-input.input';
1
+ import { InputType } from '@nestjs/graphql';
2
+ import { CoreAuthSignInInput } from './core-auth-sign-in.input';
3
3
 
4
4
  /**
5
5
  * SignUp input
6
6
  */
7
7
  @InputType({ description: 'Sign-up input' })
8
- export class CoreAuthSignUpInput extends CoreInput {
9
- // ===================================================================================================================
10
- // Properties
11
- // ===================================================================================================================
12
-
13
- @Field({ description: 'Device ID', nullable: true })
14
- deviceId?: string = undefined;
15
-
16
- @Field({ description: 'Email', nullable: false })
17
- email: string = undefined;
18
-
19
- @Field({ description: 'Password', nullable: false })
20
- password: string = undefined;
21
- }
8
+ export class CoreAuthSignUpInput extends CoreAuthSignInInput {}
@@ -1,3 +1,5 @@
1
+ import { CoreTokenData } from './core-token-data.interface';
2
+
1
3
  /**
2
4
  * Interface for user used in authorization module
3
5
  */
@@ -17,13 +19,8 @@ export interface ICoreAuthUser {
17
19
  */
18
20
  password: string;
19
21
 
20
- /**
21
- * Refresh token
22
- */
23
- refreshToken?: string;
24
-
25
22
  /**
26
23
  * Refresh tokens for different devices
27
24
  */
28
- refreshTokens?: Record<string, string>;
25
+ refreshTokens?: Record<string, CoreTokenData>;
29
26
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Data of the token
3
+ */
4
+ export interface CoreTokenData {
5
+ /**
6
+ * ID of the device from which the token was generated
7
+ */
8
+ deviceId?: string;
9
+
10
+ /**
11
+ * Description of the device from which the token was generated
12
+ */
13
+ deviceDescription?: string;
14
+
15
+ /**
16
+ * Token ID to make sure that there is only one RefreshToken for each device
17
+ */
18
+ tokenId: string;
19
+ }
@@ -2,5 +2,8 @@
2
2
  * Interface for jwt payload
3
3
  */
4
4
  export interface JwtPayload {
5
+ [key: string]: any;
5
6
  id: string;
7
+ deviceId: string;
8
+ tokenId: string;
6
9
  }
@@ -1,6 +1,7 @@
1
- import { Injectable, UnauthorizedException } from '@nestjs/common';
1
+ import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
2
2
  import { JwtService } from '@nestjs/jwt';
3
3
  import * as bcrypt from 'bcrypt';
4
+ import { randomUUID } from 'crypto';
4
5
  import { sha256 } from 'js-sha256';
5
6
  import { getStringIds } from '../../../common/helpers/db.helper';
6
7
  import { prepareServiceOptions } from '../../../common/helpers/service.helper';
@@ -37,40 +38,43 @@ export class CoreAuthService {
37
38
  /**
38
39
  * Logout user (from device)
39
40
  */
40
- async logout(serviceOptions: ServiceOptions & { deviceId?: string }): Promise<boolean> {
41
+ async logout(
42
+ tokenOrRefreshToken: string,
43
+ serviceOptions: ServiceOptions & { allDevices?: boolean }
44
+ ): Promise<boolean> {
45
+ // Check authentication
41
46
  const user = serviceOptions.currentUser;
42
- if (!serviceOptions.currentUser) {
47
+ if (!user || !tokenOrRefreshToken) {
43
48
  throw new UnauthorizedException();
44
49
  }
45
- const deviceId = serviceOptions.deviceId;
46
- if (deviceId) {
47
- if (!user.refreshTokens[deviceId]) {
48
- return false;
49
- }
50
- delete user.refreshTokens[deviceId];
51
- await this.userService.update(user.id, { refreshTokens: user.refreshTokens }, serviceOptions);
50
+
51
+ // Check authorization
52
+ const deviceId = this.decodeJwt(tokenOrRefreshToken)?.deviceId;
53
+ if (!deviceId || !user.refreshTokens[deviceId]) {
54
+ throw new UnauthorizedException('Invalid refresh token');
55
+ }
56
+
57
+ // Logout from all devices
58
+ if (serviceOptions.allDevices) {
59
+ user.refreshTokens = {};
60
+ await this.userService.update(user.id, { refreshTokens: {} }, serviceOptions);
52
61
  return true;
53
62
  }
54
- user.refreshToken = null;
55
- user.refreshTokens = {};
56
- await this.userService.update(
57
- user.id,
58
- {
59
- refreshToken: user.refreshToken,
60
- refreshTokens: user.refreshTokens,
61
- },
62
- serviceOptions
63
- );
63
+
64
+ // Logout from specific devices
65
+ delete user.refreshTokens[deviceId];
66
+ await this.userService.update(user.id, { refreshTokens: user.refreshTokens }, serviceOptions);
64
67
  return true;
65
68
  }
66
69
 
67
70
  /**
68
71
  * Refresh tokens
69
72
  */
70
- async refreshTokens(user: ICoreAuthUser, deviceId?: string) {
73
+ async refreshTokens(user: ICoreAuthUser, currentRefreshToken: string) {
71
74
  // Create new tokens
72
- const tokens = await this.getTokens(user.id);
73
- await this.updateRefreshToken(user, tokens.refreshToken, { deviceId });
75
+ const { deviceId, deviceDescription } = this.decodeJwt(currentRefreshToken);
76
+ const tokens = await this.createTokens(user.id, { deviceId, deviceDescription });
77
+ tokens.refreshToken = await this.updateRefreshToken(user, currentRefreshToken, tokens.refreshToken);
74
78
 
75
79
  // Return
76
80
  return CoreAuthModel.map({
@@ -93,10 +97,10 @@ export class CoreAuthService {
93
97
  });
94
98
 
95
99
  // Inputs
96
- const { email, password, deviceId } = input;
100
+ const { email, password, deviceId, deviceDescription } = input;
97
101
 
98
102
  // Get user
99
- const user = await this.userService.getViaEmail(email, serviceOptions);
103
+ const user = await this.userService.getViaEmail(email, serviceOptionsForUserService);
100
104
  if (
101
105
  !user ||
102
106
  !((await bcrypt.compare(password, user.password)) || (await bcrypt.compare(sha256(password), user.password)))
@@ -104,11 +108,8 @@ export class CoreAuthService {
104
108
  throw new UnauthorizedException();
105
109
  }
106
110
 
107
- // Set device ID
108
- serviceOptionsForUserService.deviceId = input.deviceId;
109
-
110
111
  // Return tokens and user
111
- return this.getResult(user, serviceOptions);
112
+ return this.getResult(user, { deviceId, deviceDescription });
112
113
  }
113
114
 
114
115
  /**
@@ -124,14 +125,14 @@ export class CoreAuthService {
124
125
  // Get and check user
125
126
  const user = await this.userService.create(input, serviceOptionsForUserService);
126
127
  if (!user) {
127
- throw Error('Email Address already in use');
128
+ throw new BadRequestException('Email Address already in use');
128
129
  }
129
130
 
130
131
  // Set device ID
131
- serviceOptionsForUserService.deviceId = input.deviceId;
132
+ const { deviceId, deviceDescription } = input;
132
133
 
133
134
  // Return tokens and user
134
- return this.getResult(user, serviceOptionsForUserService);
135
+ return this.getResult(user, { deviceId, deviceDescription });
135
136
  }
136
137
 
137
138
  /**
@@ -142,7 +143,8 @@ export class CoreAuthService {
142
143
  const user = await this.userService.get(payload.id);
143
144
 
144
145
  // Check if user exists and is logged in
145
- if (!user?.refreshToken) {
146
+ const device = user?.refreshTokens?.[payload.deviceId];
147
+ if (!device || !payload.tokenId || device.tokenId !== payload.tokenId) {
146
148
  return null;
147
149
  }
148
150
 
@@ -157,12 +159,16 @@ export class CoreAuthService {
157
159
  /**
158
160
  * Rest result with user and tokens
159
161
  */
160
- protected async getResult(user: ICoreAuthUser, serviceOptions: ServiceOptions & { deviceId?: string }) {
162
+ protected async getResult(
163
+ user: ICoreAuthUser,
164
+ data?: { [key: string]: any; deviceId?: string },
165
+ currentRefreshToken?: string
166
+ ) {
161
167
  // Create new tokens
162
- const tokens = await this.getTokens(user.id);
168
+ const tokens = await this.createTokens(user.id, data);
163
169
 
164
170
  // Set refresh token
165
- await this.updateRefreshToken(user, tokens.refreshToken, serviceOptions);
171
+ tokens.refreshToken = await this.updateRefreshToken(user, currentRefreshToken, tokens.refreshToken, data);
166
172
 
167
173
  // Return tokens and user
168
174
  return CoreAuthModel.map({
@@ -190,22 +196,22 @@ export class CoreAuthService {
190
196
  /**
191
197
  * Get JWT and refresh token
192
198
  */
193
- protected async getTokens(userId: string) {
199
+ protected async createTokens(userId: string, data?: { [key: string]: any; deviceId?: string }) {
200
+ const payload: { [key: string]: any; id: string; deviceId: string } = {
201
+ ...data,
202
+ id: userId,
203
+ deviceId: data?.deviceId || randomUUID(),
204
+ tokenId: randomUUID(),
205
+ };
194
206
  const [token, refreshToken] = await Promise.all([
195
- this.jwtService.signAsync(
196
- { id: userId },
197
- {
198
- secret: this.getSecretFromConfig(false),
199
- ...this.configService.getFastButReadOnly('jwt.signInOptions', {}),
200
- }
201
- ),
202
- this.jwtService.signAsync(
203
- { id: userId },
204
- {
205
- secret: this.getSecretFromConfig(true),
206
- ...this.configService.getFastButReadOnly('jwt.refresh.signInOptions', {}),
207
- }
208
- ),
207
+ this.jwtService.signAsync(payload, {
208
+ secret: this.getSecretFromConfig(false),
209
+ ...this.configService.getFastButReadOnly('jwt.signInOptions', {}),
210
+ }),
211
+ this.jwtService.signAsync(payload, {
212
+ secret: this.getSecretFromConfig(true),
213
+ ...this.configService.getFastButReadOnly('jwt.refresh.signInOptions', {}),
214
+ }),
209
215
  ]);
210
216
  return {
211
217
  token,
@@ -218,39 +224,45 @@ export class CoreAuthService {
218
224
  */
219
225
  protected async updateRefreshToken(
220
226
  user: ICoreAuthUser,
221
- refreshToken: string,
222
- serviceOptions: ServiceOptions & { deviceId?: string } = {}
223
- ) {
224
- const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
225
- const deviceId = serviceOptions?.deviceId;
226
- if (deviceId) {
227
- if (!user.refreshTokens) {
228
- user.refreshTokens = {};
227
+ currentRefreshToken: string,
228
+ newRefreshToken: string,
229
+ data?: Record<string, any>
230
+ ): Promise<string> {
231
+ // Check if the update of the update token is allowed
232
+ let deviceId: string;
233
+ if (currentRefreshToken) {
234
+ deviceId = this.decodeJwt(currentRefreshToken)?.deviceId;
235
+ if (!deviceId || !user.refreshTokens?.[deviceId]) {
236
+ throw new UnauthorizedException('Invalid refresh token');
229
237
  }
230
- user.refreshTokens[deviceId] = hashedRefreshToken;
231
-
232
- // Refresh token must be set even if only a specific device is logged in, because of the check in the validateUser method
233
- if (!user.refreshToken) {
234
- user.refreshToken = hashedRefreshToken;
238
+ if (!this.configService.getFastButReadOnly('jwt.refresh.renewal')) {
239
+ // Return currentToken
240
+ return currentRefreshToken;
235
241
  }
242
+ }
236
243
 
237
- return await this.userService.update(
238
- getStringIds(user),
239
- { refreshTokens: user.refreshTokens, refreshToken: user.refreshToken },
240
- {
241
- ...serviceOptions,
242
- force: true,
243
- }
244
- );
244
+ // Prepare data
245
+ data = data || {};
246
+ if (!user.refreshTokens) {
247
+ user.refreshTokens = {};
245
248
  }
246
- user.refreshToken = hashedRefreshToken;
247
- return await this.userService.update(
248
- getStringIds(user),
249
- { refreshToken: hashedRefreshToken },
250
- {
251
- ...serviceOptions,
252
- force: true,
253
- }
254
- );
249
+ if (deviceId) {
250
+ const oldData = user.refreshTokens[deviceId] || {};
251
+ data = Object.assign(oldData, data);
252
+ }
253
+
254
+ // Set new token
255
+ const payload = this.decodeJwt(newRefreshToken);
256
+ if (!payload) {
257
+ throw new UnauthorizedException();
258
+ }
259
+ if (!deviceId) {
260
+ deviceId = payload.deviceId;
261
+ }
262
+ user.refreshTokens[deviceId] = { ...data, deviceId, tokenId: payload.tokenId };
263
+ await this.userService.update(getStringIds(user), { refreshTokens: user.refreshTokens }, { force: true });
264
+
265
+ // Return new token
266
+ return newRefreshToken;
255
267
  }
256
268
  }
@@ -37,17 +37,7 @@ export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh'
37
37
  // Check user
38
38
  const user = await this.authService.validateUser(payload);
39
39
  if (!user) {
40
- throw new UnauthorizedException();
41
- }
42
-
43
- // Check refresh token
44
- const refreshToken = req
45
- .get('Authorization')
46
- .replace(/bearer/i, '')
47
- .trim();
48
- const refreshTokenMatches = await bcrypt.compare(refreshToken, user.refreshToken);
49
- if (!refreshTokenMatches) {
50
- throw new ForbiddenException('Access Denied');
40
+ throw new UnauthorizedException('Unknown user');
51
41
  }
52
42
 
53
43
  // Return user
@@ -41,7 +41,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
41
41
  async validate(payload: JwtPayload) {
42
42
  const user = await this.authService.validateUser(payload);
43
43
  if (!user) {
44
- throw new UnauthorizedException();
44
+ throw new UnauthorizedException('Unknown user');
45
45
  }
46
46
  return user;
47
47
  }