@solidstarters/solid-core 1.2.87 → 1.2.88

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidstarters/solid-core",
3
- "version": "1.2.87",
3
+ "version": "1.2.88",
4
4
  "description": "This module is a NestJS module containing all the required core providers required by a Solid application",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -859,31 +859,45 @@ export class AuthenticationService {
859
859
  }
860
860
 
861
861
  async generateTokens(user: User) {
862
- const refreshTokenId = randomUUID();
863
-
864
- // const userRoleNames = user.roles.map((role) => role.name).join(';')
865
- const userRoleNames = user.roles.map((role) => role.name);
866
862
 
867
863
  const [accessToken, refreshToken] = await Promise.all([
868
- this.signToken<Partial<ActiveUserData>>(
869
- user.id,
870
- this.jwtConfiguration.accessTokenTtl,
871
- { username: user.username, email: user.email, roles: userRoleNames },
872
- ),
873
- this.signToken(user.id, this.jwtConfiguration.refreshTokenTtl, {
874
- refreshTokenId,
875
- }),
864
+ this.generateAccessToken(user),
865
+ this.generateRefreshToken(user),
876
866
  ]);
877
867
 
878
- // store the refresh token id in the redis storage.
879
- await this.refreshTokenIdsStorage.insert(user.id, refreshTokenId);
880
-
881
868
  return {
882
869
  accessToken,
883
870
  refreshToken,
884
871
  };
885
872
  }
886
873
 
