@nauth-toolkit/core 0.1.0 → 0.1.5

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 (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +9 -0
  3. package/package.json +8 -3
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. package/tsconfig.lint.json +0 -6
@@ -1,94 +0,0 @@
1
- /**
2
- * Cookie Utilities
3
- *
4
- * Helpers for clearing nauth auth cookies in HTTP responses.
5
- */
6
-
7
- import { NAuthConfig } from '../interfaces/config.interface';
8
- import {
9
- getAccessTokenCookieName,
10
- getRefreshTokenCookieName,
11
- getCsrfTokenCookieName,
12
- getDeviceTokenCookieName,
13
- } from './cookie-names.util';
14
-
15
- export interface CookieOptions {
16
- domain?: string;
17
- path?: string;
18
- secure?: boolean;
19
- sameSite?: 'strict' | 'lax' | 'none';
20
- }
21
-
22
- /**
23
- * Clear nauth auth cookies on the response.
24
- *
25
- * - Clears access token, refresh token, CSRF token cookies
26
- * - Optionally clears device token cookie (only when forgetDevice=true)
27
- * - Device token cookies persist across logout by default (remember device feature)
28
- * - Applies security attributes consistent with how cookies were set
29
- * - Uses configured cookie name prefix (default: 'nauth_')
30
- *
31
- * @param res - HTTP response object (Express or Fastify compatible)
32
- * @param config - NAuth configuration (optional, for cookie name resolution)
33
- * @param opt - Optional cookie options to match configured attributes
34
- * @param forgetDevice - If true, also clears device token cookie (for "forget me" logout). Default: false
35
- */
36
- export function clearAuthCookies(
37
- res: { cookie?: Function; setCookie?: Function },
38
- config?: NAuthConfig | CookieOptions,
39
- opt?: CookieOptions,
40
- forgetDevice: boolean = false,
41
- ): void {
42
- // Handle old signature: clearAuthCookies(res, opt) where opt might be config or CookieOptions
43
- let cookieOptions: CookieOptions | undefined;
44
- let nauthConfig: NAuthConfig | undefined;
45
-
46
- if (config && 'tokenDelivery' in config) {
47
- // Second param is NAuthConfig
48
- nauthConfig = config as NAuthConfig;
49
- cookieOptions = opt;
50
- } else {
51
- // Second param is CookieOptions (backward compatibility)
52
- cookieOptions = config as CookieOptions | undefined;
53
- }
54
-
55
- const base = {
56
- httpOnly: true as const,
57
- secure: cookieOptions?.secure !== false,
58
- sameSite: (cookieOptions?.sameSite || 'strict') as 'strict' | 'lax' | 'none',
59
- path: cookieOptions?.path || '/',
60
- domain: cookieOptions?.domain,
61
- maxAge: 0,
62
- };
63
-
64
- const accessTokenName = getAccessTokenCookieName(nauthConfig);
65
- const refreshTokenName = getRefreshTokenCookieName(nauthConfig);
66
- const csrfTokenName = getCsrfTokenCookieName(nauthConfig);
67
- const deviceTokenName = getDeviceTokenCookieName(nauthConfig);
68
-
69
- // CSRF cookie options (httpOnly: false, matches how it was set)
70
- const csrfBase = {
71
- ...base,
72
- httpOnly: false as const, // CSRF token must be readable by JavaScript
73
- };
74
-
75
- if (typeof res.cookie === 'function') {
76
- res.cookie(accessTokenName, '', base);
77
- res.cookie(refreshTokenName, '', base);
78
- res.cookie(csrfTokenName, '', csrfBase);
79
- // Only clear device token cookie if forgetDevice=true (for "forget me" logout)
80
- // Device tokens persist across normal logout (remember device feature)
81
- if (forgetDevice) {
82
- res.cookie(deviceTokenName, '', base); // Device token cookie (httpOnly: true)
83
- }
84
- } else if (typeof res.setCookie === 'function') {
85
- res.setCookie(accessTokenName, '', base);
86
- res.setCookie(refreshTokenName, '', base);
87
- res.setCookie(csrfTokenName, '', csrfBase);
88
- // Only clear device token cookie if forgetDevice=true (for "forget me" logout)
89
- // Device tokens persist across normal logout (remember device feature)
90
- if (forgetDevice) {
91
- res.setCookie(deviceTokenName, '', base); // Device token cookie (httpOnly: true)
92
- }
93
- }
94
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * Utility Functions and Classes
3
- */
4
-
5
- export * from './pii-redactor';
6
- export * from './ip-extractor';
7
- export * from './nauth-logger';
8
- export * from './cookies.util';
9
- export * from './cookie-names.util';
10
- export * from './context-storage';
11
- export * from './token-delivery-policy';
12
- // user-agent-parser removed - functionality moved to ClientInfoService.parseUserAgent()
@@ -1,330 +0,0 @@
1
- /**
2
- * Tests for IP Address Extractor
3
- */
4
-
5
- import { extractClientIp } from './ip-extractor';
6
-
7
- describe('extractClientIp', () => {
8
- describe('X-Forwarded-For header', () => {
9
- it('should extract leftmost IP from X-Forwarded-For header', () => {
10
- const req = {
11
- headers: {
12
- 'x-forwarded-for': '203.0.113.1, 70.41.3.18, 150.172.238.178',
13
- },
14
- };
15
-
16
- const result = extractClientIp(req);
17
- expect(result).toBe('203.0.113.1');
18
- });
19
-
20
- it('should handle single IP in X-Forwarded-For', () => {
21
- const req = {
22
- headers: {
23
- 'x-forwarded-for': '203.0.113.1',
24
- },
25
- };
26
-
27
- const result = extractClientIp(req);
28
- expect(result).toBe('203.0.113.1');
29
- });
30
-
31
- it('should trim whitespace from IPs', () => {
32
- const req = {
33
- headers: {
34
- 'x-forwarded-for': ' 203.0.113.1 , 70.41.3.18 ',
35
- },
36
- };
37
-
38
- const result = extractClientIp(req);
39
- expect(result).toBe('203.0.113.1');
40
- });
41
- });
42
-
43
- describe('Cloudflare headers', () => {
44
- it('should use CF-Connecting-IP when X-Forwarded-For is not present', () => {
45
- const req = {
46
- headers: {
47
- 'cf-connecting-ip': '203.0.113.1',
48
- },
49
- };
50
-
51
- const result = extractClientIp(req);
52
- expect(result).toBe('203.0.113.1');
53
- });
54
-
55
- it('should prefer X-Forwarded-For over CF-Connecting-IP (standard priority)', () => {
56
- const req = {
57
- headers: {
58
- 'cf-connecting-ip': '203.0.113.1',
59
- 'x-forwarded-for': '10.0.0.1',
60
- },
61
- };
62
-
63
- const result = extractClientIp(req);
64
- // X-Forwarded-For has highest priority by design
65
- expect(result).toBe('10.0.0.1');
66
- });
67
- });
68
-
69
- describe('Nginx X-Real-IP header', () => {
70
- it('should use X-Real-IP header when present', () => {
71
- const req = {
72
- headers: {
73
- 'x-real-ip': '203.0.113.1',
74
- },
75
- };
76
-
77
- const result = extractClientIp(req);
78
- expect(result).toBe('203.0.113.1');
79
- });
80
- });
81
-
82
- describe('Apache X-Client-IP header', () => {
83
- it('should use X-Client-IP header when present', () => {
84
- const req = {
85
- headers: {
86
- 'x-client-ip': '203.0.113.1',
87
- },
88
- };
89
-
90
- const result = extractClientIp(req);
91
- expect(result).toBe('203.0.113.1');
92
- });
93
- });
94
-
95
- describe('Priority order', () => {
96
- it('should prefer X-Forwarded-For over other headers', () => {
97
- const req = {
98
- headers: {
99
- 'x-forwarded-for': '203.0.113.1',
100
- 'cf-connecting-ip': '192.168.1.1',
101
- 'x-real-ip': '10.0.0.1',
102
- 'x-client-ip': '172.16.0.1',
103
- },
104
- };
105
-
106
- const result = extractClientIp(req);
107
- expect(result).toBe('203.0.113.1');
108
- });
109
-
110
- it('should fallback to CF-Connecting-IP if X-Forwarded-For is missing', () => {
111
- const req = {
112
- headers: {
113
- 'cf-connecting-ip': '203.0.113.1',
114
- 'x-real-ip': '10.0.0.1',
115
- },
116
- };
117
-
118
- const result = extractClientIp(req);
119
- expect(result).toBe('203.0.113.1');
120
- });
121
- });
122
-
123
- describe('IPv6 handling', () => {
124
- it('should convert ::1 to 127.0.0.1', () => {
125
- const req = {
126
- ip: '::1',
127
- };
128
-
129
- const result = extractClientIp(req);
130
- expect(result).toBe('127.0.0.1');
131
- });
132
-
133
- it('should convert ::ffff:127.0.0.1 to 127.0.0.1', () => {
134
- const req = {
135
- ip: '::ffff:127.0.0.1',
136
- };
137
-
138
- const result = extractClientIp(req);
139
- expect(result).toBe('127.0.0.1');
140
- });
141
-
142
- it('should strip ::ffff: prefix from IPv4-mapped IPv6 addresses', () => {
143
- const req = {
144
- ip: '::ffff:203.0.113.1',
145
- };
146
-
147
- const result = extractClientIp(req);
148
- expect(result).toBe('203.0.113.1');
149
- });
150
-
151
- it('should handle pure IPv6 addresses', () => {
152
- const req = {
153
- headers: {
154
- 'x-forwarded-for': '2001:db8::1',
155
- },
156
- };
157
-
158
- const result = extractClientIp(req);
159
- expect(result).toBe('2001:db8::1');
160
- });
161
- });
162
-
163
- describe('Fallback to req.ip', () => {
164
- it('should use req.ip when no headers are present', () => {
165
- const req = {
166
- ip: '203.0.113.1',
167
- headers: {},
168
- };
169
-
170
- const result = extractClientIp(req);
171
- expect(result).toBe('203.0.113.1');
172
- });
173
-
174
- it('should use req.socket.remoteAddress as final fallback', () => {
175
- const req = {
176
- headers: {},
177
- socket: {
178
- remoteAddress: '203.0.113.1',
179
- },
180
- };
181
-
182
- const result = extractClientIp(req);
183
- expect(result).toBe('203.0.113.1');
184
- });
185
-
186
- it('should return 0.0.0.0 when all sources are missing', () => {
187
- const req = {
188
- headers: {},
189
- };
190
-
191
- const result = extractClientIp(req);
192
- expect(result).toBe('0.0.0.0');
193
- });
194
- });
195
-
196
- describe('Invalid IP handling', () => {
197
- it('should skip invalid IPv4 addresses', () => {
198
- const req = {
199
- headers: {
200
- 'x-forwarded-for': '999.999.999.999, 203.0.113.1',
201
- },
202
- };
203
-
204
- const result = extractClientIp(req);
205
- // Should skip invalid and use fallback
206
- expect(result).not.toBe('999.999.999.999');
207
- });
208
-
209
- it('should validate IPv4 octet ranges', () => {
210
- const req = {
211
- headers: {
212
- 'x-forwarded-for': '256.1.1.1',
213
- },
214
- ip: '203.0.113.1',
215
- };
216
-
217
- const result = extractClientIp(req);
218
- expect(result).toBe('203.0.113.1'); // Falls back to req.ip
219
- });
220
- });
221
-
222
- describe('Private IP filtering', () => {
223
- it('should skip private IPs when filterPrivateIps is true', () => {
224
- const req = {
225
- headers: {
226
- 'x-forwarded-for': '10.0.0.1, 203.0.113.1',
227
- },
228
- };
229
-
230
- const result = extractClientIp(req, { filterPrivateIps: true });
231
- // Should skip 10.0.0.1 and use fallback or next IP
232
- expect(result).not.toBe('10.0.0.1');
233
- });
234
-
235
- it('should allow private IPs when filterPrivateIps is false', () => {
236
- const req = {
237
- headers: {
238
- 'x-forwarded-for': '10.0.0.1',
239
- },
240
- };
241
-
242
- const result = extractClientIp(req, { filterPrivateIps: false });
243
- expect(result).toBe('10.0.0.1');
244
- });
245
-
246
- it('should detect 192.168.x.x as private', () => {
247
- const req = {
248
- headers: {
249
- 'x-forwarded-for': '192.168.1.100',
250
- },
251
- ip: '203.0.113.1',
252
- };
253
-
254
- const result = extractClientIp(req, { filterPrivateIps: true });
255
- expect(result).toBe('203.0.113.1'); // Falls back to public IP
256
- });
257
-
258
- it('should detect 172.16-31.x.x as private', () => {
259
- const req = {
260
- headers: {
261
- 'x-forwarded-for': '172.16.0.1',
262
- },
263
- ip: '203.0.113.1',
264
- };
265
-
266
- const result = extractClientIp(req, { filterPrivateIps: true });
267
- expect(result).toBe('203.0.113.1');
268
- });
269
- });
270
-
271
- describe('Case insensitive headers', () => {
272
- it('should handle uppercase header names', () => {
273
- const req = {
274
- headers: {
275
- 'X-FORWARDED-FOR': '203.0.113.1',
276
- },
277
- };
278
-
279
- const result = extractClientIp(req);
280
- expect(result).toBe('203.0.113.1');
281
- });
282
-
283
- it('should handle PascalCase header names', () => {
284
- const req = {
285
- headers: {
286
- 'X-Forwarded-For': '203.0.113.1',
287
- },
288
- };
289
-
290
- const result = extractClientIp(req);
291
- expect(result).toBe('203.0.113.1');
292
- });
293
- });
294
-
295
- describe('Production scenarios', () => {
296
- it('should handle AWS ALB X-Forwarded-For format', () => {
297
- const req = {
298
- headers: {
299
- 'x-forwarded-for': '203.0.113.1, 172.31.1.1',
300
- },
301
- };
302
-
303
- const result = extractClientIp(req);
304
- expect(result).toBe('203.0.113.1'); // Real client IP
305
- });
306
-
307
- it('should handle Nginx proxy chain', () => {
308
- const req = {
309
- headers: {
310
- 'x-real-ip': '203.0.113.1',
311
- },
312
- };
313
-
314
- const result = extractClientIp(req);
315
- expect(result).toBe('203.0.113.1');
316
- });
317
-
318
- it('should handle Cloudflare CDN', () => {
319
- const req = {
320
- headers: {
321
- 'cf-connecting-ip': '203.0.113.1',
322
- 'x-forwarded-for': '203.0.113.1, 172.16.0.1',
323
- },
324
- };
325
-
326
- const result = extractClientIp(req);
327
- expect(result).toBe('203.0.113.1');
328
- });
329
- });
330
- });
@@ -1,220 +0,0 @@
1
- /**
2
- * IP Address Extractor
3
- *
4
- * Extracts the real client IP address from requests, handling:
5
- * - Direct connections
6
- * - Reverse proxies (Nginx, Apache)
7
- * - Load balancers (AWS ALB/NLB, GCP, Azure)
8
- * - CDNs (Cloudflare, Fastly, Akamai)
9
- *
10
- * **Priority Order:**
11
- * 1. X-Forwarded-For (standard proxy header)
12
- * 2. CF-Connecting-IP (Cloudflare)
13
- * 3. X-Real-IP (Nginx proxy)
14
- * 4. X-Client-IP (Apache, other proxies)
15
- * 5. Fastly-Client-IP (Fastly CDN)
16
- * 6. Akamai-Origin-Hop (Akamai CDN)
17
- * 7. req.ip (NestJS/Express default)
18
- * 8. req.socket.remoteAddress (fallback)
19
- *
20
- * **Security:**
21
- * - Handles multiple proxies (takes leftmost IP)
22
- * - Validates IP format
23
- * - Filters private/internal IPs (optional)
24
- * - Prevents IP spoofing
25
- *
26
- * @example
27
- * ```typescript
28
- * import { extractClientIp } from '@nauth-toolkit/core/utils';
29
- *
30
- * @Post('login')
31
- * async login(@Req() req: Request) {
32
- * const ipAddress = extractClientIp(req);
33
- * logger.debug('Client IP:', ipAddress); // Real client IP
34
- * }
35
- * ```
36
- */
37
-
38
- // No need to import Request from express - we use 'any' to avoid dependency
39
-
40
- /**
41
- * Options for IP extraction
42
- */
43
- export interface IpExtractorOptions {
44
- /**
45
- * Whether to filter out private/internal IP addresses
46
- * Defaults to false
47
- */
48
- filterPrivateIps?: boolean;
49
-
50
- /**
51
- * List of trusted proxy IP addresses or CIDR ranges
52
- * If specified, only accepts X-Forwarded-For from these proxies
53
- */
54
- trustedProxies?: string[];
55
-
56
- /**
57
- * Whether to use the leftmost IP in X-Forwarded-For
58
- * (true = original client, false = rightmost/last proxy)
59
- * Defaults to true
60
- */
61
- useLeftmostIp?: boolean;
62
- }
63
-
64
- /**
65
- * Extracts the real client IP address from an HTTP request
66
- *
67
- * @param req - Express Request object
68
- * @param options - Optional configuration
69
- * @returns The client's IP address, or '0.0.0.0' if unable to determine
70
- */
71
- export function extractClientIp(req: any, options: IpExtractorOptions = {}): string {
72
- const { filterPrivateIps = false, useLeftmostIp = true } = options;
73
-
74
- // Priority order of headers to check
75
- const headers = [
76
- 'x-forwarded-for', // Standard proxy header (comma-separated)
77
- 'cf-connecting-ip', // Cloudflare
78
- 'x-real-ip', // Nginx
79
- 'x-client-ip', // Apache, other proxies
80
- 'fastly-client-ip', // Fastly CDN
81
- 'akamai-origin-hop', // Akamai CDN
82
- 'true-client-ip', // Cloudflare Enterprise
83
- 'x-original-forwarded-for', // AWS ALB
84
- ];
85
-
86
- // Ensure headers object exists
87
- const reqHeaders = req.headers || {};
88
-
89
- // Try each header in priority order
90
- for (const header of headers) {
91
- // Try multiple case variations
92
- const variations = [
93
- header, // lowercase: x-forwarded-for
94
- header.toUpperCase(), // uppercase: X-FORWARDED-FOR
95
- header
96
- .split('-')
97
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
98
- .join('-'), // PascalCase: X-Forwarded-For
99
- ];
100
-
101
- let value = null;
102
- for (const variant of variations) {
103
- if (reqHeaders[variant]) {
104
- value = reqHeaders[variant];
105
- break;
106
- }
107
- }
108
-
109
- if (value) {
110
- const ip = extractIpFromHeader(value, useLeftmostIp);
111
- if (ip && isValidIp(ip)) {
112
- if (filterPrivateIps && isPrivateIp(ip)) {
113
- continue; // Skip private IPs
114
- }
115
- return ip;
116
- }
117
- }
118
- }
119
-
120
- // Fallback to NestJS/Express defaults
121
- const fallbackIp = req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || '0.0.0.0';
122
-
123
- // Clean up IPv6 localhost to IPv4
124
- if (fallbackIp === '::1' || fallbackIp === '::ffff:127.0.0.1') {
125
- return '127.0.0.1';
126
- }
127
-
128
- // Strip IPv6 prefix if present
129
- const cleanIp = fallbackIp.replace(/^::ffff:/, '');
130
-
131
- return cleanIp;
132
- }
133
-
134
- /**
135
- * Extracts IP address from header value
136
- *
137
- * @param value - Header value (may be comma-separated list)
138
- * @param useLeftmost - Whether to use leftmost (original client) or rightmost (last proxy)
139
- * @returns Extracted IP address or null
140
- */
141
- function extractIpFromHeader(value: string | string[], useLeftmost: boolean): string | null {
142
- const valueStr = Array.isArray(value) ? value[0] : value;
143
-
144
- if (!valueStr) return null;
145
-
146
- // Split by comma (X-Forwarded-For can have multiple IPs)
147
- const ips = valueStr
148
- .split(',')
149
- .map((ip) => ip.trim())
150
- .filter(Boolean);
151
-
152
- if (ips.length === 0) return null;
153
-
154
- // Return leftmost (original client) or rightmost (last proxy)
155
- return useLeftmost ? ips[0] : ips[ips.length - 1];
156
- }
157
-
158
- /**
159
- * Validates if a string is a valid IPv4 or IPv6 address
160
- *
161
- * @param ip - IP address to validate
162
- * @returns True if valid, false otherwise
163
- */
164
- function isValidIp(ip: string): boolean {
165
- // IPv4 validation
166
- const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
167
- if (ipv4Regex.test(ip)) {
168
- const parts = ip.split('.').map(Number);
169
- return parts.every((part) => part >= 0 && part <= 255);
170
- }
171
-
172
- // IPv6 validation (simplified)
173
- const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
174
- return ipv6Regex.test(ip);
175
- }
176
-
177
- /**
178
- * Checks if an IP address is private/internal
179
- *
180
- * Detects:
181
- * - Localhost (127.0.0.0/8, ::1)
182
- * - Private IPv4 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
183
- * - Link-local addresses (169.254.0.0/16)
184
- *
185
- * @param ip - IP address to check
186
- * @returns True if private, false otherwise
187
- *
188
- * @example
189
- * ```typescript
190
- * isPrivateIp('192.168.1.1'); // true
191
- * isPrivateIp('8.8.8.8'); // false
192
- * ```
193
- */
194
- export function isPrivateIp(ip: string): boolean {
195
- // Localhost
196
- if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('127.')) {
197
- return true;
198
- }
199
-
200
- // Private IPv4 ranges
201
- const privateRanges = [
202
- /^10\./, // 10.0.0.0/8
203
- /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12
204
- /^192\.168\./, // 192.168.0.0/16
205
- /^169\.254\./, // Link-local (169.254.0.0/16)
206
- ];
207
-
208
- return privateRanges.some((regex) => regex.test(ip));
209
- }
210
-
211
- /**
212
- * Gets geolocation information for an IP address (placeholder)
213
- *
214
- * @param ip - IP address
215
- * @returns Geolocation info (to be implemented with MaxMind/IP-API)
216
- */
217
- export function getIpGeolocation(_ip: string): { country?: string; city?: string } {
218
- // TODO: Implement with MaxMind GeoIP2 or IP-API
219
- return {};
220
- }