@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/dist/services/authentication.service.d.ts +2 -0
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +20 -15
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/refresh-token-ids-storage.service.d.ts +6 -3
- package/dist/services/refresh-token-ids-storage.service.d.ts.map +1 -1
- package/dist/services/refresh-token-ids-storage.service.js +52 -7
- package/dist/services/refresh-token-ids-storage.service.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/services/authentication.service.ts +44 -23
- package/src/services/refresh-token-ids-storage.service.ts +90 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidstarters/solid-core",
|
|
3
|
-
"version": "1.2.
|
|
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.
|
|
869
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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(
|
|
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,
|
|
27
|
-
|
|
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,
|
|
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 !==
|
|
46
|
+
if (storedId !== refreshToken) {
|
|
33
47
|
throw new InvalidatedRefreshTokenError();
|
|
34
48
|
}
|
|
35
|
-
return storedId ===
|
|
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
|
}
|