@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,98 @@
1
+ // src/security/rate-limiting/auth/brute-force.service.ts
2
+ import { Injectable, BadRequestException } from "@nestjs/common";
3
+ import { NUMERIX } from "@plyaz/config";
4
+ import type { Redis } from "ioredis";
5
+
6
+ const thirty = 30;
7
+ @Injectable()
8
+ export class BruteForceService {
9
+ private static readonly USER_HARD_LIMIT = 6;
10
+
11
+ private static readonly USER_HARD_BLOCK_MS = thirty * NUMERIX.SIXTY * NUMERIX.THOUSAND; // 30 min
12
+ private static readonly USER_PROGRESSIVE_DELAYS: Record<number, number> = { 4: 5000, 5: 5000 };
13
+
14
+ private static readonly IP_HARD_LIMIT = 10;
15
+ private static readonly IP_HARD_BLOCK_MS = NUMERIX.SIXTY * NUMERIX.SIXTY * NUMERIX.THOUSAND; // 1 hour
16
+
17
+ private static readonly PROVIDER_HARD_LIMIT = 5;
18
+ private static readonly PROVIDER_HARD_BLOCK_MS = thirty * NUMERIX.SIXTY * NUMERIX.THOUSAND; // 30 min
19
+
20
+ constructor(private readonly redis: Redis) {}
21
+
22
+ async trackFailedLogin(
23
+ userId: string,
24
+ ip: string,
25
+ providerId?: string
26
+ ): Promise<void> {
27
+ // User-level progressive delay + hard block
28
+ await this.trackEntity(
29
+ `bf:user:${userId}`,
30
+ `bf:user:block:${userId}`,
31
+ BruteForceService.USER_HARD_LIMIT,
32
+ BruteForceService.USER_HARD_BLOCK_MS,
33
+ BruteForceService.USER_PROGRESSIVE_DELAYS
34
+ );
35
+
36
+ // IP-level block
37
+ await this.trackEntity(
38
+ `bf:ip:${ip}`,
39
+ `bf:ip:block:${ip}`,
40
+ BruteForceService.IP_HARD_LIMIT,
41
+ BruteForceService.IP_HARD_BLOCK_MS
42
+ );
43
+
44
+ // Provider-level block (optional)
45
+ if (providerId) {
46
+ await this.trackEntity(
47
+ `bf:provider:${providerId}`,
48
+ `bf:provider:block:${providerId}`,
49
+ BruteForceService.PROVIDER_HARD_LIMIT,
50
+ BruteForceService.PROVIDER_HARD_BLOCK_MS
51
+ );
52
+ }
53
+ }
54
+
55
+ // eslint-disable-next-line max-params
56
+ private async trackEntity(
57
+ entityKey: string,
58
+ blockKey: string,
59
+ limit: number,
60
+ blockMs: number,
61
+ progressiveDelays?: Record<number, number>
62
+ ): Promise<void> {
63
+ const blockedUntil = await this.redis.get(blockKey);
64
+ if (blockedUntil && Date.now() < Number(blockedUntil)) {
65
+ throw new BadRequestException(
66
+ "Account temporarily blocked due to repeated failed attempts."
67
+ );
68
+ }
69
+
70
+ const failures = await this.redis.incr(entityKey);
71
+ if (failures === 1) await this.redis.pexpire(entityKey, blockMs);
72
+
73
+ if (progressiveDelays?.[failures]) {
74
+ await new Promise<void>((resolve) => {
75
+ globalThis.setTimeout(resolve, progressiveDelays[failures]);
76
+ });
77
+ }
78
+
79
+
80
+ if (failures >= limit) {
81
+ await this.redis.set(blockKey, Date.now() + blockMs, "PX", blockMs);
82
+ await this.redis.del(entityKey); // reset failures
83
+ throw new BadRequestException(
84
+ "Account locked due to too many failed login attempts."
85
+ );
86
+ }
87
+ }
88
+
89
+ async reset(userId: string, ip: string, providerId?: string): Promise<void> {
90
+ await this.redis.del(`bf:user:${userId}`, `bf:user:block:${userId}`);
91
+ await this.redis.del(`bf:ip:${ip}`, `bf:ip:block:${ip}`);
92
+ if (providerId)
93
+ await this.redis.del(
94
+ `bf:provider:${providerId}`,
95
+ `bf:provider:block:${providerId}`
96
+ );
97
+ }
98
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview Server services barrel export for @plyaz/auth
3
+ * @module @plyaz/auth/server/services
4
+ *
5
+ * @description
6
+ * Centralized export of all NestJS services for authentication and authorization.
7
+ * Provides a single import point for all service functionality.
8
+ */
9
+
10
+ export * from "./auth.service";
11
+ export * from "./session.service";
12
+ export * from "./token.service";
13
+ export * from "./account.service";
14
+ export * from "./brute-force.service";
15
+ export * from "./rate-limiter.service"
@@ -0,0 +1,60 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import type { Redis } from "ioredis";
3
+
4
+ export interface RateLimiterOptions {
5
+ windowMs: number; // time window in milliseconds
6
+ limit: number; // max requests allowed in the window
7
+ blockMs?: number; // optional block duration in milliseconds
8
+ }
9
+
10
+ @Injectable()
11
+ export class RateLimiterService {
12
+ constructor(private readonly redis: Redis) {}
13
+
14
+ getOption(routeKey: string): RateLimiterOptions {
15
+ globalThis.console.log(routeKey)
16
+ // Example placeholder, replace with your actual logic
17
+ throw new Error("Method not implemented.");
18
+ }
19
+
20
+ async isRateLimited(
21
+ identifier: string,
22
+ options: RateLimiterOptions
23
+ ): Promise<boolean> {
24
+ const rateKey = `rate:${identifier}`;
25
+ const blockKey = `block:${identifier}`;
26
+
27
+ // Check if user is already blocked
28
+ const blockedUntil = await this.redis.get(blockKey);
29
+ if (blockedUntil && Date.now() < Number(blockedUntil)) {
30
+ return true;
31
+ }
32
+
33
+ // Increase request count
34
+ const requests = await this.redis.incr(rateKey);
35
+
36
+ // Set window expiry on first request
37
+ if (requests === 1) {
38
+ await this.redis.pexpire(rateKey, options.windowMs);
39
+ }
40
+
41
+ // Block user if limit exceeded
42
+ if (requests > options.limit && options.blockMs) {
43
+ const blockUntil = Date.now() + options.blockMs;
44
+
45
+ await this.redis.set(blockKey, blockUntil.toString(), "PX", options.blockMs);
46
+ await this.redis.del(rateKey); // reset counter
47
+
48
+ return true;
49
+ }
50
+
51
+ return false;
52
+ }
53
+
54
+ async reset(identifier: string): Promise<void> {
55
+ const rateKey = `rate:${identifier}`;
56
+ const blockKey = `block:${identifier}`;
57
+
58
+ await this.redis.del(rateKey, blockKey);
59
+ }
60
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * @fileoverview Session service for @plyaz/auth
3
+ * @module @plyaz/auth/server/services/session-service
4
+ *
5
+ * @description
6
+ * NestJS service for session management operations. Provides high-level
7
+ * session operations for controllers and other services. Wraps the
8
+ * EnhancedSessionManager with NestJS dependency injection and logging.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { SessionService } from '@plyaz/auth';
13
+ *
14
+ * @Controller('auth')
15
+ * export class AuthController {
16
+ * constructor(private sessionService: SessionService) {}
17
+ *
18
+ * @Post('logout-all')
19
+ * async logoutAll(@CurrentUser() user) {
20
+ * await this.sessionService.invalidateAllUserSessions(user.id);
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+
26
+ import { Injectable, Logger } from '@nestjs/common';
27
+ import type { EnhancedSessionManager, UserContext, SessionCreationData } from '../../session/enhanced-session-manager';
28
+
29
+ import type { Session, SessionData } from '@plyaz/types';
30
+ import { AUTH_EVENTS } from '@plyaz/types';
31
+
32
+ /**
33
+ * Session service implementation
34
+ * Provides session management operations for NestJS applications
35
+ */
36
+ @Injectable()
37
+ export class SessionService {
38
+ private readonly logger = new Logger(SessionService.name);
39
+
40
+ constructor(private sessionManager: EnhancedSessionManager) {}
41
+
42
+ /**
43
+ * Create a new session
44
+ * @param userContext - User context information
45
+ * @param sessionData - Session creation data
46
+ * @returns Created session
47
+ */
48
+ async createSession(userContext: UserContext, sessionData: SessionCreationData = {}): Promise<Session> {
49
+ this.logger.debug(`Creating session for user: ${userContext.userId}`);
50
+
51
+ try {
52
+ const session = await this.sessionManager.createSession(userContext, sessionData);
53
+
54
+ this.logger.log(`Session created: ${session.id} for user: ${userContext.userId}`);
55
+ this.emitEvent(AUTH_EVENTS.SESSION_CREATED, {
56
+ userId: userContext.userId,
57
+ sessionId: session.id,
58
+ timestamp: new Date()
59
+ });
60
+
61
+ return session;
62
+ } catch (error) {
63
+ this.logger.error(`Failed to create session for user: ${userContext.userId}`, error);
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get session by ID
70
+ * @param sessionId - Session identifier
71
+ * @returns Session or null if not found
72
+ */
73
+ async getSession(sessionId: string): Promise<Session | null> {
74
+ this.logger.debug(`Getting session: ${sessionId}`);
75
+
76
+ try {
77
+ return await this.sessionManager.getSession(sessionId);
78
+ } catch (error) {
79
+ this.logger.error(`Failed to get session: ${sessionId}`, error);
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Validate session
86
+ * @param sessionId - Session identifier
87
+ * @returns True if session is valid
88
+ */
89
+ async validateSession(sessionId: string): Promise<boolean> {
90
+ this.logger.debug(`Validating session: ${sessionId}`);
91
+
92
+ try {
93
+ const isValid = await this.sessionManager.validateSession(sessionId);
94
+
95
+ if (!isValid) {
96
+ this.logger.warn(`Invalid session: ${sessionId}`);
97
+ }
98
+
99
+ return isValid;
100
+ } catch (error) {
101
+ this.logger.error(`Failed to validate session: ${sessionId}`, error);
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Refresh session
108
+ * @param sessionId - Session identifier
109
+ * @returns Refreshed session
110
+ */
111
+ async refreshSession(sessionId: string): Promise<Session> {
112
+ this.logger.debug(`Refreshing session: ${sessionId}`);
113
+
114
+ try {
115
+ const session = await this.sessionManager.refreshSession(sessionId);
116
+
117
+ this.logger.log(`Session refreshed: ${sessionId}`);
118
+ this.emitEvent(AUTH_EVENTS.SESSION_REFRESHED, {
119
+ userId: session.userId,
120
+ sessionId: session.id,
121
+ timestamp: new Date()
122
+ });
123
+
124
+ return session;
125
+ } catch (error) {
126
+ this.logger.error(`Failed to refresh session: ${sessionId}`, error);
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Update session data
133
+ * @param sessionId - Session identifier
134
+ * @param updates - Partial session updates
135
+ * @returns Updated session
136
+ */
137
+ async updateSession(sessionId: string, updates:Partial<SessionData>): Promise<Session> {
138
+ this.logger.debug(`Updating session: ${sessionId}`);
139
+
140
+ try {
141
+ return await this.sessionManager.updateSession(sessionId, updates);
142
+ } catch (error) {
143
+ this.logger.error(`Failed to update session: ${sessionId}`, error);
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Delete session
150
+ * @param sessionId - Session identifier
151
+ */
152
+ async deleteSession(sessionId: string): Promise<void> {
153
+ this.logger.debug(`Deleting session: ${sessionId}`);
154
+
155
+ try {
156
+ await this.sessionManager.deleteSession(sessionId);
157
+
158
+ this.logger.log(`Session deleted: ${sessionId}`);
159
+ this.emitEvent(AUTH_EVENTS.SESSION_INVALIDATED, {
160
+ sessionId,
161
+ timestamp: new Date()
162
+ });
163
+ } catch (error) {
164
+ this.logger.error(`Failed to delete session: ${sessionId}`, error);
165
+ throw error;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Invalidate all sessions for a user
171
+ * @param userId - User identifier
172
+ */
173
+ async invalidateAllUserSessions(userId: string): Promise<void> {
174
+ this.logger.debug(`Invalidating all sessions for user: ${userId}`);
175
+
176
+ try {
177
+ await this.sessionManager.invalidateAllUserSessions(userId);
178
+
179
+ this.logger.log(`All sessions invalidated for user: ${userId}`);
180
+ this.emitEvent(AUTH_EVENTS.SESSION_INVALIDATED, {
181
+ userId,
182
+ timestamp: new Date()
183
+ });
184
+ } catch (error) {
185
+ this.logger.error(`Failed to invalidate all sessions for user: ${userId}`, error);
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Detect concurrent sessions for a user
192
+ * @param userId - User identifier
193
+ * @returns Array of active sessions
194
+ */
195
+ async detectConcurrentSessions(userId: string): Promise<Session[]> {
196
+ this.logger.debug(`Detecting concurrent sessions for user: ${userId}`);
197
+
198
+ try {
199
+ return await this.sessionManager.detectConcurrentSessions(userId);
200
+ } catch (error) {
201
+ this.logger.error(`Failed to detect concurrent sessions for user: ${userId}`, error);
202
+ throw error;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Generate CSRF token for session
208
+ * @param sessionId - Session identifier
209
+ * @returns CSRF token
210
+ */
211
+ async generateCsrfToken(sessionId: string): Promise<string> {
212
+ this.logger.debug(`Generating CSRF token for session: ${sessionId}`);
213
+
214
+ try {
215
+ return await this.sessionManager.generateCsrfToken(sessionId);
216
+ } catch (error) {
217
+ this.logger.error(`Failed to generate CSRF token for session: ${sessionId}`, error);
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Validate CSRF token for session
224
+ * @param sessionId - Session identifier
225
+ * @param token - CSRF token to validate
226
+ * @returns True if token is valid
227
+ */
228
+ async validateCsrfToken(sessionId: string, token: string): Promise<boolean> {
229
+ this.logger.debug(`Validating CSRF token for session: ${sessionId}`);
230
+
231
+ try {
232
+ const isValid = await this.sessionManager.validateCsrfToken(sessionId, token);
233
+
234
+ if (!isValid) {
235
+ this.logger.warn(`Invalid CSRF token for session: ${sessionId}`);
236
+ }
237
+
238
+ return isValid;
239
+ } catch (error) {
240
+ this.logger.error(`Failed to validate CSRF token for session: ${sessionId}`, error);
241
+ return false;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Check if session needs refresh
247
+ * @param sessionId - Session identifier
248
+ * @returns True if session should be refreshed
249
+ */
250
+ async shouldRefreshSession(sessionId: string): Promise<boolean> {
251
+ try {
252
+ return await this.sessionManager.shouldRefreshSession(sessionId);
253
+ } catch (error) {
254
+ this.logger.error(`Failed to check if session needs refresh: ${sessionId}`, error);
255
+ return false;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Get session statistics
261
+ * @returns Session statistics
262
+ */
263
+ async getSessionStats(): Promise<{
264
+ totalSessions: number;
265
+ activeUsers: number;
266
+ }> {
267
+ this.logger.debug('Getting session statistics');
268
+
269
+ try {
270
+ return await this.sessionManager.getSessionStats();
271
+ } catch (error) {
272
+ this.logger.error('Failed to get session statistics', error);
273
+ throw error;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Emit event (mock implementation)
279
+ * @param eventType - Event type
280
+ * @param payload - Event payload
281
+ * @private
282
+ */
283
+ private emitEvent(eventType: string, payload:unknown): void {
284
+ // Mock event emission - in real implementation would use event system
285
+ this.logger.debug(`Event: ${eventType}`, payload);
286
+ }
287
+ }