@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.
- package/LICENSE +90 -0
- package/README.md +9 -0
- package/package.json +8 -3
- package/jest.config.js +0 -15
- package/jest.setup.ts +0 -6
- package/src/adapters/database-columns.ts +0 -165
- package/src/adapters/express.adapter.ts +0 -385
- package/src/adapters/fastify.adapter.ts +0 -416
- package/src/adapters/index.ts +0 -16
- package/src/adapters/storage.factory.ts +0 -143
- package/src/bootstrap.ts +0 -374
- package/src/dto/auth-challenge.dto.ts +0 -231
- package/src/dto/auth-response.dto.ts +0 -253
- package/src/dto/challenge-response.dto.ts +0 -234
- package/src/dto/change-password-request.dto.ts +0 -50
- package/src/dto/change-password-response.dto.ts +0 -29
- package/src/dto/change-password.dto.ts +0 -57
- package/src/dto/error-response.dto.ts +0 -136
- package/src/dto/get-available-methods.dto.ts +0 -55
- package/src/dto/get-challenge-data-response.dto.ts +0 -28
- package/src/dto/get-challenge-data.dto.ts +0 -69
- package/src/dto/get-client-info.dto.ts +0 -104
- package/src/dto/get-device-token-response.dto.ts +0 -25
- package/src/dto/get-events-by-type.dto.ts +0 -76
- package/src/dto/get-ip-address-response.dto.ts +0 -24
- package/src/dto/get-mfa-status.dto.ts +0 -94
- package/src/dto/get-risk-assessment-history.dto.ts +0 -39
- package/src/dto/get-session-id-response.dto.ts +0 -25
- package/src/dto/get-setup-data-response.dto.ts +0 -31
- package/src/dto/get-setup-data.dto.ts +0 -75
- package/src/dto/get-suspicious-activity.dto.ts +0 -42
- package/src/dto/get-user-agent-response.dto.ts +0 -23
- package/src/dto/get-user-auth-history.dto.ts +0 -95
- package/src/dto/get-user-by-email.dto.ts +0 -61
- package/src/dto/get-user-by-id.dto.ts +0 -46
- package/src/dto/get-user-devices.dto.ts +0 -53
- package/src/dto/get-user-response.dto.ts +0 -17
- package/src/dto/has-provider.dto.ts +0 -56
- package/src/dto/index.ts +0 -57
- package/src/dto/is-trusted-device-response.dto.ts +0 -34
- package/src/dto/list-providers-response.dto.ts +0 -23
- package/src/dto/login.dto.ts +0 -95
- package/src/dto/logout-all-response.dto.ts +0 -24
- package/src/dto/logout-all.dto.ts +0 -65
- package/src/dto/logout-response.dto.ts +0 -25
- package/src/dto/logout.dto.ts +0 -64
- package/src/dto/refresh-token.dto.ts +0 -36
- package/src/dto/remove-devices.dto.ts +0 -85
- package/src/dto/resend-code-response.dto.ts +0 -32
- package/src/dto/resend-code.dto.ts +0 -51
- package/src/dto/reset-password.dto.ts +0 -115
- package/src/dto/respond-challenge.dto.ts +0 -272
- package/src/dto/set-mfa-exemption.dto.ts +0 -112
- package/src/dto/set-must-change-password-response.dto.ts +0 -27
- package/src/dto/set-must-change-password.dto.ts +0 -46
- package/src/dto/set-preferred-method.dto.ts +0 -80
- package/src/dto/setup-mfa.dto.ts +0 -98
- package/src/dto/signup.dto.ts +0 -174
- package/src/dto/social-auth.dto.ts +0 -422
- package/src/dto/trust-device-response.dto.ts +0 -30
- package/src/dto/trust-device.dto.ts +0 -9
- package/src/dto/update-user-attributes-request.dto.ts +0 -51
- package/src/dto/user-response.dto.ts +0 -138
- package/src/dto/user-update.dto.ts +0 -222
- package/src/dto/verify-email.dto.ts +0 -313
- package/src/dto/verify-mfa-code.dto.ts +0 -103
- package/src/dto/verify-phone-by-sub.dto.ts +0 -78
- package/src/dto/verify-phone.dto.ts +0 -245
- package/src/entities/auth-audit.entity.ts +0 -232
- package/src/entities/challenge-session.entity.ts +0 -116
- package/src/entities/index.ts +0 -29
- package/src/entities/login-attempt.entity.ts +0 -64
- package/src/entities/mfa-device.entity.ts +0 -151
- package/src/entities/rate-limit.entity.ts +0 -44
- package/src/entities/session.entity.ts +0 -180
- package/src/entities/social-account.entity.ts +0 -96
- package/src/entities/storage-lock.entity.ts +0 -39
- package/src/entities/trusted-device.entity.ts +0 -112
- package/src/entities/user.entity.ts +0 -243
- package/src/entities/verification-token.entity.ts +0 -141
- package/src/enums/auth-audit-event-type.enum.ts +0 -360
- package/src/enums/error-codes.enum.ts +0 -420
- package/src/enums/mfa-method.enum.ts +0 -97
- package/src/enums/risk-factor.enum.ts +0 -111
- package/src/exceptions/nauth.exception.ts +0 -231
- package/src/handlers/auth.handler.ts +0 -260
- package/src/handlers/client-info.handler.ts +0 -101
- package/src/handlers/csrf.handler.ts +0 -156
- package/src/handlers/token-delivery.handler.ts +0 -118
- package/src/index.ts +0 -118
- package/src/interfaces/client-info.interface.ts +0 -85
- package/src/interfaces/config.interface.ts +0 -2135
- package/src/interfaces/entities.interface.ts +0 -226
- package/src/interfaces/index.ts +0 -15
- package/src/interfaces/logger.interface.ts +0 -283
- package/src/interfaces/mfa-provider.interface.ts +0 -154
- package/src/interfaces/oauth.interface.ts +0 -148
- package/src/interfaces/provider.interface.ts +0 -47
- package/src/interfaces/social-auth-provider.interface.ts +0 -131
- package/src/interfaces/storage-adapter.interface.ts +0 -82
- package/src/interfaces/template.interface.ts +0 -510
- package/src/interfaces/token-verifier.interface.ts +0 -110
- package/src/internal.ts +0 -178
- package/src/platform/interfaces.ts +0 -299
- package/src/schemas/auth-config.schema.ts +0 -646
- package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
- package/src/services/adaptive-mfa-decision.service.ts +0 -457
- package/src/services/auth-audit.service.spec.ts +0 -675
- package/src/services/auth-audit.service.ts +0 -558
- package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
- package/src/services/auth-challenge-helper.service.ts +0 -825
- package/src/services/auth-flow-context-builder.service.ts +0 -520
- package/src/services/auth-flow-rules.ts +0 -202
- package/src/services/auth-flow-state-definitions.ts +0 -190
- package/src/services/auth-flow-state-machine.service.ts +0 -207
- package/src/services/auth-flow-state-machine.types.ts +0 -316
- package/src/services/auth.service.spec.ts +0 -4195
- package/src/services/auth.service.ts +0 -3727
- package/src/services/challenge.service.spec.ts +0 -1363
- package/src/services/challenge.service.ts +0 -696
- package/src/services/client-info.service.spec.ts +0 -572
- package/src/services/client-info.service.ts +0 -374
- package/src/services/csrf.service.ts +0 -54
- package/src/services/email-verification.service.spec.ts +0 -1229
- package/src/services/email-verification.service.ts +0 -578
- package/src/services/geo-location.service.spec.ts +0 -603
- package/src/services/geo-location.service.ts +0 -599
- package/src/services/index.ts +0 -13
- package/src/services/jwt.service.spec.ts +0 -882
- package/src/services/jwt.service.ts +0 -621
- package/src/services/mfa-base.service.spec.ts +0 -246
- package/src/services/mfa-base.service.ts +0 -611
- package/src/services/mfa.service.spec.ts +0 -693
- package/src/services/mfa.service.ts +0 -960
- package/src/services/password.service.spec.ts +0 -166
- package/src/services/password.service.ts +0 -309
- package/src/services/phone-verification.service.spec.ts +0 -1120
- package/src/services/phone-verification.service.ts +0 -751
- package/src/services/risk-detection.service.spec.ts +0 -1292
- package/src/services/risk-detection.service.ts +0 -1012
- package/src/services/risk-scoring.service.spec.ts +0 -204
- package/src/services/risk-scoring.service.ts +0 -131
- package/src/services/session.service.spec.ts +0 -1293
- package/src/services/session.service.ts +0 -803
- package/src/services/social-account.service.spec.ts +0 -725
- package/src/services/social-auth-base.service.spec.ts +0 -418
- package/src/services/social-auth-base.service.ts +0 -581
- package/src/services/social-auth.service.spec.ts +0 -238
- package/src/services/social-auth.service.ts +0 -436
- package/src/services/social-provider-registry.service.spec.ts +0 -238
- package/src/services/social-provider-registry.service.ts +0 -122
- package/src/services/trusted-device.service.spec.ts +0 -505
- package/src/services/trusted-device.service.ts +0 -339
- package/src/storage/account-lockout-storage.service.spec.ts +0 -310
- package/src/storage/account-lockout-storage.service.ts +0 -89
- package/src/storage/index.ts +0 -3
- package/src/storage/memory-storage.adapter.ts +0 -443
- package/src/storage/rate-limit-storage.service.spec.ts +0 -247
- package/src/storage/rate-limit-storage.service.ts +0 -38
- package/src/templates/html-template.engine.spec.ts +0 -161
- package/src/templates/html-template.engine.ts +0 -688
- package/src/templates/index.ts +0 -7
- package/src/utils/common-passwords.spec.ts +0 -230
- package/src/utils/common-passwords.ts +0 -170
- package/src/utils/context-storage.ts +0 -188
- package/src/utils/cookie-names.util.ts +0 -67
- package/src/utils/cookies.util.ts +0 -94
- package/src/utils/index.ts +0 -12
- package/src/utils/ip-extractor.spec.ts +0 -330
- package/src/utils/ip-extractor.ts +0 -220
- package/src/utils/nauth-logger.spec.ts +0 -388
- package/src/utils/nauth-logger.ts +0 -215
- package/src/utils/pii-redactor.spec.ts +0 -130
- package/src/utils/pii-redactor.ts +0 -288
- package/src/utils/setup/get-repositories.ts +0 -140
- package/src/utils/setup/init-services.ts +0 -422
- package/src/utils/setup/init-social.ts +0 -189
- package/src/utils/setup/init-storage.ts +0 -94
- package/src/utils/setup/register-mfa.ts +0 -165
- package/src/utils/setup/run-nauth-migrations.ts +0 -61
- package/src/utils/token-delivery-policy.ts +0 -38
- package/src/validators/template.validator.ts +0 -219
- package/tsconfig.json +0 -37
- 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
|
-
}
|
package/src/utils/index.ts
DELETED
|
@@ -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
|
-
}
|