@alliance-droid/svelte-auth-core 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/dist/adapter-context.d.ts +19 -0
- package/dist/adapter-context.d.ts.map +1 -0
- package/dist/adapter-context.js +68 -0
- package/dist/adapter-context.js.map +1 -0
- package/dist/adapters/__tests__/adapter-tests.d.ts +7 -0
- package/dist/adapters/__tests__/adapter-tests.d.ts.map +1 -0
- package/dist/adapters/__tests__/adapter-tests.js +206 -0
- package/dist/adapters/__tests__/adapter-tests.js.map +1 -0
- package/dist/adapters/adapter.d.ts +60 -0
- package/dist/adapters/adapter.d.ts.map +1 -0
- package/dist/adapters/adapter.js +2 -0
- package/dist/adapters/adapter.js.map +1 -0
- package/dist/adapters/filesystem-adapter.d.ts +26 -0
- package/dist/adapters/filesystem-adapter.d.ts.map +1 -0
- package/dist/adapters/filesystem-adapter.js +148 -0
- package/dist/adapters/filesystem-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +5 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mongodb-adapter.d.ts +27 -0
- package/dist/adapters/mongodb-adapter.d.ts.map +1 -0
- package/dist/adapters/mongodb-adapter.js +213 -0
- package/dist/adapters/mongodb-adapter.js.map +1 -0
- package/dist/adapters/postgres-adapter.d.ts +30 -0
- package/dist/adapters/postgres-adapter.d.ts.map +1 -0
- package/dist/adapters/postgres-adapter.js +237 -0
- package/dist/adapters/postgres-adapter.js.map +1 -0
- package/dist/adapters/sqlite-adapter.d.ts +26 -0
- package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
- package/dist/adapters/sqlite-adapter.js +261 -0
- package/dist/adapters/sqlite-adapter.js.map +1 -0
- package/dist/auth.d.ts +48 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +205 -0
- package/dist/auth.js.map +1 -0
- package/dist/client-jwt.d.ts +30 -0
- package/dist/client-jwt.d.ts.map +1 -0
- package/dist/client-jwt.js +57 -0
- package/dist/client-jwt.js.map +1 -0
- package/dist/client-store.d.ts +31 -0
- package/dist/client-store.d.ts.map +1 -0
- package/dist/client-store.js +122 -0
- package/dist/client-store.js.map +1 -0
- package/dist/cors.d.ts +48 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +88 -0
- package/dist/cors.js.map +1 -0
- package/dist/csrf.d.ts +57 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +95 -0
- package/dist/csrf.js.map +1 -0
- package/dist/db.d.ts +22 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +43 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/input-validation.d.ts +78 -0
- package/dist/input-validation.d.ts.map +1 -0
- package/dist/input-validation.js +238 -0
- package/dist/input-validation.js.map +1 -0
- package/dist/oauth-callback.d.ts +31 -0
- package/dist/oauth-callback.d.ts.map +1 -0
- package/dist/oauth-callback.js +254 -0
- package/dist/oauth-callback.js.map +1 -0
- package/dist/oauth-providers.d.ts +92 -0
- package/dist/oauth-providers.d.ts.map +1 -0
- package/dist/oauth-providers.js +213 -0
- package/dist/oauth-providers.js.map +1 -0
- package/dist/oauth-types.d.ts +77 -0
- package/dist/oauth-types.d.ts.map +1 -0
- package/dist/oauth-types.js +2 -0
- package/dist/oauth-types.js.map +1 -0
- package/dist/password.d.ts +31 -0
- package/dist/password.d.ts.map +1 -0
- package/dist/password.js +54 -0
- package/dist/password.js.map +1 -0
- package/dist/providers/github-oauth.d.ts +58 -0
- package/dist/providers/github-oauth.d.ts.map +1 -0
- package/dist/providers/github-oauth.js +230 -0
- package/dist/providers/github-oauth.js.map +1 -0
- package/dist/providers/google-oauth.d.ts +46 -0
- package/dist/providers/google-oauth.d.ts.map +1 -0
- package/dist/providers/google-oauth.js +177 -0
- package/dist/providers/google-oauth.js.map +1 -0
- package/dist/providers/oidc-oauth.d.ts +85 -0
- package/dist/providers/oidc-oauth.d.ts.map +1 -0
- package/dist/providers/oidc-oauth.js +301 -0
- package/dist/providers/oidc-oauth.js.map +1 -0
- package/dist/rate-limit.d.ts +36 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +88 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/rate-limiting.d.ts +113 -0
- package/dist/rate-limiting.d.ts.map +1 -0
- package/dist/rate-limiting.js +221 -0
- package/dist/rate-limiting.js.map +1 -0
- package/dist/security-headers.d.ts +54 -0
- package/dist/security-headers.d.ts.map +1 -0
- package/dist/security-headers.js +123 -0
- package/dist/security-headers.js.map +1 -0
- package/dist/session.d.ts +13 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +33 -0
- package/dist/session.js.map +1 -0
- package/dist/sql-injection-prevention.d.ts +94 -0
- package/dist/sql-injection-prevention.d.ts.map +1 -0
- package/dist/sql-injection-prevention.js +222 -0
- package/dist/sql-injection-prevention.js.map +1 -0
- package/dist/token.d.ts +22 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +31 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/user.d.ts +33 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +144 -0
- package/dist/user.js.map +1 -0
- package/package.json +48 -0
- package/src/adapter-context.ts +72 -0
- package/src/adapters/__tests__/adapter-tests.ts +254 -0
- package/src/adapters/__tests__/filesystem-adapter.test.ts +48 -0
- package/src/adapters/__tests__/mongodb-adapter.test.ts +64 -0
- package/src/adapters/__tests__/postgres-adapter.test.ts +62 -0
- package/src/adapters/__tests__/sqlite-adapter.test.ts +103 -0
- package/src/adapters/__tests__/test-fs-adapter.json +4 -0
- package/src/adapters/adapter.ts +72 -0
- package/src/adapters/filesystem-adapter.ts +153 -0
- package/src/adapters/index.ts +5 -0
- package/src/adapters/mongodb-adapter.ts +208 -0
- package/src/adapters/postgres-adapter.ts +261 -0
- package/src/adapters/sqlite-adapter.ts +284 -0
- package/src/auth.ts +239 -0
- package/src/client-jwt.test.ts +137 -0
- package/src/client-jwt.ts +67 -0
- package/src/client-store.test.ts +149 -0
- package/src/client-store.ts +144 -0
- package/src/cors.test.ts +175 -0
- package/src/cors.ts +115 -0
- package/src/csrf.test.ts +226 -0
- package/src/csrf.ts +126 -0
- package/src/db.ts +57 -0
- package/src/index.ts +143 -0
- package/src/input-validation.test.ts +347 -0
- package/src/input-validation.ts +307 -0
- package/src/integration.test.ts +322 -0
- package/src/oauth-callback.test.ts +282 -0
- package/src/oauth-callback.ts +323 -0
- package/src/oauth-providers.ts +232 -0
- package/src/oauth-types.ts +82 -0
- package/src/password.test.ts +89 -0
- package/src/password.ts +62 -0
- package/src/providers/github-oauth.test.ts +290 -0
- package/src/providers/github-oauth.ts +226 -0
- package/src/providers/google-oauth.test.ts +240 -0
- package/src/providers/google-oauth.ts +166 -0
- package/src/providers/oidc-oauth.test.ts +367 -0
- package/src/providers/oidc-oauth.ts +302 -0
- package/src/rate-limit.test.ts +308 -0
- package/src/rate-limit.ts +118 -0
- package/src/rate-limiting.test.ts +390 -0
- package/src/rate-limiting.ts +275 -0
- package/src/security-headers.test.ts +242 -0
- package/src/security-headers.ts +160 -0
- package/src/security-penetration.test.ts +705 -0
- package/src/session.ts +42 -0
- package/src/sql-injection-prevention.test.ts +337 -0
- package/src/sql-injection-prevention.ts +272 -0
- package/src/token.test.ts +67 -0
- package/src/token.ts +34 -0
- package/src/types.ts +87 -0
- package/src/user.ts +165 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
checkRateLimit,
|
|
4
|
+
resetRateLimit,
|
|
5
|
+
getRateLimitStatus,
|
|
6
|
+
clearAllRateLimits,
|
|
7
|
+
extractClientIp,
|
|
8
|
+
type RateLimitConfig
|
|
9
|
+
} from './rate-limit';
|
|
10
|
+
|
|
11
|
+
describe('Rate Limiting', () => {
|
|
12
|
+
const loginConfig: RateLimitConfig = {
|
|
13
|
+
maxAttempts: 5,
|
|
14
|
+
windowMs: 10 * 60 * 1000 // 10 minutes
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const registrationConfig: RateLimitConfig = {
|
|
18
|
+
maxAttempts: 3,
|
|
19
|
+
windowMs: 60 * 60 * 1000 // 1 hour
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const passwordResetConfig: RateLimitConfig = {
|
|
23
|
+
maxAttempts: 2,
|
|
24
|
+
windowMs: 60 * 60 * 1000 // 1 hour
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
clearAllRateLimits();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('checkRateLimit', () => {
|
|
32
|
+
it('should allow requests under the limit', () => {
|
|
33
|
+
const result1 = checkRateLimit('user@example.com', loginConfig);
|
|
34
|
+
expect(result1.allowed).toBe(true);
|
|
35
|
+
expect(result1.remaining).toBe(4);
|
|
36
|
+
|
|
37
|
+
const result2 = checkRateLimit('user@example.com', loginConfig);
|
|
38
|
+
expect(result2.allowed).toBe(true);
|
|
39
|
+
expect(result2.remaining).toBe(3);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should block requests exceeding the limit', () => {
|
|
43
|
+
// Make max attempts
|
|
44
|
+
for (let i = 0; i < loginConfig.maxAttempts; i++) {
|
|
45
|
+
checkRateLimit('user@example.com', loginConfig);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Next attempt should be blocked
|
|
49
|
+
const result = checkRateLimit('user@example.com', loginConfig);
|
|
50
|
+
expect(result.allowed).toBe(false);
|
|
51
|
+
expect(result.remaining).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should enforce login rate limits (5 attempts/10 min)', () => {
|
|
55
|
+
const key = 'test@example.com';
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < 5; i++) {
|
|
58
|
+
const result = checkRateLimit(key, loginConfig);
|
|
59
|
+
expect(result.allowed).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 6th attempt should fail
|
|
63
|
+
const result = checkRateLimit(key, loginConfig);
|
|
64
|
+
expect(result.allowed).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should enforce registration rate limits (3/hour per IP)', () => {
|
|
68
|
+
const key = '192.168.1.1';
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < 3; i++) {
|
|
71
|
+
const result = checkRateLimit(key, registrationConfig);
|
|
72
|
+
expect(result.allowed).toBe(true);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4th attempt should fail
|
|
76
|
+
const result = checkRateLimit(key, registrationConfig);
|
|
77
|
+
expect(result.allowed).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should enforce password reset rate limits (2/hour)', () => {
|
|
81
|
+
const key = 'reset@example.com';
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < 2; i++) {
|
|
84
|
+
const result = checkRateLimit(key, passwordResetConfig);
|
|
85
|
+
expect(result.allowed).toBe(true);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3rd attempt should fail
|
|
89
|
+
const result = checkRateLimit(key, passwordResetConfig);
|
|
90
|
+
expect(result.allowed).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should track resetAt timestamp', () => {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const result = checkRateLimit('user@example.com', loginConfig);
|
|
96
|
+
|
|
97
|
+
expect(result.resetAt).toBeGreaterThan(now);
|
|
98
|
+
expect(result.resetAt).toBeLessThanOrEqual(now + loginConfig.windowMs);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should maintain separate limits for different keys', () => {
|
|
102
|
+
const result1 = checkRateLimit('user1@example.com', loginConfig);
|
|
103
|
+
const result2 = checkRateLimit('user2@example.com', loginConfig);
|
|
104
|
+
|
|
105
|
+
expect(result1.allowed).toBe(true);
|
|
106
|
+
expect(result1.remaining).toBe(4);
|
|
107
|
+
|
|
108
|
+
expect(result2.allowed).toBe(true);
|
|
109
|
+
expect(result2.remaining).toBe(4);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('resetRateLimit', () => {
|
|
114
|
+
it('should reset rate limit for a key', () => {
|
|
115
|
+
const key = 'user@example.com';
|
|
116
|
+
|
|
117
|
+
// Hit rate limit
|
|
118
|
+
for (let i = 0; i < loginConfig.maxAttempts; i++) {
|
|
119
|
+
checkRateLimit(key, loginConfig);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const blockedResult = checkRateLimit(key, loginConfig);
|
|
123
|
+
expect(blockedResult.allowed).toBe(false);
|
|
124
|
+
|
|
125
|
+
// Reset the limit
|
|
126
|
+
resetRateLimit(key);
|
|
127
|
+
|
|
128
|
+
// Should be able to attempt again
|
|
129
|
+
const allowedResult = checkRateLimit(key, loginConfig);
|
|
130
|
+
expect(allowedResult.allowed).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should allow resetting before hitting limit', () => {
|
|
134
|
+
const key = 'user@example.com';
|
|
135
|
+
|
|
136
|
+
checkRateLimit(key, loginConfig);
|
|
137
|
+
checkRateLimit(key, loginConfig);
|
|
138
|
+
|
|
139
|
+
// Reset early
|
|
140
|
+
resetRateLimit(key);
|
|
141
|
+
|
|
142
|
+
// Status should be reset
|
|
143
|
+
const status = getRateLimitStatus(key, loginConfig);
|
|
144
|
+
expect(status.attempts).toBe(0);
|
|
145
|
+
expect(status.remaining).toBe(loginConfig.maxAttempts);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('getRateLimitStatus', () => {
|
|
150
|
+
it('should return correct status for a key', () => {
|
|
151
|
+
const key = 'user@example.com';
|
|
152
|
+
|
|
153
|
+
checkRateLimit(key, loginConfig);
|
|
154
|
+
checkRateLimit(key, loginConfig);
|
|
155
|
+
|
|
156
|
+
const status = getRateLimitStatus(key, loginConfig);
|
|
157
|
+
|
|
158
|
+
expect(status.attempts).toBe(2);
|
|
159
|
+
expect(status.remaining).toBe(3);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return full remaining attempts for untouched key', () => {
|
|
163
|
+
const status = getRateLimitStatus('new@example.com', loginConfig);
|
|
164
|
+
|
|
165
|
+
expect(status.attempts).toBe(0);
|
|
166
|
+
expect(status.remaining).toBe(loginConfig.maxAttempts);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should provide resetAt timestamp', () => {
|
|
170
|
+
const key = 'user@example.com';
|
|
171
|
+
checkRateLimit(key, loginConfig);
|
|
172
|
+
|
|
173
|
+
const status = getRateLimitStatus(key, loginConfig);
|
|
174
|
+
expect(status.resetAt).toBeGreaterThan(Date.now());
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('clearAllRateLimits', () => {
|
|
179
|
+
it('should clear all stored rate limits', () => {
|
|
180
|
+
const key1 = 'user1@example.com';
|
|
181
|
+
const key2 = 'user2@example.com';
|
|
182
|
+
|
|
183
|
+
checkRateLimit(key1, loginConfig);
|
|
184
|
+
checkRateLimit(key2, loginConfig);
|
|
185
|
+
|
|
186
|
+
clearAllRateLimits();
|
|
187
|
+
|
|
188
|
+
const status1 = getRateLimitStatus(key1, loginConfig);
|
|
189
|
+
const status2 = getRateLimitStatus(key2, loginConfig);
|
|
190
|
+
|
|
191
|
+
expect(status1.attempts).toBe(0);
|
|
192
|
+
expect(status2.attempts).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('extractClientIp', () => {
|
|
197
|
+
it('should extract IP from x-forwarded-for header', () => {
|
|
198
|
+
const headers: Record<string, string | string[] | undefined> = {
|
|
199
|
+
'x-forwarded-for': '192.168.1.1, 10.0.0.1'
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const ip = extractClientIp(headers);
|
|
203
|
+
expect(ip).toBe('192.168.1.1');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should extract IP from x-real-ip header', () => {
|
|
207
|
+
const headers: Record<string, string | string[] | undefined> = {
|
|
208
|
+
'x-real-ip': '192.168.1.2'
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const ip = extractClientIp(headers);
|
|
212
|
+
expect(ip).toBe('192.168.1.2');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should return unknown if no IP headers present', () => {
|
|
216
|
+
const headers: Record<string, string | string[] | undefined> = {};
|
|
217
|
+
|
|
218
|
+
const ip = extractClientIp(headers);
|
|
219
|
+
expect(ip).toBe('unknown');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should handle whitespace in x-forwarded-for', () => {
|
|
223
|
+
const headers: Record<string, string | string[] | undefined> = {
|
|
224
|
+
'x-forwarded-for': ' 192.168.1.3 , 10.0.0.2'
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const ip = extractClientIp(headers);
|
|
228
|
+
expect(ip).toBe('192.168.1.3');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('Rate limit expiration', () => {
|
|
233
|
+
it('should clean up expired entries', () => {
|
|
234
|
+
const key = 'user@example.com';
|
|
235
|
+
const shortConfig: RateLimitConfig = {
|
|
236
|
+
maxAttempts: 2,
|
|
237
|
+
windowMs: 100 // 100ms window
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Make attempts
|
|
241
|
+
checkRateLimit(key, shortConfig);
|
|
242
|
+
checkRateLimit(key, shortConfig);
|
|
243
|
+
|
|
244
|
+
// Should be blocked
|
|
245
|
+
let result = checkRateLimit(key, shortConfig);
|
|
246
|
+
expect(result.allowed).toBe(false);
|
|
247
|
+
|
|
248
|
+
// Wait for window to expire
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
// After expiration, should be allowed again
|
|
252
|
+
result = checkRateLimit(key, shortConfig);
|
|
253
|
+
expect(result.allowed).toBe(true);
|
|
254
|
+
resolve(undefined);
|
|
255
|
+
}, 150);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('Integration scenarios', () => {
|
|
261
|
+
it('should handle login flow with failed then successful attempt', () => {
|
|
262
|
+
const key = 'test@example.com';
|
|
263
|
+
|
|
264
|
+
// Simulate failed login attempts
|
|
265
|
+
for (let i = 0; i < 5; i++) {
|
|
266
|
+
const result = checkRateLimit(key, loginConfig);
|
|
267
|
+
expect(result.allowed).toBe(true);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Should be rate limited
|
|
271
|
+
let result = checkRateLimit(key, loginConfig);
|
|
272
|
+
expect(result.allowed).toBe(false);
|
|
273
|
+
|
|
274
|
+
// After successful login (outside rate limit), reset
|
|
275
|
+
resetRateLimit(key);
|
|
276
|
+
|
|
277
|
+
// Should be able to login again
|
|
278
|
+
result = checkRateLimit(key, loginConfig);
|
|
279
|
+
expect(result.allowed).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle multiple endpoints with different limits', () => {
|
|
283
|
+
const email = 'user@example.com';
|
|
284
|
+
const ip = '192.168.1.1';
|
|
285
|
+
|
|
286
|
+
// Login limit - use operation-specific key
|
|
287
|
+
const loginKey = `login:${email}`;
|
|
288
|
+
checkRateLimit(loginKey, loginConfig);
|
|
289
|
+
const loginStatus = getRateLimitStatus(loginKey, loginConfig);
|
|
290
|
+
expect(loginStatus.remaining).toBe(4);
|
|
291
|
+
|
|
292
|
+
// Registration limit (IP based)
|
|
293
|
+
const regKey = `register:${ip}`;
|
|
294
|
+
checkRateLimit(regKey, registrationConfig);
|
|
295
|
+
const regStatus = getRateLimitStatus(regKey, registrationConfig);
|
|
296
|
+
expect(regStatus.remaining).toBe(2);
|
|
297
|
+
|
|
298
|
+
// Password reset limit (email based)
|
|
299
|
+
const resetKey = `reset:${email}`;
|
|
300
|
+
checkRateLimit(resetKey, passwordResetConfig);
|
|
301
|
+
const resetStatus = getRateLimitStatus(resetKey, passwordResetConfig);
|
|
302
|
+
expect(resetStatus.remaining).toBe(1);
|
|
303
|
+
|
|
304
|
+
// Verify they are independent
|
|
305
|
+
expect(loginStatus.remaining).not.toBe(regStatus.remaining);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
interface RateLimitEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
count: number;
|
|
4
|
+
resetAt: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RateLimitConfig {
|
|
8
|
+
maxAttempts: number;
|
|
9
|
+
windowMs: number; // Time window in milliseconds
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const RATE_LIMITS: Record<string, RateLimitEntry[]> = {};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if a request should be rate limited
|
|
16
|
+
* @param key - Unique identifier for the rate limit (e.g., email, IP address)
|
|
17
|
+
* @param config - Rate limit configuration
|
|
18
|
+
* @returns Object with remaining attempts and resetTime
|
|
19
|
+
*/
|
|
20
|
+
export function checkRateLimit(
|
|
21
|
+
key: string,
|
|
22
|
+
config: RateLimitConfig
|
|
23
|
+
): { allowed: boolean; remaining: number; resetAt: number } {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
|
|
26
|
+
// Initialize rate limit storage for this key if not exists
|
|
27
|
+
if (!RATE_LIMITS[key]) {
|
|
28
|
+
RATE_LIMITS[key] = [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Remove expired entries
|
|
32
|
+
RATE_LIMITS[key] = RATE_LIMITS[key].filter((entry) => entry.resetAt > now);
|
|
33
|
+
|
|
34
|
+
// Count current attempts
|
|
35
|
+
const currentCount = RATE_LIMITS[key].length;
|
|
36
|
+
|
|
37
|
+
if (currentCount >= config.maxAttempts) {
|
|
38
|
+
// Get the earliest reset time
|
|
39
|
+
const resetAt = Math.min(...RATE_LIMITS[key].map((e) => e.resetAt));
|
|
40
|
+
return {
|
|
41
|
+
allowed: false,
|
|
42
|
+
remaining: 0,
|
|
43
|
+
resetAt
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add new attempt
|
|
48
|
+
const resetAt = now + config.windowMs;
|
|
49
|
+
RATE_LIMITS[key].push({
|
|
50
|
+
key,
|
|
51
|
+
count: 1,
|
|
52
|
+
resetAt
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
allowed: true,
|
|
57
|
+
remaining: config.maxAttempts - currentCount - 1,
|
|
58
|
+
resetAt
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reset rate limit for a key (e.g., after successful login)
|
|
64
|
+
*/
|
|
65
|
+
export function resetRateLimit(key: string): void {
|
|
66
|
+
if (RATE_LIMITS[key]) {
|
|
67
|
+
delete RATE_LIMITS[key];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get current rate limit status for a key
|
|
73
|
+
*/
|
|
74
|
+
export function getRateLimitStatus(key: string, config: RateLimitConfig) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
|
|
77
|
+
if (!RATE_LIMITS[key]) {
|
|
78
|
+
return {
|
|
79
|
+
attempts: 0,
|
|
80
|
+
remaining: config.maxAttempts,
|
|
81
|
+
resetAt: now + config.windowMs
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Remove expired entries
|
|
86
|
+
RATE_LIMITS[key] = RATE_LIMITS[key].filter((entry) => entry.resetAt > now);
|
|
87
|
+
|
|
88
|
+
const currentCount = RATE_LIMITS[key].length;
|
|
89
|
+
const resetAt = RATE_LIMITS[key].length > 0 ? Math.max(...RATE_LIMITS[key].map((e) => e.resetAt)) : now + config.windowMs;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
attempts: currentCount,
|
|
93
|
+
remaining: Math.max(0, config.maxAttempts - currentCount),
|
|
94
|
+
resetAt
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear all rate limits (useful for testing)
|
|
100
|
+
*/
|
|
101
|
+
export function clearAllRateLimits(): void {
|
|
102
|
+
for (const key in RATE_LIMITS) {
|
|
103
|
+
delete RATE_LIMITS[key];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract IP address from headers
|
|
109
|
+
*/
|
|
110
|
+
export function extractClientIp(
|
|
111
|
+
headers: Record<string, string | string[] | undefined>
|
|
112
|
+
): string {
|
|
113
|
+
const forwarded = headers['x-forwarded-for'];
|
|
114
|
+
if (typeof forwarded === 'string') {
|
|
115
|
+
return forwarded.split(',')[0].trim();
|
|
116
|
+
}
|
|
117
|
+
return headers['x-real-ip'] as string || 'unknown';
|
|
118
|
+
}
|