@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,231 +0,0 @@
|
|
|
1
|
-
import { AuthErrorCode } from '../enums/error-codes.enum';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Custom exception for nauth-toolkit
|
|
5
|
-
*
|
|
6
|
-
* **Framework-Agnostic Design:**
|
|
7
|
-
* This exception extends standard `Error`, not `HttpException`, making it
|
|
8
|
-
* usable in any context:
|
|
9
|
-
* - HTTP APIs (REST, NestJS)
|
|
10
|
-
* - WebSocket connections
|
|
11
|
-
* - GraphQL resolvers
|
|
12
|
-
* - gRPC services
|
|
13
|
-
* - Message queue workers
|
|
14
|
-
* - CLI tools
|
|
15
|
-
* - Standalone services
|
|
16
|
-
*
|
|
17
|
-
* **Consumer Responsibility:**
|
|
18
|
-
* The consumer application decides how to map these domain exceptions
|
|
19
|
-
* to their transport layer (HTTP status codes, WebSocket events, etc.)
|
|
20
|
-
*
|
|
21
|
-
* **Structured Error Data:**
|
|
22
|
-
* Provides error code, message, and optional metadata. Consumer can
|
|
23
|
-
* transform this into any response format needed.
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```typescript
|
|
27
|
-
* // Throw domain exception
|
|
28
|
-
* throw new NAuthException(
|
|
29
|
-
* AuthErrorCode.RATE_LIMIT_SMS,
|
|
30
|
-
* 'Too many verification SMS sent',
|
|
31
|
-
* { retryAfter: 3600, maxAttempts: 3 }
|
|
32
|
-
* );
|
|
33
|
-
*
|
|
34
|
-
* // Consumer maps to HTTP (if using HTTP)
|
|
35
|
-
* catch (error) {
|
|
36
|
-
* if (error instanceof NAuthException) {
|
|
37
|
-
* const statusCode = this.mapErrorCodeToHttpStatus(error.code);
|
|
38
|
-
* return res.status(statusCode).json({
|
|
39
|
-
* code: error.code,
|
|
40
|
-
* message: error.message,
|
|
41
|
-
* details: error.details,
|
|
42
|
-
* timestamp: new Date().toISOString()
|
|
43
|
-
* });
|
|
44
|
-
* }
|
|
45
|
-
* }
|
|
46
|
-
*
|
|
47
|
-
* // Or map to WebSocket
|
|
48
|
-
* catch (error) {
|
|
49
|
-
* if (error instanceof NAuthException) {
|
|
50
|
-
* socket.emit('error', {
|
|
51
|
-
* code: error.code,
|
|
52
|
-
* message: error.message,
|
|
53
|
-
* details: error.details
|
|
54
|
-
* });
|
|
55
|
-
* }
|
|
56
|
-
* }
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
export class NAuthException extends Error {
|
|
60
|
-
/**
|
|
61
|
-
* Error code for programmatic handling
|
|
62
|
-
*/
|
|
63
|
-
public readonly code: AuthErrorCode;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Additional error details/metadata
|
|
67
|
-
*/
|
|
68
|
-
public readonly details?: Record<string, unknown>;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Timestamp when error was created
|
|
72
|
-
*/
|
|
73
|
-
public readonly timestamp: string;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Create a new NAuthException
|
|
77
|
-
*
|
|
78
|
-
* @param code - Error code from AuthErrorCode enum
|
|
79
|
-
* @param message - Human-readable error message
|
|
80
|
-
* @param details - Optional metadata (retryAfter, validation errors, etc.)
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* ```typescript
|
|
84
|
-
* throw new NAuthException(
|
|
85
|
-
* AuthErrorCode.INVALID_CREDENTIALS,
|
|
86
|
-
* 'Invalid email or password'
|
|
87
|
-
* );
|
|
88
|
-
*
|
|
89
|
-
* throw new NAuthException(
|
|
90
|
-
* AuthErrorCode.RATE_LIMIT_SMS,
|
|
91
|
-
* 'Too many SMS sent',
|
|
92
|
-
* { retryAfter: 3600, currentCount: 4 }
|
|
93
|
-
* );
|
|
94
|
-
* ```
|
|
95
|
-
*/
|
|
96
|
-
constructor(code: AuthErrorCode, message: string, details?: Record<string, unknown>) {
|
|
97
|
-
super(message);
|
|
98
|
-
|
|
99
|
-
this.code = code;
|
|
100
|
-
this.details = details;
|
|
101
|
-
this.timestamp = new Date().toISOString();
|
|
102
|
-
this.name = 'NAuthException';
|
|
103
|
-
|
|
104
|
-
// Ensure proper prototype chain for instanceof checks
|
|
105
|
-
Object.setPrototypeOf(this, NAuthException.prototype);
|
|
106
|
-
|
|
107
|
-
// Capture stack trace (excluding constructor call)
|
|
108
|
-
if (Error.captureStackTrace) {
|
|
109
|
-
Error.captureStackTrace(this, this.constructor);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Get the error code
|
|
115
|
-
*
|
|
116
|
-
* @returns Error code
|
|
117
|
-
*/
|
|
118
|
-
getCode(): AuthErrorCode {
|
|
119
|
-
return this.code;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Get error details/metadata
|
|
124
|
-
*
|
|
125
|
-
* @returns Error details or undefined
|
|
126
|
-
*/
|
|
127
|
-
getDetails(): Record<string, unknown> | undefined {
|
|
128
|
-
return this.details;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Check if error is a specific code
|
|
133
|
-
*
|
|
134
|
-
* @param code - Error code to check
|
|
135
|
-
* @returns True if error matches code
|
|
136
|
-
*
|
|
137
|
-
* @example
|
|
138
|
-
* ```typescript
|
|
139
|
-
* try {
|
|
140
|
-
* await sendSMS();
|
|
141
|
-
* } catch (error) {
|
|
142
|
-
* if (error instanceof NAuthException && error.isCode(AuthErrorCode.RATE_LIMIT_SMS)) {
|
|
143
|
-
* // Handle rate limit specifically
|
|
144
|
-
* }
|
|
145
|
-
* }
|
|
146
|
-
* ```
|
|
147
|
-
*/
|
|
148
|
-
isCode(code: AuthErrorCode): boolean {
|
|
149
|
-
return this.code === code;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Serialize error to plain object
|
|
154
|
-
*
|
|
155
|
-
* Useful for logging, HTTP responses, or any serialization needs.
|
|
156
|
-
*
|
|
157
|
-
* @returns Plain object representation
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* ```typescript
|
|
161
|
-
* catch (error) {
|
|
162
|
-
* if (error instanceof NAuthException) {
|
|
163
|
-
* console.log(error.toJSON());
|
|
164
|
-
* // { code: 'RATE_LIMIT_SMS', message: '...', details: {...}, timestamp: '...' }
|
|
165
|
-
* }
|
|
166
|
-
* }
|
|
167
|
-
* ```
|
|
168
|
-
*/
|
|
169
|
-
toJSON(): { code: string; message: string; details?: Record<string, unknown>; timestamp: string } {
|
|
170
|
-
return {
|
|
171
|
-
code: this.code,
|
|
172
|
-
message: this.message,
|
|
173
|
-
details: this.details,
|
|
174
|
-
timestamp: this.timestamp,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Helper function to map error codes to suggested HTTP status codes
|
|
181
|
-
*
|
|
182
|
-
* **Optional** - Consumer can use this or define their own mapping.
|
|
183
|
-
* Provided as a convenience for HTTP-based applications.
|
|
184
|
-
*
|
|
185
|
-
* @param code - Error code
|
|
186
|
-
* @returns Suggested HTTP status code
|
|
187
|
-
*
|
|
188
|
-
* @example
|
|
189
|
-
* ```typescript
|
|
190
|
-
* // In NestJS exception filter
|
|
191
|
-
* catch (exception: NAuthException, host: ArgumentsHost) {
|
|
192
|
-
* const statusCode = getHttpStatusForErrorCode(exception.code);
|
|
193
|
-
* const response = host.switchToHttp().getResponse();
|
|
194
|
-
* response.status(statusCode).json(exception.toJSON());
|
|
195
|
-
* }
|
|
196
|
-
* ```
|
|
197
|
-
*/
|
|
198
|
-
export function getHttpStatusForErrorCode(code: AuthErrorCode): number {
|
|
199
|
-
// Rate limits
|
|
200
|
-
if (code.startsWith('RATE_LIMIT_')) return 429;
|
|
201
|
-
|
|
202
|
-
// Authentication errors
|
|
203
|
-
if (code.startsWith('AUTH_')) {
|
|
204
|
-
if (code === AuthErrorCode.ACCOUNT_INACTIVE || code === AuthErrorCode.ACCOUNT_LOCKED) return 403;
|
|
205
|
-
return 401;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Signup conflicts
|
|
209
|
-
if (
|
|
210
|
-
code === AuthErrorCode.EMAIL_EXISTS ||
|
|
211
|
-
code === AuthErrorCode.USERNAME_EXISTS ||
|
|
212
|
-
code === AuthErrorCode.PHONE_EXISTS
|
|
213
|
-
)
|
|
214
|
-
return 409;
|
|
215
|
-
if (code === AuthErrorCode.SIGNUP_DISABLED) return 403;
|
|
216
|
-
|
|
217
|
-
// Validation errors
|
|
218
|
-
if (code.startsWith('VALIDATION_') || code.startsWith('INVALID_')) return 400;
|
|
219
|
-
|
|
220
|
-
// Not found
|
|
221
|
-
if (code === AuthErrorCode.NOT_FOUND) return 404;
|
|
222
|
-
|
|
223
|
-
// Forbidden
|
|
224
|
-
if (code === AuthErrorCode.FORBIDDEN) return 403;
|
|
225
|
-
|
|
226
|
-
// Server errors
|
|
227
|
-
if (code === AuthErrorCode.INTERNAL_ERROR || code === AuthErrorCode.SERVICE_UNAVAILABLE) return 500;
|
|
228
|
-
|
|
229
|
-
// Default to 400
|
|
230
|
-
return 400;
|
|
231
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authentication Handler
|
|
3
|
-
*
|
|
4
|
-
* Validates JWT tokens and attaches user to request.
|
|
5
|
-
*
|
|
6
|
-
* **Platform-Agnostic:**
|
|
7
|
-
* This handler operates purely on NAuthRequest interface.
|
|
8
|
-
* Context is managed by the adapter, not this handler.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Repository } from 'typeorm';
|
|
12
|
-
import {
|
|
13
|
-
NAuthConfig,
|
|
14
|
-
NAuthException,
|
|
15
|
-
AuthErrorCode,
|
|
16
|
-
resolveDeliveryForRequest,
|
|
17
|
-
BaseUser,
|
|
18
|
-
getAccessTokenCookieName,
|
|
19
|
-
NAuthLogger,
|
|
20
|
-
ContextStorage,
|
|
21
|
-
IClientInfo,
|
|
22
|
-
} from '../index';
|
|
23
|
-
import { JwtService, SessionService } from '../internal';
|
|
24
|
-
import { NAuthRequest, NAuthResponse } from '../platform/interfaces';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* AuthHandler
|
|
28
|
-
*
|
|
29
|
-
* Validates JWT tokens and populates user context.
|
|
30
|
-
* Performs optional authentication by default (doesn't reject unauthenticated requests).
|
|
31
|
-
*/
|
|
32
|
-
export class AuthHandler {
|
|
33
|
-
constructor(
|
|
34
|
-
private jwtService: JwtService,
|
|
35
|
-
private sessionService: SessionService,
|
|
36
|
-
private userRepository: Repository<BaseUser>,
|
|
37
|
-
private config: NAuthConfig,
|
|
38
|
-
private logger?: NAuthLogger,
|
|
39
|
-
) {}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Handle request - validate token and attach user
|
|
43
|
-
*
|
|
44
|
-
* Note: Context is managed by adapter. This handler assumes context is available.
|
|
45
|
-
*/
|
|
46
|
-
public async handle(req: NAuthRequest, _res: NAuthResponse, next: () => Promise<void> | void): Promise<void> {
|
|
47
|
-
try {
|
|
48
|
-
// Skip if route is marked as public
|
|
49
|
-
if (req.attributes.nauthPublic) {
|
|
50
|
-
await next();
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const token = this.extractToken(req);
|
|
55
|
-
|
|
56
|
-
if (!token) {
|
|
57
|
-
// No token - continue without authentication (optional auth)
|
|
58
|
-
await next();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const validation = await this.jwtService.validateAccessToken(token);
|
|
63
|
-
|
|
64
|
-
if (!validation.valid) {
|
|
65
|
-
this.logger?.debug?.('Invalid token:', validation.error);
|
|
66
|
-
await next();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Validate session
|
|
71
|
-
const sessionId = validation.payload!.sessionId;
|
|
72
|
-
const userId = validation.payload!.sub; // Extract userId from token sub claim
|
|
73
|
-
const session = await this.sessionService.findByIdLight(sessionId);
|
|
74
|
-
|
|
75
|
-
if (!session) {
|
|
76
|
-
this.logger?.debug?.('Session not found:', sessionId);
|
|
77
|
-
await next();
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const initialVersion = session.version;
|
|
82
|
-
|
|
83
|
-
if (session.isRevoked) {
|
|
84
|
-
this.logger?.warn?.('Session has been revoked:', sessionId);
|
|
85
|
-
await next();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (session.expiresAt < new Date()) {
|
|
90
|
-
this.logger?.debug?.('Session has expired:', sessionId);
|
|
91
|
-
await next();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Load user
|
|
96
|
-
const user = await this.userRepository.findOne({
|
|
97
|
-
select: this.getUserSelectFields(),
|
|
98
|
-
where: { sub: validation.payload!.sub },
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
if (!user) {
|
|
102
|
-
this.logger?.warn?.('User not found:', validation.payload!.sub);
|
|
103
|
-
await next();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!user.isActive) {
|
|
108
|
-
this.logger?.warn?.('Account is not active:', user.sub);
|
|
109
|
-
await next();
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Optimistic locking check - ensure session wasn't modified during request
|
|
114
|
-
const revalidated = await this.sessionService.findByIdLight(sessionId);
|
|
115
|
-
if (!revalidated || revalidated.version !== initialVersion || revalidated.isRevoked) {
|
|
116
|
-
this.logger?.error?.('Session was modified during request - possible security breach');
|
|
117
|
-
await next();
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Attach to request attributes
|
|
122
|
-
req.attributes.user = user;
|
|
123
|
-
req.attributes.token = validation.payload;
|
|
124
|
-
|
|
125
|
-
// Store in ContextStorage for service access
|
|
126
|
-
ContextStorage.set('CURRENT_USER', user);
|
|
127
|
-
ContextStorage.set('JWT_PAYLOAD', validation.payload);
|
|
128
|
-
ContextStorage.set('CURRENT_SESSION', sessionId);
|
|
129
|
-
|
|
130
|
-
this.logger?.debug?.(`User ${user.sub} authenticated successfully`);
|
|
131
|
-
|
|
132
|
-
// Update CLIENT_INFO with sessionId and userId
|
|
133
|
-
this.updateClientInfoSessionId(sessionId);
|
|
134
|
-
this.updateClientInfoUserId(userId);
|
|
135
|
-
|
|
136
|
-
await next();
|
|
137
|
-
} catch (error) {
|
|
138
|
-
this.logger?.error?.(
|
|
139
|
-
'Error in auth handler:',
|
|
140
|
-
error instanceof Error ? error.message : String(error),
|
|
141
|
-
error instanceof Error ? error.stack : undefined,
|
|
142
|
-
);
|
|
143
|
-
await next();
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Extract token from request based on delivery mode
|
|
149
|
-
*/
|
|
150
|
-
private extractToken(req: NAuthRequest): string | null {
|
|
151
|
-
const method = this.config.tokenDelivery?.method || 'json';
|
|
152
|
-
|
|
153
|
-
// Get token from header
|
|
154
|
-
const authHeader = req.getHeader('authorization');
|
|
155
|
-
const headerToken = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
|
156
|
-
|
|
157
|
-
// Get token from cookie
|
|
158
|
-
const accessTokenCookieName = getAccessTokenCookieName(this.config);
|
|
159
|
-
const cookieToken = req.cookies[accessTokenCookieName];
|
|
160
|
-
|
|
161
|
-
// Check for route-level override
|
|
162
|
-
const routeMode = req.attributes.nauthTokenDelivery;
|
|
163
|
-
|
|
164
|
-
let effective: 'cookies' | 'json' = 'json';
|
|
165
|
-
|
|
166
|
-
if (routeMode) {
|
|
167
|
-
effective = routeMode;
|
|
168
|
-
} else if (method === 'hybrid') {
|
|
169
|
-
// Determine mode based on request characteristics
|
|
170
|
-
effective = resolveDeliveryForRequest(req.raw, this.config.tokenDelivery?.hybridPolicy);
|
|
171
|
-
} else {
|
|
172
|
-
effective = method === 'cookies' ? 'cookies' : 'json';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (effective === 'cookies') {
|
|
176
|
-
// Cookie mode: Reject if Bearer header present
|
|
177
|
-
if (headerToken && !cookieToken) {
|
|
178
|
-
throw new NAuthException(
|
|
179
|
-
AuthErrorCode.BEARER_NOT_ALLOWED,
|
|
180
|
-
'Bearer tokens are not allowed in cookie-only path.',
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
return cookieToken || null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// JSON mode: Reject if cookie present
|
|
187
|
-
if (cookieToken && !headerToken) {
|
|
188
|
-
throw new NAuthException(AuthErrorCode.COOKIES_NOT_ALLOWED, 'Cookie tokens are not allowed in JSON-only path.');
|
|
189
|
-
}
|
|
190
|
-
return headerToken || null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Update CLIENT_INFO with session ID from token
|
|
195
|
-
*/
|
|
196
|
-
private updateClientInfoSessionId(sessionId: string | number): void {
|
|
197
|
-
const clientInfo = ContextStorage.get<IClientInfo>('CLIENT_INFO');
|
|
198
|
-
if (clientInfo) {
|
|
199
|
-
const sessionIdNumber = typeof sessionId === 'number' ? sessionId : parseInt(String(sessionId), 10);
|
|
200
|
-
|
|
201
|
-
if (!isNaN(sessionIdNumber) && sessionIdNumber > 0) {
|
|
202
|
-
clientInfo.sessionId = sessionIdNumber;
|
|
203
|
-
ContextStorage.set('CLIENT_INFO', clientInfo);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Update CLIENT_INFO with user ID from token
|
|
210
|
-
*/
|
|
211
|
-
private updateClientInfoUserId(userId: string | number): void {
|
|
212
|
-
const clientInfo = ContextStorage.get<IClientInfo>('CLIENT_INFO');
|
|
213
|
-
if (clientInfo) {
|
|
214
|
-
const userIdNumber = typeof userId === 'number' ? userId : parseInt(String(userId), 10);
|
|
215
|
-
|
|
216
|
-
if (!isNaN(userIdNumber) && userIdNumber > 0) {
|
|
217
|
-
clientInfo.userId = userIdNumber;
|
|
218
|
-
ContextStorage.set('CLIENT_INFO', clientInfo);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Get fields to select when loading user
|
|
225
|
-
*/
|
|
226
|
-
private getUserSelectFields(): (keyof BaseUser)[] {
|
|
227
|
-
return [
|
|
228
|
-
'id',
|
|
229
|
-
'sub',
|
|
230
|
-
'username',
|
|
231
|
-
'firstName',
|
|
232
|
-
'lastName',
|
|
233
|
-
'email',
|
|
234
|
-
'phone',
|
|
235
|
-
'isEmailVerified',
|
|
236
|
-
'isPhoneVerified',
|
|
237
|
-
'isActive',
|
|
238
|
-
'mustChangePassword',
|
|
239
|
-
'isLocked',
|
|
240
|
-
'lockReason',
|
|
241
|
-
'lockedAt',
|
|
242
|
-
'lockedUntil',
|
|
243
|
-
'failedLoginAttempts',
|
|
244
|
-
'lastFailedLoginAt',
|
|
245
|
-
'lastLoginAt',
|
|
246
|
-
'lastLoginIp',
|
|
247
|
-
'hasSocialAuth',
|
|
248
|
-
'socialProviders',
|
|
249
|
-
'mfaEnabled',
|
|
250
|
-
'mfaMethods',
|
|
251
|
-
'preferredMfaMethod',
|
|
252
|
-
'mfaExempt',
|
|
253
|
-
'mfaExemptReason',
|
|
254
|
-
'mfaExemptGrantedAt',
|
|
255
|
-
'metadata',
|
|
256
|
-
'createdAt',
|
|
257
|
-
'updatedAt',
|
|
258
|
-
] as (keyof BaseUser)[];
|
|
259
|
-
}
|
|
260
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client Info Handler
|
|
3
|
-
*
|
|
4
|
-
* Extracts client information (IP, user agent, device info) from NAuthRequest
|
|
5
|
-
* and stores in AsyncLocalStorage context.
|
|
6
|
-
*
|
|
7
|
-
* **Platform-Agnostic:**
|
|
8
|
-
* This handler operates purely on NAuthRequest interface.
|
|
9
|
-
* Context initialization is handled by the adapter, not this handler.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { ContextStorage, ClientInfoService, IClientInfo, NAuthLogger, getDeviceTokenCookieName } from '../index';
|
|
13
|
-
import { GeoLocationService } from '../internal';
|
|
14
|
-
import { NAuthRequest, NAuthResponse } from '../platform/interfaces';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* ClientInfoHandler
|
|
18
|
-
*
|
|
19
|
-
* First handler in the chain. Extracts client information and stores it
|
|
20
|
-
* in the context for downstream handlers and services.
|
|
21
|
-
*/
|
|
22
|
-
export class ClientInfoHandler {
|
|
23
|
-
constructor(
|
|
24
|
-
private clientInfoService: ClientInfoService,
|
|
25
|
-
private geoLocationService?: GeoLocationService,
|
|
26
|
-
private logger?: NAuthLogger,
|
|
27
|
-
) {}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Handle request - extract and store client info
|
|
31
|
-
*
|
|
32
|
-
* Context initialization is handled by the adapter.
|
|
33
|
-
* This handler assumes context is already available.
|
|
34
|
-
*/
|
|
35
|
-
public async handle(req: NAuthRequest, res: NAuthResponse, next: () => Promise<void> | void): Promise<void> {
|
|
36
|
-
try {
|
|
37
|
-
await this.extractAndStore(req, res);
|
|
38
|
-
} catch (error) {
|
|
39
|
-
this.logger?.error?.('Error extracting client info:', error);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
await next();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Extract client information and store in context
|
|
47
|
-
*/
|
|
48
|
-
private async extractAndStore(req: NAuthRequest, res: NAuthResponse): Promise<void> {
|
|
49
|
-
// Extract user agent
|
|
50
|
-
const userAgent = req.getHeader('user-agent') || 'unknown';
|
|
51
|
-
|
|
52
|
-
// Parse user agent for device/browser info
|
|
53
|
-
const parsedUA = this.clientInfoService.parseUserAgent(userAgent);
|
|
54
|
-
|
|
55
|
-
// Extract device token from cookie or header
|
|
56
|
-
// Use default cookie name (nauth_device_token) if config not available
|
|
57
|
-
const deviceTokenCookieName = getDeviceTokenCookieName();
|
|
58
|
-
const deviceToken = req.cookies[deviceTokenCookieName] || req.getHeader('x-device-token');
|
|
59
|
-
|
|
60
|
-
// Build client info object
|
|
61
|
-
const clientInfo: IClientInfo = {
|
|
62
|
-
ipAddress: req.ip,
|
|
63
|
-
userAgent,
|
|
64
|
-
deviceToken,
|
|
65
|
-
deviceName: (req.body.deviceName as string) || parsedUA.deviceName || undefined,
|
|
66
|
-
deviceType: ((req.body.deviceType as string) || parsedUA.deviceType || undefined) as IClientInfo['deviceType'],
|
|
67
|
-
platform: parsedUA.platform || undefined,
|
|
68
|
-
browser: parsedUA.browser || undefined,
|
|
69
|
-
sessionId: undefined, // Set later by AuthHandler
|
|
70
|
-
userId: undefined, // Set later by AuthHandler
|
|
71
|
-
ipCountry: undefined,
|
|
72
|
-
ipCity: undefined,
|
|
73
|
-
ipLatitude: undefined,
|
|
74
|
-
ipLongitude: undefined,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Populate geolocation if service available
|
|
78
|
-
if (this.geoLocationService && clientInfo.ipAddress && clientInfo.ipAddress !== '0.0.0.0') {
|
|
79
|
-
try {
|
|
80
|
-
const geo = await this.geoLocationService.getIpGeolocation(clientInfo.ipAddress);
|
|
81
|
-
clientInfo.ipCountry = geo.country;
|
|
82
|
-
clientInfo.ipCity = geo.city;
|
|
83
|
-
clientInfo.ipLatitude = geo.latitude;
|
|
84
|
-
clientInfo.ipLongitude = geo.longitude;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
// Log error instead of silently failing
|
|
87
|
-
this.logger?.error?.(
|
|
88
|
-
`Geolocation lookup failed for IP ${clientInfo.ipAddress}:`,
|
|
89
|
-
error instanceof Error ? error.message : 'Unknown error',
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Store in context
|
|
95
|
-
ContextStorage.set('CLIENT_INFO', clientInfo);
|
|
96
|
-
ContextStorage.set('HTTP_RESPONSE', res.raw);
|
|
97
|
-
|
|
98
|
-
// Also attach to request attributes for handler access
|
|
99
|
-
req.attributes.clientInfo = clientInfo;
|
|
100
|
-
}
|
|
101
|
-
}
|