@plyaz/auth 1.0.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/.github/pull_request_template.md +71 -0
- package/.github/workflows/deploy.yml +9 -0
- package/.github/workflows/publish.yml +14 -0
- package/.github/workflows/security.yml +20 -0
- package/README.md +89 -0
- package/commits.txt +5 -0
- package/dist/common/index.cjs +48 -0
- package/dist/common/index.cjs.map +1 -0
- package/dist/common/index.mjs +43 -0
- package/dist/common/index.mjs.map +1 -0
- package/dist/index.cjs +20411 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +5139 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.mjs +13 -0
- package/index.html +13 -0
- package/package.json +141 -0
- package/src/adapters/auth-adapter-factory.ts +26 -0
- package/src/adapters/auth-adapter.mapper.ts +53 -0
- package/src/adapters/base-auth.adapter.ts +119 -0
- package/src/adapters/clerk/clerk.adapter.ts +204 -0
- package/src/adapters/custom/custom.adapter.ts +119 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/next-auth/authOptions.ts +81 -0
- package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
- package/src/api/client.ts +37 -0
- package/src/audit/audit.logger.ts +52 -0
- package/src/client/components/ProtectedRoute.tsx +37 -0
- package/src/client/hooks/useAuth.ts +128 -0
- package/src/client/hooks/useConnectedAccounts.ts +108 -0
- package/src/client/hooks/usePermissions.ts +36 -0
- package/src/client/hooks/useRBAC.ts +36 -0
- package/src/client/hooks/useSession.ts +18 -0
- package/src/client/providers/AuthProvider.tsx +104 -0
- package/src/client/store/auth.store.ts +306 -0
- package/src/client/utils/storage.ts +70 -0
- package/src/common/constants/oauth-providers.ts +49 -0
- package/src/common/errors/auth.errors.ts +64 -0
- package/src/common/errors/specific-auth-errors.ts +201 -0
- package/src/common/index.ts +19 -0
- package/src/common/regex/index.ts +27 -0
- package/src/common/types/auth.types.ts +641 -0
- package/src/common/types/index.ts +297 -0
- package/src/common/utils/index.ts +84 -0
- package/src/core/blacklist/token.blacklist.ts +60 -0
- package/src/core/index.ts +2 -0
- package/src/core/jwt/jwt.manager.ts +131 -0
- package/src/core/session/session.manager.ts +56 -0
- package/src/db/repositories/connected-account.repository.ts +415 -0
- package/src/db/repositories/role.repository.ts +519 -0
- package/src/db/repositories/session.repository.ts +308 -0
- package/src/db/repositories/user.repository.ts +320 -0
- package/src/flows/index.ts +2 -0
- package/src/flows/sign-in.flow.ts +106 -0
- package/src/flows/sign-up.flow.ts +121 -0
- package/src/index.ts +54 -0
- package/src/libs/clerk.helper.ts +36 -0
- package/src/libs/supabase.helper.ts +255 -0
- package/src/libs/supabaseClient.ts +6 -0
- package/src/providers/base/auth-provider.interface.ts +42 -0
- package/src/providers/base/index.ts +1 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/oauth/facebook.provider.ts +97 -0
- package/src/providers/oauth/github.provider.ts +148 -0
- package/src/providers/oauth/google.provider.ts +126 -0
- package/src/providers/oauth/index.ts +3 -0
- package/src/rbac/dynamic-roles.ts +552 -0
- package/src/rbac/index.ts +4 -0
- package/src/rbac/permission-checker.ts +464 -0
- package/src/rbac/role-hierarchy.ts +545 -0
- package/src/rbac/role.manager.ts +75 -0
- package/src/security/csrf/csrf.protection.ts +37 -0
- package/src/security/index.ts +3 -0
- package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
- package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
- package/src/security/rate-limiting/auth.module.ts +32 -0
- package/src/server/auth.module.ts +158 -0
- package/src/server/decorators/auth.decorator.ts +43 -0
- package/src/server/decorators/auth.decorators.ts +31 -0
- package/src/server/decorators/current-user.decorator.ts +49 -0
- package/src/server/decorators/permission.decorator.ts +49 -0
- package/src/server/guards/auth.guard.ts +56 -0
- package/src/server/guards/custom-throttler.guard.ts +46 -0
- package/src/server/guards/permissions.guard.ts +115 -0
- package/src/server/guards/roles.guard.ts +31 -0
- package/src/server/middleware/auth.middleware.ts +46 -0
- package/src/server/middleware/index.ts +2 -0
- package/src/server/middleware/middleware.ts +11 -0
- package/src/server/middleware/session.middleware.ts +255 -0
- package/src/server/services/account.service.ts +269 -0
- package/src/server/services/auth.service.ts +79 -0
- package/src/server/services/brute-force.service.ts +98 -0
- package/src/server/services/index.ts +15 -0
- package/src/server/services/rate-limiter.service.ts +60 -0
- package/src/server/services/session.service.ts +287 -0
- package/src/server/services/token.service.ts +262 -0
- package/src/session/cookie-store.ts +255 -0
- package/src/session/enhanced-session-manager.ts +406 -0
- package/src/session/index.ts +14 -0
- package/src/session/memory-store.ts +320 -0
- package/src/session/redis-store.ts +443 -0
- package/src/strategies/oauth.strategy.ts +128 -0
- package/src/strategies/traditional-auth.strategy.ts +116 -0
- package/src/tokens/index.ts +4 -0
- package/src/tokens/refresh-token-manager.ts +448 -0
- package/src/tokens/token-validator.ts +311 -0
- package/tsconfig.build.json +28 -0
- package/tsconfig.json +38 -0
- package/tsup.config.mjs +28 -0
- package/vitest.config.mjs +16 -0
- package/vitest.setup.d.ts +2 -0
- package/vitest.setup.d.ts.map +1 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
/* eslint-disable no-magic-numbers */
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Refresh token manager for @plyaz/auth
|
|
5
|
+
* @module @plyaz/auth/tokens/refresh-token-manager
|
|
6
|
+
*
|
|
7
|
+
* @description
|
|
8
|
+
* Manages refresh token lifecycle including generation, validation, rotation,
|
|
9
|
+
* and revocation. Implements security best practices like token rotation
|
|
10
|
+
* and family tracking to prevent token theft and replay attacks.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { RefreshTokenManager } from '@plyaz/auth';
|
|
15
|
+
*
|
|
16
|
+
* const manager = new RefreshTokenManager({
|
|
17
|
+
* secretKey: 'your-secret-key',
|
|
18
|
+
* tokenTTL: 604800, // 7 days
|
|
19
|
+
* enableRotation: true
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* const tokens = await manager.generateTokenPair(userId, sessionId);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { sign, verify } from 'jsonwebtoken';
|
|
27
|
+
import { randomBytes } from 'crypto';
|
|
28
|
+
import { TokenBlacklist } from '../core/blacklist/token.blacklist';
|
|
29
|
+
import { AuthenticationError } from '@plyaz/errors';
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Refresh token manager configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface RefreshTokenManagerConfig {
|
|
36
|
+
/** Secret key for signing tokens */
|
|
37
|
+
secretKey: string;
|
|
38
|
+
/** Refresh token TTL in seconds */
|
|
39
|
+
tokenTTL: number;
|
|
40
|
+
/** Access token TTL in seconds */
|
|
41
|
+
accessTokenTTL: number;
|
|
42
|
+
/** Token issuer */
|
|
43
|
+
issuer: string;
|
|
44
|
+
/** Token audience */
|
|
45
|
+
audience: string;
|
|
46
|
+
/** Enable token rotation */
|
|
47
|
+
enableRotation: boolean;
|
|
48
|
+
/** Enable token family tracking */
|
|
49
|
+
enableFamilyTracking: boolean;
|
|
50
|
+
/** Maximum token family size */
|
|
51
|
+
maxFamilySize: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Token pair (access + refresh)
|
|
56
|
+
*/
|
|
57
|
+
export interface TokenPair {
|
|
58
|
+
/** Access token */
|
|
59
|
+
accessToken: string;
|
|
60
|
+
/** Refresh token */
|
|
61
|
+
refreshToken: string;
|
|
62
|
+
/** Access token expiration */
|
|
63
|
+
accessTokenExpiresAt: Date;
|
|
64
|
+
/** Refresh token expiration */
|
|
65
|
+
refreshTokenExpiresAt: Date;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Refresh token payload
|
|
70
|
+
*/
|
|
71
|
+
export interface RefreshTokenPayload {
|
|
72
|
+
/** User ID */
|
|
73
|
+
sub: string;
|
|
74
|
+
/** Session ID */
|
|
75
|
+
sessionId: string;
|
|
76
|
+
/** Token family ID (for rotation tracking) */
|
|
77
|
+
family?: string;
|
|
78
|
+
/** Token generation number */
|
|
79
|
+
generation?: number;
|
|
80
|
+
/** Token type */
|
|
81
|
+
type: 'refresh';
|
|
82
|
+
/** Issued at */
|
|
83
|
+
iat: number;
|
|
84
|
+
/** Expires at */
|
|
85
|
+
exp: number;
|
|
86
|
+
/** Issuer */
|
|
87
|
+
iss: string;
|
|
88
|
+
/** Audience */
|
|
89
|
+
aud: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Access token payload
|
|
94
|
+
*/
|
|
95
|
+
export interface AccessTokenPayload {
|
|
96
|
+
/** User ID */
|
|
97
|
+
sub: string;
|
|
98
|
+
/** Session ID */
|
|
99
|
+
sessionId: string;
|
|
100
|
+
/** User roles */
|
|
101
|
+
roles?: string[];
|
|
102
|
+
/** User permissions */
|
|
103
|
+
permissions?: string[];
|
|
104
|
+
/** Token type */
|
|
105
|
+
type: 'access';
|
|
106
|
+
/** Issued at */
|
|
107
|
+
iat: number;
|
|
108
|
+
/** Expires at */
|
|
109
|
+
exp: number;
|
|
110
|
+
/** Issuer */
|
|
111
|
+
iss: string;
|
|
112
|
+
/** Audience */
|
|
113
|
+
aud: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Token family for rotation tracking
|
|
118
|
+
*/
|
|
119
|
+
interface TokenFamily {
|
|
120
|
+
/** Family ID */
|
|
121
|
+
id: string;
|
|
122
|
+
/** User ID */
|
|
123
|
+
userId: string;
|
|
124
|
+
/** Session ID */
|
|
125
|
+
sessionId: string;
|
|
126
|
+
/** Current generation */
|
|
127
|
+
generation: number;
|
|
128
|
+
/** Created at */
|
|
129
|
+
createdAt: Date;
|
|
130
|
+
/** Last used at */
|
|
131
|
+
lastUsedAt: Date;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Refresh token manager implementation
|
|
136
|
+
* Handles refresh token lifecycle with security features
|
|
137
|
+
*/
|
|
138
|
+
export class RefreshTokenManager {
|
|
139
|
+
private readonly config: RefreshTokenManagerConfig;
|
|
140
|
+
private readonly blacklist: TokenBlacklist;
|
|
141
|
+
private readonly tokenFamilies = new Map<string, TokenFamily>();
|
|
142
|
+
|
|
143
|
+
constructor(config: RefreshTokenManagerConfig) {
|
|
144
|
+
this.config = config;
|
|
145
|
+
this.blacklist = new TokenBlacklist({ keyPrefix: 'token:', defaultTTL: 3600 });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate new token pair (access + refresh)
|
|
150
|
+
* @param userId - User identifier
|
|
151
|
+
* @param sessionId - Session identifier
|
|
152
|
+
* @param userRoles - User roles for access token
|
|
153
|
+
* @param userPermissions - User permissions for access token
|
|
154
|
+
* @returns Token pair
|
|
155
|
+
*/
|
|
156
|
+
async generateTokenPair(
|
|
157
|
+
userId: string,
|
|
158
|
+
sessionId: string,
|
|
159
|
+
userRoles?: string[],
|
|
160
|
+
userPermissions?: string[]
|
|
161
|
+
): Promise<TokenPair> {
|
|
162
|
+
const now = Math.floor(Date.now() / 1000);
|
|
163
|
+
const accessTokenExp = now + this.config.accessTokenTTL;
|
|
164
|
+
const refreshTokenExp = now + this.config.tokenTTL;
|
|
165
|
+
|
|
166
|
+
// Generate token family if rotation is enabled
|
|
167
|
+
let family: string | undefined;
|
|
168
|
+
let generation = 1;
|
|
169
|
+
|
|
170
|
+
if (this.config.enableFamilyTracking) {
|
|
171
|
+
family = this.generateFamilyId();
|
|
172
|
+
this.tokenFamilies.set(family, {
|
|
173
|
+
id: family,
|
|
174
|
+
userId,
|
|
175
|
+
sessionId,
|
|
176
|
+
generation,
|
|
177
|
+
createdAt: new Date(),
|
|
178
|
+
lastUsedAt: new Date()
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Create access token payload
|
|
183
|
+
const accessPayload: AccessTokenPayload = {
|
|
184
|
+
sub: userId,
|
|
185
|
+
sessionId,
|
|
186
|
+
roles: userRoles,
|
|
187
|
+
permissions: userPermissions,
|
|
188
|
+
type: 'access',
|
|
189
|
+
iat: now,
|
|
190
|
+
exp: accessTokenExp,
|
|
191
|
+
iss: this.config.issuer,
|
|
192
|
+
aud: this.config.audience
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Create refresh token payload
|
|
196
|
+
const refreshPayload: RefreshTokenPayload = {
|
|
197
|
+
sub: userId,
|
|
198
|
+
sessionId,
|
|
199
|
+
family,
|
|
200
|
+
generation,
|
|
201
|
+
type: 'refresh',
|
|
202
|
+
iat: now,
|
|
203
|
+
exp: refreshTokenExp,
|
|
204
|
+
iss: this.config.issuer,
|
|
205
|
+
aud: this.config.audience
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Sign tokens
|
|
209
|
+
const accessToken = sign(accessPayload, this.config.secretKey, { algorithm: 'HS256' });
|
|
210
|
+
const refreshToken = sign(refreshPayload, this.config.secretKey, { algorithm: 'HS256' });
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
accessToken,
|
|
214
|
+
refreshToken,
|
|
215
|
+
accessTokenExpiresAt: new Date(accessTokenExp * 1000),
|
|
216
|
+
refreshTokenExpiresAt: new Date(refreshTokenExp * 1000)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Refresh token pair using refresh token
|
|
222
|
+
* @param refreshToken - Current refresh token
|
|
223
|
+
* @param userRoles - Updated user roles
|
|
224
|
+
* @param userPermissions - Updated user permissions
|
|
225
|
+
* @returns New token pair
|
|
226
|
+
*/
|
|
227
|
+
async refreshTokenPair(
|
|
228
|
+
refreshToken: string,
|
|
229
|
+
userRoles?: string[],
|
|
230
|
+
userPermissions?: string[]
|
|
231
|
+
): Promise<TokenPair> {
|
|
232
|
+
// Validate refresh token
|
|
233
|
+
const payload = await this.validateRefreshToken(refreshToken);
|
|
234
|
+
|
|
235
|
+
// Check token family if rotation is enabled
|
|
236
|
+
if (this.config.enableFamilyTracking && payload.family) {
|
|
237
|
+
await this.validateTokenFamily(payload);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Revoke old refresh token if rotation is enabled
|
|
241
|
+
if (this.config.enableRotation) {
|
|
242
|
+
await this.blacklist.add(refreshToken, payload.exp);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Generate new token pair
|
|
246
|
+
const newTokenPair = await this.generateTokenPair(
|
|
247
|
+
payload.sub,
|
|
248
|
+
payload.sessionId,
|
|
249
|
+
userRoles,
|
|
250
|
+
userPermissions
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Update token family
|
|
254
|
+
if (this.config.enableFamilyTracking && payload.family) {
|
|
255
|
+
const family = this.tokenFamilies.get(payload.family);
|
|
256
|
+
if (family) {
|
|
257
|
+
family.generation++;
|
|
258
|
+
family.lastUsedAt = new Date();
|
|
259
|
+
|
|
260
|
+
// Enforce family size limit
|
|
261
|
+
if (family.generation > this.config.maxFamilySize) {
|
|
262
|
+
await this.revokeTokenFamily(payload.family);
|
|
263
|
+
throw new AuthenticationError('AUTH_TOKEN_INVALID');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return newTokenPair;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validate refresh token
|
|
273
|
+
* @param refreshToken - Refresh token to validate
|
|
274
|
+
* @returns Decoded payload
|
|
275
|
+
*/
|
|
276
|
+
async validateRefreshToken(refreshToken: string): Promise<RefreshTokenPayload> {
|
|
277
|
+
try {
|
|
278
|
+
// Check if token is blacklisted
|
|
279
|
+
if (await this.blacklist.isBlacklisted(refreshToken)) {
|
|
280
|
+
throw new AuthenticationError('AUTH_TOKEN_REVOKED');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Verify token
|
|
284
|
+
const payload = verify(refreshToken, this.config.secretKey, {
|
|
285
|
+
issuer: this.config.issuer,
|
|
286
|
+
audience: this.config.audience,
|
|
287
|
+
algorithms: ['HS256']
|
|
288
|
+
}) as RefreshTokenPayload;
|
|
289
|
+
|
|
290
|
+
// Validate token type
|
|
291
|
+
if (payload.type !== 'refresh') {
|
|
292
|
+
throw new AuthenticationError('AUTH_TOKEN_INVALID');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return payload;
|
|
296
|
+
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (error instanceof Error) {
|
|
299
|
+
if (error.name === 'TokenExpiredError') {
|
|
300
|
+
throw new AuthenticationError('AUTH_TOKEN_EXPIRED');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (error.name === 'JsonWebTokenError') {
|
|
304
|
+
throw new AuthenticationError('AUTH_TOKEN_INVALID');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Revoke refresh token
|
|
315
|
+
* @param refreshToken - Refresh token to revoke
|
|
316
|
+
*/
|
|
317
|
+
async revokeRefreshToken(refreshToken: string): Promise<void> {
|
|
318
|
+
try {
|
|
319
|
+
const payload = await this.validateRefreshToken(refreshToken);
|
|
320
|
+
|
|
321
|
+
// Add to blacklist
|
|
322
|
+
await this.blacklist.add(refreshToken, payload.exp);
|
|
323
|
+
|
|
324
|
+
// Revoke entire token family if enabled
|
|
325
|
+
if (this.config.enableFamilyTracking && payload.family) {
|
|
326
|
+
await this.revokeTokenFamily(payload.family);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
} catch (error) {
|
|
330
|
+
// Token might already be invalid, but still try to blacklist
|
|
331
|
+
await this.blacklist.add(refreshToken, Math.floor(Date.now() / 1000) + 3600);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Revoke all refresh tokens for a user
|
|
337
|
+
* @param userId - User identifier
|
|
338
|
+
*/
|
|
339
|
+
async revokeAllUserTokens(userId: string): Promise<void> {
|
|
340
|
+
// Revoke all token families for user
|
|
341
|
+
for (const [familyId, family] of this.tokenFamilies.entries()) {
|
|
342
|
+
if (family.userId === userId) {
|
|
343
|
+
await this.revokeTokenFamily(familyId);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Revoke all refresh tokens for a session
|
|
350
|
+
* @param sessionId - Session identifier
|
|
351
|
+
*/
|
|
352
|
+
async revokeSessionTokens(sessionId: string): Promise<void> {
|
|
353
|
+
// Revoke all token families for session
|
|
354
|
+
for (const [familyId, family] of this.tokenFamilies.entries()) {
|
|
355
|
+
if (family.sessionId === sessionId) {
|
|
356
|
+
await this.revokeTokenFamily(familyId);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get token family information
|
|
363
|
+
* @param familyId - Token family identifier
|
|
364
|
+
* @returns Token family or null
|
|
365
|
+
*/
|
|
366
|
+
getTokenFamily(familyId: string): TokenFamily | null {
|
|
367
|
+
return this.tokenFamilies.get(familyId) ?? null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Clean up expired token families
|
|
372
|
+
* @returns Number of cleaned families
|
|
373
|
+
*/
|
|
374
|
+
async cleanupExpiredFamilies(): Promise<number> {
|
|
375
|
+
let cleanedCount = 0;
|
|
376
|
+
const now = new Date();
|
|
377
|
+
const expiredFamilies: string[] = [];
|
|
378
|
+
|
|
379
|
+
for (const [familyId, family] of this.tokenFamilies.entries()) {
|
|
380
|
+
// Consider family expired if not used for longer than token TTL
|
|
381
|
+
const expiredTime = new Date(family.lastUsedAt.getTime() + this.config.tokenTTL * 1000);
|
|
382
|
+
if (expiredTime < now) {
|
|
383
|
+
expiredFamilies.push(familyId);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const familyId of expiredFamilies) {
|
|
388
|
+
this.tokenFamilies.delete(familyId);
|
|
389
|
+
cleanedCount++;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return cleanedCount;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Validate token family for rotation security
|
|
397
|
+
* @param payload - Refresh token payload
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
private async validateTokenFamily(payload: RefreshTokenPayload): Promise<void> {
|
|
401
|
+
if (!payload.family) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const family = this.tokenFamilies.get(payload.family);
|
|
406
|
+
|
|
407
|
+
if (!family) {
|
|
408
|
+
throw new AuthenticationError('AUTH_TOKEN_INVALID');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Check if token generation is valid
|
|
412
|
+
if (payload.generation !== family.generation) {
|
|
413
|
+
// Potential token theft - revoke entire family
|
|
414
|
+
await this.revokeTokenFamily(payload.family);
|
|
415
|
+
throw new AuthenticationError('AUTH_TOKEN_REVOKED');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Check if user and session match
|
|
419
|
+
if (family.userId !== payload.sub || family.sessionId !== payload.sessionId) {
|
|
420
|
+
await this.revokeTokenFamily(payload.family);
|
|
421
|
+
throw new AuthenticationError('AUTH_TOKEN_REVOKED');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Revoke entire token family
|
|
427
|
+
* @param familyId - Token family identifier
|
|
428
|
+
* @private
|
|
429
|
+
*/
|
|
430
|
+
private async revokeTokenFamily(familyId: string): Promise<void> {
|
|
431
|
+
const family = this.tokenFamilies.get(familyId);
|
|
432
|
+
|
|
433
|
+
if (family) {
|
|
434
|
+
// In a real implementation, this would blacklist all tokens in the family
|
|
435
|
+
// For now, we just remove the family tracking
|
|
436
|
+
this.tokenFamilies.delete(familyId);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Generate unique family ID
|
|
442
|
+
* @returns Family identifier
|
|
443
|
+
* @private
|
|
444
|
+
*/
|
|
445
|
+
private generateFamilyId(): string {
|
|
446
|
+
return randomBytes(16).toString('hex');
|
|
447
|
+
}
|
|
448
|
+
}
|