@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.
Files changed (113) hide show
  1. package/.github/pull_request_template.md +71 -0
  2. package/.github/workflows/deploy.yml +9 -0
  3. package/.github/workflows/publish.yml +14 -0
  4. package/.github/workflows/security.yml +20 -0
  5. package/README.md +89 -0
  6. package/commits.txt +5 -0
  7. package/dist/common/index.cjs +48 -0
  8. package/dist/common/index.cjs.map +1 -0
  9. package/dist/common/index.mjs +43 -0
  10. package/dist/common/index.mjs.map +1 -0
  11. package/dist/index.cjs +20411 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +5139 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/eslint.config.mjs +13 -0
  16. package/index.html +13 -0
  17. package/package.json +141 -0
  18. package/src/adapters/auth-adapter-factory.ts +26 -0
  19. package/src/adapters/auth-adapter.mapper.ts +53 -0
  20. package/src/adapters/base-auth.adapter.ts +119 -0
  21. package/src/adapters/clerk/clerk.adapter.ts +204 -0
  22. package/src/adapters/custom/custom.adapter.ts +119 -0
  23. package/src/adapters/index.ts +4 -0
  24. package/src/adapters/next-auth/authOptions.ts +81 -0
  25. package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
  26. package/src/api/client.ts +37 -0
  27. package/src/audit/audit.logger.ts +52 -0
  28. package/src/client/components/ProtectedRoute.tsx +37 -0
  29. package/src/client/hooks/useAuth.ts +128 -0
  30. package/src/client/hooks/useConnectedAccounts.ts +108 -0
  31. package/src/client/hooks/usePermissions.ts +36 -0
  32. package/src/client/hooks/useRBAC.ts +36 -0
  33. package/src/client/hooks/useSession.ts +18 -0
  34. package/src/client/providers/AuthProvider.tsx +104 -0
  35. package/src/client/store/auth.store.ts +306 -0
  36. package/src/client/utils/storage.ts +70 -0
  37. package/src/common/constants/oauth-providers.ts +49 -0
  38. package/src/common/errors/auth.errors.ts +64 -0
  39. package/src/common/errors/specific-auth-errors.ts +201 -0
  40. package/src/common/index.ts +19 -0
  41. package/src/common/regex/index.ts +27 -0
  42. package/src/common/types/auth.types.ts +641 -0
  43. package/src/common/types/index.ts +297 -0
  44. package/src/common/utils/index.ts +84 -0
  45. package/src/core/blacklist/token.blacklist.ts +60 -0
  46. package/src/core/index.ts +2 -0
  47. package/src/core/jwt/jwt.manager.ts +131 -0
  48. package/src/core/session/session.manager.ts +56 -0
  49. package/src/db/repositories/connected-account.repository.ts +415 -0
  50. package/src/db/repositories/role.repository.ts +519 -0
  51. package/src/db/repositories/session.repository.ts +308 -0
  52. package/src/db/repositories/user.repository.ts +320 -0
  53. package/src/flows/index.ts +2 -0
  54. package/src/flows/sign-in.flow.ts +106 -0
  55. package/src/flows/sign-up.flow.ts +121 -0
  56. package/src/index.ts +54 -0
  57. package/src/libs/clerk.helper.ts +36 -0
  58. package/src/libs/supabase.helper.ts +255 -0
  59. package/src/libs/supabaseClient.ts +6 -0
  60. package/src/providers/base/auth-provider.interface.ts +42 -0
  61. package/src/providers/base/index.ts +1 -0
  62. package/src/providers/index.ts +2 -0
  63. package/src/providers/oauth/facebook.provider.ts +97 -0
  64. package/src/providers/oauth/github.provider.ts +148 -0
  65. package/src/providers/oauth/google.provider.ts +126 -0
  66. package/src/providers/oauth/index.ts +3 -0
  67. package/src/rbac/dynamic-roles.ts +552 -0
  68. package/src/rbac/index.ts +4 -0
  69. package/src/rbac/permission-checker.ts +464 -0
  70. package/src/rbac/role-hierarchy.ts +545 -0
  71. package/src/rbac/role.manager.ts +75 -0
  72. package/src/security/csrf/csrf.protection.ts +37 -0
  73. package/src/security/index.ts +3 -0
  74. package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
  75. package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
  76. package/src/security/rate-limiting/auth.module.ts +32 -0
  77. package/src/server/auth.module.ts +158 -0
  78. package/src/server/decorators/auth.decorator.ts +43 -0
  79. package/src/server/decorators/auth.decorators.ts +31 -0
  80. package/src/server/decorators/current-user.decorator.ts +49 -0
  81. package/src/server/decorators/permission.decorator.ts +49 -0
  82. package/src/server/guards/auth.guard.ts +56 -0
  83. package/src/server/guards/custom-throttler.guard.ts +46 -0
  84. package/src/server/guards/permissions.guard.ts +115 -0
  85. package/src/server/guards/roles.guard.ts +31 -0
  86. package/src/server/middleware/auth.middleware.ts +46 -0
  87. package/src/server/middleware/index.ts +2 -0
  88. package/src/server/middleware/middleware.ts +11 -0
  89. package/src/server/middleware/session.middleware.ts +255 -0
  90. package/src/server/services/account.service.ts +269 -0
  91. package/src/server/services/auth.service.ts +79 -0
  92. package/src/server/services/brute-force.service.ts +98 -0
  93. package/src/server/services/index.ts +15 -0
  94. package/src/server/services/rate-limiter.service.ts +60 -0
  95. package/src/server/services/session.service.ts +287 -0
  96. package/src/server/services/token.service.ts +262 -0
  97. package/src/session/cookie-store.ts +255 -0
  98. package/src/session/enhanced-session-manager.ts +406 -0
  99. package/src/session/index.ts +14 -0
  100. package/src/session/memory-store.ts +320 -0
  101. package/src/session/redis-store.ts +443 -0
  102. package/src/strategies/oauth.strategy.ts +128 -0
  103. package/src/strategies/traditional-auth.strategy.ts +116 -0
  104. package/src/tokens/index.ts +4 -0
  105. package/src/tokens/refresh-token-manager.ts +448 -0
  106. package/src/tokens/token-validator.ts +311 -0
  107. package/tsconfig.build.json +28 -0
  108. package/tsconfig.json +38 -0
  109. package/tsup.config.mjs +28 -0
  110. package/vitest.config.mjs +16 -0
  111. package/vitest.setup.d.ts +2 -0
  112. package/vitest.setup.d.ts.map +1 -0
  113. 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
+ }