auth-rate-limit.test.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import assert from 'node:assert/strict';
  2. import { test } from 'node:test';
  3. import {
  4. AuthenticationRateLimiter,
  5. authenticateWithRateLimit,
  6. defaultAuthenticationRateLimits,
  7. normalizeAuthenticationAccount,
  8. normalizeAuthenticationIp
  9. } from '../src/auth-rate-limit.js';
  10. test('authentication rate limiter uses production thresholds and normalizes identities', () => {
  11. assert.deepEqual(defaultAuthenticationRateLimits, {
  12. windowMs: 600_000,
  13. blockMs: 600_000,
  14. combinationLimit: 10,
  15. accountLimit: 30,
  16. ipLimit: 100,
  17. maxEntries: 50_000,
  18. cleanupIntervalMs: 60_000
  19. });
  20. assert.equal(normalizeAuthenticationIp('::ffff:192.0.2.10'), '192.0.2.10');
  21. assert.equal(normalizeAuthenticationIp('[2001:DB8::1]'), '2001:db8::1');
  22. assert.equal(normalizeAuthenticationAccount(' Admin@Example.COM '), 'admin@example.com');
  23. });
  24. test('authentication rate limiter blocks an IP and account combination for the configured duration', () => {
  25. let now = 1_000;
  26. const limiter = new AuthenticationRateLimiter({
  27. now: () => now,
  28. windowMs: 10_000,
  29. blockMs: 20_000,
  30. combinationLimit: 3,
  31. accountLimit: 10,
  32. ipLimit: 20
  33. });
  34. const identity = { ip: '192.0.2.1', account: 'user@example.com' };
  35. limiter.recordFailure(identity);
  36. limiter.recordFailure(identity);
  37. assert.equal(limiter.isBlocked(identity), false);
  38. assert.equal(limiter.recordFailure(identity), true);
  39. assert.equal(limiter.isBlocked(identity), true);
  40. now += 19_999;
  41. assert.equal(limiter.isBlocked(identity), true);
  42. now += 1;
  43. assert.equal(limiter.isBlocked(identity), false);
  44. });
  45. test('authentication rate limiter aggregates failures by account across IPs', () => {
  46. const limiter = new AuthenticationRateLimiter({
  47. combinationLimit: 10,
  48. accountLimit: 3,
  49. ipLimit: 20
  50. });
  51. const account = 'user@example.com';
  52. limiter.recordFailure({ ip: '192.0.2.1', account });
  53. limiter.recordFailure({ ip: '192.0.2.2', account });
  54. limiter.recordFailure({ ip: '192.0.2.3', account });
  55. assert.equal(limiter.isBlocked({ ip: '192.0.2.99', account }), true);
  56. assert.equal(limiter.isBlocked({ ip: '192.0.2.99', account: 'other@example.com' }), false);
  57. });
  58. test('authentication rate limiter aggregates failures by IP across accounts', () => {
  59. const limiter = new AuthenticationRateLimiter({
  60. combinationLimit: 10,
  61. accountLimit: 10,
  62. ipLimit: 3
  63. });
  64. const ip = '192.0.2.1';
  65. limiter.recordFailure({ ip, account: 'one@example.com' });
  66. limiter.recordFailure({ ip, account: 'two@example.com' });
  67. limiter.recordFailure({ ip, account: 'three@example.com' });
  68. assert.equal(limiter.isBlocked({ ip, account: 'four@example.com' }), true);
  69. assert.equal(limiter.isBlocked({ ip: '192.0.2.2', account: 'four@example.com' }), false);
  70. });
  71. test('successful authentication clears account records without clearing unrelated IP failures', () => {
  72. const limiter = new AuthenticationRateLimiter({
  73. combinationLimit: 5,
  74. accountLimit: 5,
  75. ipLimit: 3
  76. });
  77. const ip = '192.0.2.1';
  78. const account = 'user@example.com';
  79. limiter.recordFailure({ ip, account });
  80. limiter.recordFailure({ ip, account });
  81. const result = authenticateWithRateLimit({
  82. limiter,
  83. ip,
  84. account,
  85. authenticate: () => ({ id: 1 })
  86. });
  87. assert.deepEqual(result, { id: 1 });
  88. limiter.recordFailure({ ip, account: 'second@example.com' });
  89. assert.equal(limiter.isBlocked({ ip, account: 'third@example.com' }), true);
  90. assert.equal(limiter.isBlocked({ ip: '192.0.2.2', account }), false);
  91. });
  92. test('rate-limited authentication skips credential verification while keeping storage bounded', () => {
  93. let now = 1_000;
  94. const limiter = new AuthenticationRateLimiter({
  95. now: () => now,
  96. windowMs: 100,
  97. blockMs: 100,
  98. combinationLimit: 1,
  99. accountLimit: 100,
  100. ipLimit: 100,
  101. maxEntries: 5,
  102. cleanupIntervalMs: 1
  103. });
  104. let verifications = 0;
  105. assert.equal(authenticateWithRateLimit({
  106. limiter,
  107. ip: '192.0.2.1',
  108. account: 'blocked@example.com',
  109. authenticate: () => {
  110. verifications += 1;
  111. return null;
  112. }
  113. }), null);
  114. assert.equal(authenticateWithRateLimit({
  115. limiter,
  116. ip: '192.0.2.1',
  117. account: 'blocked@example.com',
  118. authenticate: () => {
  119. verifications += 1;
  120. return { id: 1 };
  121. }
  122. }), null);
  123. assert.equal(verifications, 1);
  124. for (let index = 0; index < 20; index += 1) {
  125. limiter.recordFailure({ ip: `198.51.100.${index}`, account: `user-${index}@example.com` });
  126. assert.ok(limiter.entryCount <= 5);
  127. }
  128. now += 201;
  129. limiter.isBlocked({ ip: '203.0.113.1', account: 'fresh@example.com' });
  130. assert.equal(limiter.entryCount, 0);
  131. });