874
+ async generateAccessToken(user: User) {
875
+
876
+ // const userRoleNames = user.roles.map((role) => role.name).join(';')
877
+ const userRoleNames = user.roles.map((role) => role.name);
878
+
879
+ const accessToken = await this.signToken<Partial<ActiveUserData>>(
880
+ user.id,
881
+ this.jwtConfiguration.accessTokenTtl,
882
+ { username: user.username, email: user.email, roles: userRoleNames },
883
+ );
884
+
885
+ return accessToken;
886
+ }
887
+
888
+ async generateRefreshToken(user: User) {
889
+ const refreshTokenId = randomUUID();
890
+
891
+ const refreshToken = await this.signToken(user.id, this.jwtConfiguration.refreshTokenTtl, {
892
+ refreshTokenId,
893
+ })
894
+
895
+ // store the refresh token id in the redis storage.
896
+ await this.refreshTokenIdsStorage.insert(user.id, refreshToken);
897
+
898
+ return refreshToken;
899
+ }
900
+
887
901
  async refreshTokens(refreshTokenDto: RefreshTokenDto) {
888
902
  try {
889
903
  const { sub, refreshTokenId } = await this.jwtService.verifyAsync<Pick<ActiveUserData, 'sub'> & { refreshTokenId: string }>(refreshTokenDto.refreshToken, {
@@ -904,14 +918,21 @@ export class AuthenticationService {
904
918
  throw new UnauthorizedException();
905
919
  }
906
920
 
907
- const isValid = await this.refreshTokenIdsStorage.validate(user.id, refreshTokenId);
908
- if (isValid) {
909
- // Refresh token rotation.
910
- await this.refreshTokenIdsStorage.invalidate(user.id);
911
- } else {
912
- throw new Error('Refresh token is invalid');
913
- }
914
- return this.generateTokens(user);
921
+ // TODO: Replace the if else condition below with a call to validateAndRotate - Done
922
+ // const isValid = await this.refreshTokenIdsStorage.validate(user.id, refreshTokenId);
923
+ // if (isValid) {
924
+ // // Refresh token rotation.
925
+ // await this.refreshTokenIdsStorage.invalidate(user.id);
926
+ // } else {
927
+ // throw new Error('Refresh token is invalid');
928
+ // }
929
+
930
+ const currentRefreshToken = await this.refreshTokenIdsStorage.validateAndRotate(user, refreshTokenDto.refreshToken);
931
+
932
+ return {
933
+ accessToken: await this.generateAccessToken(user),
934
+ refreshToken: currentRefreshToken,
935
+ };
915
936
  } catch (err) {
916
937
  if (err instanceof InvalidatedRefreshTokenError) {
917
938
  // Take action: notify user that his refresh token might have been stolen?
@@ -1,6 +1,7 @@
1
1
  import { CACHE_MANAGER } from '@nestjs/cache-manager';
2
- import { Inject, Injectable } from '@nestjs/common';
2
+ import { Inject, Injectable, forwardRef } from '@nestjs/common';
3
3
  import { Cache } from 'cache-manager';
4
+ import { AuthenticationService } from './authentication.service';
4
5
 
5
6
  // TODO: Ideally this should be in a separate file - putting this here for brevity
6
7
  export class InvalidatedRefreshTokenError extends Error { }
@@ -21,24 +22,106 @@ export class RefreshTokenIdsStorageService {
21
22
  // return this.redisClient.quit();
22
23
  // }
23
24
 
24
- constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
25
+ constructor(
26
+ @Inject(CACHE_MANAGER) private cacheManager: Cache,
27
+ @Inject(forwardRef(() => AuthenticationService))
28
+ private readonly authenticationService: AuthenticationService
29
+ ) { }
25
30
 
26
- async insert(userId: number, tokenId: string): Promise<void> {
27
- await this.cacheManager.set(this.getKey(userId), tokenId);
31
+ async insert(userId: number, refreshToken: string): Promise<void> {
32
+ // TODO: save a refresh token object with this shape {"currentRefreshToken": "", "previousRefreshToken": ""}
33
+ // Save a refresh token object with the shape: { currentRefreshToken: string, previousRefreshToken: string }
34
+ const existing = (await this.cacheManager.get(this.getKey(userId))) as { currentRefreshToken?: string, previousRefreshToken?: string } | undefined;
35
+ const refreshTokenState = {
36
+ currentRefreshToken: refreshToken,
37
+ previousRefreshToken: "",
38
+ };
39
+ await this.cacheManager.set(this.getKey(userId), refreshTokenState);
28
40
  }
29
41
 
30
- async validate(userId: number, tokenId: string): Promise<boolean> {
42
+ async validate(userId: number, refreshToken: string): Promise<boolean> {
43
+ // TODO: Assume you get this shape out of the cache {"currentRefreshToken": "", "previousRefreshToken": ""}
44
+ // Then you will compare against the currentRefreshToken.
31
45
  const storedId = await this.cacheManager.get(this.getKey(userId));
32
- if (storedId !== tokenId) {
46
+ if (storedId !== refreshToken) {
33
47
  throw new InvalidatedRefreshTokenError();
34
48
  }
35
- return storedId === tokenId;
49
+ return storedId === refreshToken;
36
50
  }
37
51
 
38
52
  async invalidate(userId: number): Promise<void> {
39
53
  await this.cacheManager.del(this.getKey(userId));
40
54
  }
41
55
 
56
+ async validateAndRotate(user: any, refreshToken: string): Promise<string> {
57
+ let valid = false;
58
+
59
+ // TODO: Assume you get this shape out of the cache {"currentRefreshToken": "", "previousRefreshToken": ""}
60
+ // Then you will compare against the currentRefreshToken.
61
+ const refreshTokenState = await this.cacheManager.get(this.getKey(user.id));
62
+ console.log("refreshTokenState", refreshTokenState);
63
+
64
+ // Use the authentication service to generate a new refresh token, set it in the currentRefreshToken in scenario 1 and return.
65
+
66
+ // if UI.refresh_token is matching with Cache.currentRefreshToken
67
+ // then invalidate (updated cache state, no need to delete anything), then generate new token and return.
68
+ // also set a setTimeout to run after X minutes, this will simply update the RefreshTokenCacheState to this object {"currentRefreshToken": "R2","justInvalidatedRefreshToken": ""}
69
+ // valid=true
70
+
71
+ // - if UI.refresh_token is matching Cache.justInvalidatedRefreshToken
72
+ // then use the Cache.currentRefreshToken, generate new access token and return.
73
+ // We do not modify the cache state at all.
74
+ // valid=true
75
+
76
+ let newRefreshToken: string | undefined;
77
+ if (
78
+ refreshTokenState &&
79
+ typeof refreshTokenState === 'object' &&
80
+ 'currentRefreshToken' in refreshTokenState &&
81
+ 'previousRefreshToken' in refreshTokenState
82
+ ) {
83
+ if (refreshTokenState.currentRefreshToken === refreshToken) {
84
+ // Scenario 1: Token matches currentRefreshToken
85
+ valid = true;
86
+ // Rotate tokens: move current to previous, set new current (simulate generation)
87
+ newRefreshToken = await this.authenticationService.generateRefreshToken(user); // Replace with real token generation logic
88
+
89
+
90
+ // updated cache state
91
+ await this.cacheManager.set(this.getKey(user.id), {
92
+ currentRefreshToken: newRefreshToken,
93
+ previousRefreshToken: refreshTokenState.currentRefreshToken,
94
+ });
95
+
96
+ // Optionally, set a timeout to clear previousRefreshToken after X minutes
97
+ setTimeout(async () => {
98
+ const state = (await this.cacheManager.get(this.getKey(user.id))) as any;
99
+ if (state && state.currentRefreshToken === newRefreshToken) {
100
+ await this.cacheManager.set(this.getKey(user.id), {
101
+ currentRefreshToken: newRefreshToken,
102
+ previousRefreshToken: "",
103
+ });
104
+ }
105
+ }, 1 * 60 * 1000); // 5 minutes
106
+ } else if (refreshTokenState.previousRefreshToken === refreshToken) {
107
+ // Scenario 2: Token matches previousRefreshToken
108
+ valid = true;
109
+ // Do not modify cache
110
+ // Generate new refresh token based on currentRefreshToken
111
+ const existingRefreshTokenState = (await this.cacheManager.get(this.getKey(user.id))) as { currentRefreshToken?: string, previousRefreshToken?: string } | undefined;
112
+ newRefreshToken = existingRefreshTokenState?.currentRefreshToken;
113
+ }
114
+ }
115
+
116
+
117
+ if (!valid) {
118
+ throw new InvalidatedRefreshTokenError();
119
+ }
120
+
121
+ // TODO: return the refresh token either currentRefreshToken
122
+ return newRefreshToken; // Fallback to the provided tokenId if no new token was generated
123
+ }
124
+
42
125
  private getKey(userId: number): string {
43
126
  return `user-${userId}`;
44
127
  }