@nauth-toolkit/core 0.1.0 → 0.1.3
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 +30 -0
- package/package.json +7 -2
- 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,558 +0,0 @@
|
|
|
1
|
-
import { Repository } from 'typeorm';
|
|
2
|
-
import { BaseAuthAudit, BaseUser } from '../entities';
|
|
3
|
-
import { IAuthAudit, IUser } from '../interfaces/entities.interface';
|
|
4
|
-
import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
|
|
5
|
-
import { AuthAuditEventStatus } from '../entities/auth-audit.entity';
|
|
6
|
-
import { NAuthLogger } from '../utils/nauth-logger';
|
|
7
|
-
import { NAuthException } from '../exceptions/nauth.exception';
|
|
8
|
-
import { AuthErrorCode } from '../enums/error-codes.enum';
|
|
9
|
-
import { ClientInfoService } from './client-info.service';
|
|
10
|
-
import { RiskFactor } from '../enums/risk-factor.enum';
|
|
11
|
-
import { GetUserAuthHistoryDTO, GetUserAuthHistoryResponseDTO } from '../dto/get-user-auth-history.dto';
|
|
12
|
-
import { GetEventsByTypeDTO, GetEventsByTypeResponseDTO } from '../dto/get-events-by-type.dto';
|
|
13
|
-
import { GetSuspiciousActivityDTO, GetSuspiciousActivityResponseDTO } from '../dto/get-suspicious-activity.dto';
|
|
14
|
-
import {
|
|
15
|
-
GetRiskAssessmentHistoryDTO,
|
|
16
|
-
GetRiskAssessmentHistoryResponseDTO,
|
|
17
|
-
} from '../dto/get-risk-assessment-history.dto';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* DTO for creating audit events
|
|
21
|
-
*
|
|
22
|
-
* @internal
|
|
23
|
-
* This DTO is only used by InternalAuthAuditService and should not be exposed
|
|
24
|
-
* to consumer applications.
|
|
25
|
-
*/
|
|
26
|
-
export interface CreateAuthAuditEventDTO {
|
|
27
|
-
userId?: number; // Internal user ID (preferred)
|
|
28
|
-
userSub?: string; // External user identifier (will lookup userId)
|
|
29
|
-
eventType: AuthAuditEventType;
|
|
30
|
-
eventStatus: AuthAuditEventStatus;
|
|
31
|
-
riskFactor?: number | null;
|
|
32
|
-
riskFactors?: RiskFactor[] | null;
|
|
33
|
-
adaptiveMfaTriggered?: boolean | null;
|
|
34
|
-
// Note: ipAddress, ipCountry, ipCity, userAgent, platform, browser, deviceId, deviceName, deviceType
|
|
35
|
-
// are automatically captured from ClientInfoService and cannot be overridden
|
|
36
|
-
// Use metadata for event-specific data only
|
|
37
|
-
deviceId?: string | null; // Special case: can override for newly created device tokens
|
|
38
|
-
sessionId?: number | null;
|
|
39
|
-
challengeSessionId?: number | null;
|
|
40
|
-
authMethod?: string | null;
|
|
41
|
-
performedBy?: string | null; // Auto-populated from session context if not provided (userId of authenticated user)
|
|
42
|
-
reason?: string | null;
|
|
43
|
-
description?: string | null;
|
|
44
|
-
metadata?: Record<string, unknown> | null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Authentication Audit Service (Base Class - Public API)
|
|
49
|
-
*
|
|
50
|
-
* Manages audit trail queries for authentication and security events.
|
|
51
|
-
* Provides query capabilities for retrieving audit history.
|
|
52
|
-
*
|
|
53
|
-
* **Key Features:**
|
|
54
|
-
* - Efficient queries using userId (internal integer ID)
|
|
55
|
-
* - Pagination support for large datasets
|
|
56
|
-
* - Query filtering by event type, status, date ranges
|
|
57
|
-
* - User history queries (resolves userSub to userId automatically)
|
|
58
|
-
*
|
|
59
|
-
* **Design Notes:**
|
|
60
|
-
* - Only stores `userId` (integer) - no userSub duplication
|
|
61
|
-
* - All methods accepting userSub resolve to userId before querying
|
|
62
|
-
* - Risk tracking fields are infrastructure for future adaptive MFA (no business logic)
|
|
63
|
-
*
|
|
64
|
-
* **Note:** This is the public API class. Event recording is handled internally
|
|
65
|
-
* by `InternalAuthAuditService` and is not exposed to consumer applications.
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```typescript
|
|
69
|
-
* // Get user history (accepts userSub, resolves to userId)
|
|
70
|
-
* const history = await auditService.getUserAuthHistory({
|
|
71
|
-
* userSub: 'user-uuid',
|
|
72
|
-
* page: 1,
|
|
73
|
-
* limit: 50,
|
|
74
|
-
* startDate: new Date('2025-01-01'),
|
|
75
|
-
* });
|
|
76
|
-
* ```
|
|
77
|
-
*/
|
|
78
|
-
export class AuthAuditService {
|
|
79
|
-
constructor(
|
|
80
|
-
protected readonly auditRepository: Repository<BaseAuthAudit>,
|
|
81
|
-
protected readonly userRepository: Repository<BaseUser>,
|
|
82
|
-
protected readonly logger: NAuthLogger,
|
|
83
|
-
protected readonly clientInfoService?: ClientInfoService,
|
|
84
|
-
) {}
|
|
85
|
-
|
|
86
|
-
// ============================================================================
|
|
87
|
-
// Query Methods
|
|
88
|
-
// ============================================================================
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Get paginated authentication history for a user
|
|
92
|
-
*
|
|
93
|
-
* Accepts userSub (external identifier) and resolves to userId for efficient queries.
|
|
94
|
-
* Supports filtering by event types, status, and date ranges.
|
|
95
|
-
*
|
|
96
|
-
* @param request - Request DTO containing userSub and filtering options
|
|
97
|
-
* @returns Response DTO with paginated audit records
|
|
98
|
-
* @throws {NAuthException} If user not found
|
|
99
|
-
*
|
|
100
|
-
* @example
|
|
101
|
-
* ```typescript
|
|
102
|
-
* const history = await auditService.getUserAuthHistory({
|
|
103
|
-
* userSub: 'user-uuid',
|
|
104
|
-
* page: 1,
|
|
105
|
-
* limit: 50,
|
|
106
|
-
* eventTypes: [AuthAuditEventType.LOGIN_SUCCESS, AuthAuditEventType.LOGIN_FAILED],
|
|
107
|
-
* startDate: new Date('2025-01-01'),
|
|
108
|
-
* });
|
|
109
|
-
* ```
|
|
110
|
-
*/
|
|
111
|
-
async getUserAuthHistory(request: GetUserAuthHistoryDTO): Promise<GetUserAuthHistoryResponseDTO> {
|
|
112
|
-
// Resolve userSub to userId
|
|
113
|
-
const user = (await this.userRepository.findOne({ where: { sub: request.userSub } })) as IUser | null;
|
|
114
|
-
if (!user) {
|
|
115
|
-
throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const page = request.page || 1;
|
|
119
|
-
const limit = request.limit || 50;
|
|
120
|
-
const skip = (page - 1) * limit;
|
|
121
|
-
|
|
122
|
-
// Build query
|
|
123
|
-
const queryBuilder = this.auditRepository
|
|
124
|
-
.createQueryBuilder('audit')
|
|
125
|
-
.where('audit.userId = :userId', { userId: user.id });
|
|
126
|
-
|
|
127
|
-
// Date range filter
|
|
128
|
-
if (request.startDate) {
|
|
129
|
-
queryBuilder.andWhere('audit.createdAt >= :startDate', { startDate: request.startDate });
|
|
130
|
-
}
|
|
131
|
-
if (request.endDate) {
|
|
132
|
-
queryBuilder.andWhere('audit.createdAt <= :endDate', { endDate: request.endDate });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Event type filter
|
|
136
|
-
if (request.eventTypes && request.eventTypes.length > 0) {
|
|
137
|
-
queryBuilder.andWhere('audit.eventType IN (:...eventTypes)', { eventTypes: request.eventTypes });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Event status filter
|
|
141
|
-
if (request.eventStatus && request.eventStatus.length > 0) {
|
|
142
|
-
queryBuilder.andWhere('audit.eventStatus IN (:...eventStatus)', { eventStatus: request.eventStatus });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Order by date (newest first)
|
|
146
|
-
queryBuilder.orderBy('audit.createdAt', 'DESC');
|
|
147
|
-
|
|
148
|
-
// Pagination
|
|
149
|
-
queryBuilder.skip(skip).take(limit);
|
|
150
|
-
|
|
151
|
-
const [data, total] = await queryBuilder.getManyAndCount();
|
|
152
|
-
|
|
153
|
-
const response = new GetUserAuthHistoryResponseDTO();
|
|
154
|
-
response.data = data as unknown as IAuthAudit[];
|
|
155
|
-
response.total = total;
|
|
156
|
-
response.page = page;
|
|
157
|
-
response.limit = limit;
|
|
158
|
-
response.totalPages = Math.ceil(total / limit);
|
|
159
|
-
|
|
160
|
-
return response;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Get events by type with pagination
|
|
165
|
-
*
|
|
166
|
-
* @param request - Request DTO containing eventType and pagination options
|
|
167
|
-
* @returns Response DTO with paginated audit records
|
|
168
|
-
*
|
|
169
|
-
* @example
|
|
170
|
-
* ```typescript
|
|
171
|
-
* const events = await auditService.getEventsByType({
|
|
172
|
-
* eventType: AuthAuditEventType.SUSPICIOUS_ACTIVITY,
|
|
173
|
-
* page: 1,
|
|
174
|
-
* limit: 100,
|
|
175
|
-
* });
|
|
176
|
-
* ```
|
|
177
|
-
*/
|
|
178
|
-
async getEventsByType(request: GetEventsByTypeDTO): Promise<GetEventsByTypeResponseDTO> {
|
|
179
|
-
const page = request.page || 1;
|
|
180
|
-
const limit = request.limit || 50;
|
|
181
|
-
const skip = (page - 1) * limit;
|
|
182
|
-
|
|
183
|
-
const queryBuilder = this.auditRepository.createQueryBuilder('audit').where('audit.eventType = :eventType', {
|
|
184
|
-
eventType: request.eventType,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Date range filter
|
|
188
|
-
if (request.startDate) {
|
|
189
|
-
queryBuilder.andWhere('audit.createdAt >= :startDate', { startDate: request.startDate });
|
|
190
|
-
}
|
|
191
|
-
if (request.endDate) {
|
|
192
|
-
queryBuilder.andWhere('audit.createdAt <= :endDate', { endDate: request.endDate });
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
queryBuilder.orderBy('audit.createdAt', 'DESC').skip(skip).take(limit);
|
|
196
|
-
|
|
197
|
-
const [data, total] = await queryBuilder.getManyAndCount();
|
|
198
|
-
|
|
199
|
-
const response = new GetEventsByTypeResponseDTO();
|
|
200
|
-
response.data = data as unknown as IAuthAudit[];
|
|
201
|
-
response.total = total;
|
|
202
|
-
response.page = page;
|
|
203
|
-
response.limit = limit;
|
|
204
|
-
response.totalPages = Math.ceil(total / limit);
|
|
205
|
-
|
|
206
|
-
return response;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Get suspicious activity events
|
|
211
|
-
*
|
|
212
|
-
* Returns events with SUSPICIOUS status or SUSPICIOUS_ACTIVITY event type.
|
|
213
|
-
*
|
|
214
|
-
* @param request - Request DTO containing optional userSub and limit
|
|
215
|
-
* @returns Response DTO with array of suspicious audit events
|
|
216
|
-
*
|
|
217
|
-
* @example
|
|
218
|
-
* ```typescript
|
|
219
|
-
* // Get all suspicious activity
|
|
220
|
-
* const suspicious = await auditService.getSuspiciousActivity({});
|
|
221
|
-
*
|
|
222
|
-
* // Get suspicious activity for specific user
|
|
223
|
-
* const userSuspicious = await auditService.getSuspiciousActivity({
|
|
224
|
-
* userSub: 'user-uuid',
|
|
225
|
-
* limit: 50,
|
|
226
|
-
* });
|
|
227
|
-
* ```
|
|
228
|
-
*/
|
|
229
|
-
async getSuspiciousActivity(request: GetSuspiciousActivityDTO): Promise<GetSuspiciousActivityResponseDTO> {
|
|
230
|
-
const limit = request.limit || 100;
|
|
231
|
-
|
|
232
|
-
const queryBuilder = this.auditRepository
|
|
233
|
-
.createQueryBuilder('audit')
|
|
234
|
-
.where('(audit.eventStatus = :status OR audit.eventType = :eventType)', {
|
|
235
|
-
status: 'SUSPICIOUS',
|
|
236
|
-
eventType: AuthAuditEventType.SUSPICIOUS_ACTIVITY,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Filter by user if provided
|
|
240
|
-
if (request.userSub) {
|
|
241
|
-
const user = (await this.userRepository.findOne({ where: { sub: request.userSub } })) as IUser | null;
|
|
242
|
-
if (!user) {
|
|
243
|
-
throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
|
|
244
|
-
}
|
|
245
|
-
queryBuilder.andWhere('audit.userId = :userId', { userId: user.id });
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
queryBuilder.orderBy('audit.createdAt', 'DESC').take(limit);
|
|
249
|
-
|
|
250
|
-
const data = await queryBuilder.getMany();
|
|
251
|
-
|
|
252
|
-
const response = new GetSuspiciousActivityResponseDTO();
|
|
253
|
-
response.data = data as unknown as IAuthAudit[];
|
|
254
|
-
|
|
255
|
-
return response;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Get risk assessment history for adaptive MFA analysis
|
|
260
|
-
*
|
|
261
|
-
* Returns events where risk assessment was performed (ADAPTIVE_MFA_RISK_ASSESSED,
|
|
262
|
-
* ADAPTIVE_MFA_TRIGGERED, ADAPTIVE_MFA_BYPASSED).
|
|
263
|
-
*
|
|
264
|
-
* @param request - Request DTO containing userSub and limit
|
|
265
|
-
* @returns Response DTO with array of risk assessment audit events
|
|
266
|
-
* @throws {NAuthException} If user not found
|
|
267
|
-
*
|
|
268
|
-
* @example
|
|
269
|
-
* ```typescript
|
|
270
|
-
* const riskHistory = await auditService.getRiskAssessmentHistory({
|
|
271
|
-
* userSub: 'user-uuid',
|
|
272
|
-
* limit: 50,
|
|
273
|
-
* });
|
|
274
|
-
* ```
|
|
275
|
-
*/
|
|
276
|
-
async getRiskAssessmentHistory(request: GetRiskAssessmentHistoryDTO): Promise<GetRiskAssessmentHistoryResponseDTO> {
|
|
277
|
-
const limit = request.limit || 100;
|
|
278
|
-
|
|
279
|
-
// Resolve userSub to userId
|
|
280
|
-
const user = (await this.userRepository.findOne({ where: { sub: request.userSub } })) as IUser | null;
|
|
281
|
-
if (!user) {
|
|
282
|
-
throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const queryBuilder = this.auditRepository
|
|
286
|
-
.createQueryBuilder('audit')
|
|
287
|
-
.where('audit.userId = :userId', { userId: user.id })
|
|
288
|
-
.andWhere('audit.eventType IN (:...eventTypes)', {
|
|
289
|
-
eventTypes: [
|
|
290
|
-
AuthAuditEventType.ADAPTIVE_MFA_RISK_ASSESSED,
|
|
291
|
-
AuthAuditEventType.ADAPTIVE_MFA_TRIGGERED,
|
|
292
|
-
AuthAuditEventType.ADAPTIVE_MFA_BYPASSED,
|
|
293
|
-
],
|
|
294
|
-
})
|
|
295
|
-
.orderBy('audit.createdAt', 'DESC')
|
|
296
|
-
.take(limit);
|
|
297
|
-
|
|
298
|
-
const data = await queryBuilder.getMany();
|
|
299
|
-
|
|
300
|
-
const response = new GetRiskAssessmentHistoryResponseDTO();
|
|
301
|
-
response.data = data as unknown as IAuthAudit[];
|
|
302
|
-
|
|
303
|
-
return response;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// ============================================================================
|
|
308
|
-
// Internal Service (Framework Adapters Only)
|
|
309
|
-
// ============================================================================
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Internal Authentication Audit Service
|
|
313
|
-
*
|
|
314
|
-
* Extends the base AuthAuditService with event recording capabilities.
|
|
315
|
-
* This service is only available via `@nauth-toolkit/core/internal` and should
|
|
316
|
-
* NOT be used by consumer applications.
|
|
317
|
-
*
|
|
318
|
-
* **Event Recording:**
|
|
319
|
-
* The `recordEvent()` method is internal-only and is used by nauth-toolkit
|
|
320
|
-
* services to log authentication events. Consumer applications should use
|
|
321
|
-
* the query methods from the base `AuthAuditService` class.
|
|
322
|
-
*
|
|
323
|
-
* @internal
|
|
324
|
-
* This class is only exported from `@nauth-toolkit/core/internal` for use
|
|
325
|
-
* by framework adapters. Consumer applications should use the base
|
|
326
|
-
* `AuthAuditService` from `@nauth-toolkit/core`.
|
|
327
|
-
*
|
|
328
|
-
* @example
|
|
329
|
-
* ```typescript
|
|
330
|
-
* // Framework adapter usage
|
|
331
|
-
* import { AuthAuditService } from '@nauth-toolkit/core/internal';
|
|
332
|
-
*
|
|
333
|
-
* const auditService = new AuthAuditService(...);
|
|
334
|
-
* // Can use recordEvent() here (internal only)
|
|
335
|
-
* await auditService.recordEvent({ ... });
|
|
336
|
-
* ```
|
|
337
|
-
*/
|
|
338
|
-
export class InternalAuthAuditService extends AuthAuditService {
|
|
339
|
-
/**
|
|
340
|
-
* Record an authentication audit event
|
|
341
|
-
*
|
|
342
|
-
* Creates an audit record for an authentication or security event.
|
|
343
|
-
* Automatically extracts client information from request context when available.
|
|
344
|
-
* This method is non-blocking - errors are logged but don't throw exceptions.
|
|
345
|
-
*
|
|
346
|
-
* **Automatic Client Info Extraction:**
|
|
347
|
-
* When ClientInfoService is available, the following fields are automatically populated:
|
|
348
|
-
* - ipAddress, ipCountry, ipCity (from request and geolocation)
|
|
349
|
-
* - userAgent, platform, browser (from user agent parsing)
|
|
350
|
-
* - deviceId, deviceName, deviceType (from request context)
|
|
351
|
-
*
|
|
352
|
-
* Note: These fields cannot be overridden via the DTO - they are always captured from the request context.
|
|
353
|
-
* Only deviceId can be explicitly set for special cases (e.g., newly created device tokens).
|
|
354
|
-
* Do not include ipAddress, ipCountry, ipCity, userAgent, platform, browser, deviceName, or deviceType
|
|
355
|
-
* in the DTO - they will be automatically captured and attempts to include them will cause TypeScript errors.
|
|
356
|
-
*
|
|
357
|
-
* **Automatic performedBy Population:**
|
|
358
|
-
* The `performedBy` field is automatically populated from the authenticated user's context:
|
|
359
|
-
* - If userId is available from ClientInfoService (extracted from JWT token by interceptors/handlers), it is used as `performedBy`
|
|
360
|
-
* - This captures who performed the action (e.g., admin performing action on another user)
|
|
361
|
-
* - If no userId is found in client info, `performedBy` defaults to the event's `userId` (user performing action on themselves)
|
|
362
|
-
* - Explicit `performedBy` in DTO overrides automatic population
|
|
363
|
-
*
|
|
364
|
-
* @internal
|
|
365
|
-
* This method is only available in InternalAuthAuditService and should not
|
|
366
|
-
* be exposed to consumer applications.
|
|
367
|
-
*
|
|
368
|
-
* @param data - Audit event data (only event-specific fields needed)
|
|
369
|
-
* @param data.userId - Internal user ID (preferred, more efficient)
|
|
370
|
-
* @param data.userSub - External user identifier (will lookup userId if userId not provided)
|
|
371
|
-
* @param data.eventType - Type of event
|
|
372
|
-
* @param data.eventStatus - Event classification status
|
|
373
|
-
* @returns Created audit record
|
|
374
|
-
*
|
|
375
|
-
* @example
|
|
376
|
-
* ```typescript
|
|
377
|
-
* // Simple recording - client info auto-populated
|
|
378
|
-
* await auditService.recordEvent({
|
|
379
|
-
* userId: user.id,
|
|
380
|
-
* eventType: AuthAuditEventType.LOGIN_SUCCESS,
|
|
381
|
-
* eventStatus: 'SUCCESS',
|
|
382
|
-
* authMethod: 'password',
|
|
383
|
-
* // ipAddress, userAgent, deviceName, etc. automatically included!
|
|
384
|
-
* });
|
|
385
|
-
*
|
|
386
|
-
* // Override specific fields if needed
|
|
387
|
-
* await auditService.recordEvent({
|
|
388
|
-
* userId: user.id,
|
|
389
|
-
* eventType: AuthAuditEventType.LOGIN_SUCCESS,
|
|
390
|
-
* eventStatus: 'SUCCESS',
|
|
391
|
-
* // ipAddress, userAgent, etc. automatically captured from request context
|
|
392
|
-
* });
|
|
393
|
-
* ```
|
|
394
|
-
*/
|
|
395
|
-
async recordEvent(data: CreateAuthAuditEventDTO): Promise<IAuthAudit | null> {
|
|
396
|
-
try {
|
|
397
|
-
// Resolve userId if userSub provided
|
|
398
|
-
let userId = data.userId;
|
|
399
|
-
if (!userId && data.userSub) {
|
|
400
|
-
const user = (await this.userRepository.findOne({ where: { sub: data.userSub } })) as IUser | null;
|
|
401
|
-
if (!user) {
|
|
402
|
-
this.logger?.warn?.(`Cannot record audit event - user not found: ${data.userSub}`);
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
userId = user.id;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (!userId) {
|
|
409
|
-
this.logger?.warn?.('Cannot record audit event - userId or userSub required');
|
|
410
|
-
return null;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// ============================================================================
|
|
414
|
-
// Auto-extract client info from context (when available)
|
|
415
|
-
// Note: These fields are automatically captured and cannot be overridden by callers
|
|
416
|
-
// ============================================================================
|
|
417
|
-
let clientInfo: {
|
|
418
|
-
ipAddress?: string | null;
|
|
419
|
-
ipCountry?: string | null;
|
|
420
|
-
ipCity?: string | null;
|
|
421
|
-
ipLatitude?: number | null;
|
|
422
|
-
ipLongitude?: number | null;
|
|
423
|
-
userAgent?: string | null;
|
|
424
|
-
platform?: string | null;
|
|
425
|
-
browser?: string | null;
|
|
426
|
-
deviceId?: string | null;
|
|
427
|
-
deviceName?: string | null;
|
|
428
|
-
deviceType?: string | null;
|
|
429
|
-
sessionId?: number | null;
|
|
430
|
-
} = {};
|
|
431
|
-
|
|
432
|
-
if (this.clientInfoService) {
|
|
433
|
-
try {
|
|
434
|
-
const clientInfoFromContext = this.clientInfoService.get();
|
|
435
|
-
|
|
436
|
-
// Debug logging
|
|
437
|
-
if (!clientInfoFromContext.ipLatitude || !clientInfoFromContext.ipLongitude) {
|
|
438
|
-
this.logger?.warn?.(
|
|
439
|
-
`[AuthAuditService] Creating audit WITHOUT coordinates from context: ` +
|
|
440
|
-
`IP=${clientInfoFromContext.ipAddress}, country=${clientInfoFromContext.ipCountry}, ` +
|
|
441
|
-
`city=${clientInfoFromContext.ipCity}, lat=${clientInfoFromContext.ipLatitude}, ` +
|
|
442
|
-
`lon=${clientInfoFromContext.ipLongitude}`,
|
|
443
|
-
);
|
|
444
|
-
} else {
|
|
445
|
-
this.logger?.debug?.(
|
|
446
|
-
`[AuthAuditService] Creating audit WITH coordinates from context: ` +
|
|
447
|
-
`IP=${clientInfoFromContext.ipAddress}, ${clientInfoFromContext.ipCity}, ` +
|
|
448
|
-
`${clientInfoFromContext.ipCountry} (${clientInfoFromContext.ipLatitude}, ${clientInfoFromContext.ipLongitude})`,
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Automatically capture from context (no override allowed)
|
|
453
|
-
clientInfo = {
|
|
454
|
-
ipAddress: clientInfoFromContext.ipAddress || null,
|
|
455
|
-
ipCountry: clientInfoFromContext.ipCountry || null,
|
|
456
|
-
ipCity: clientInfoFromContext.ipCity || null,
|
|
457
|
-
ipLatitude: clientInfoFromContext.ipLatitude || null,
|
|
458
|
-
ipLongitude: clientInfoFromContext.ipLongitude || null,
|
|
459
|
-
userAgent: clientInfoFromContext.userAgent || null,
|
|
460
|
-
platform: clientInfoFromContext.platform || null,
|
|
461
|
-
browser: clientInfoFromContext.browser || null,
|
|
462
|
-
deviceId: clientInfoFromContext.deviceToken || null,
|
|
463
|
-
deviceName: clientInfoFromContext.deviceName || null,
|
|
464
|
-
deviceType: clientInfoFromContext.deviceType || null,
|
|
465
|
-
sessionId: clientInfoFromContext.sessionId || null,
|
|
466
|
-
};
|
|
467
|
-
} catch (error) {
|
|
468
|
-
// Non-blocking: If client info extraction fails, continue without it
|
|
469
|
-
// This can happen if called outside request context (e.g., cron jobs)
|
|
470
|
-
this.logger?.debug?.(
|
|
471
|
-
`Failed to extract client info for audit: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// ============================================================================
|
|
477
|
-
// Use auto-extracted client info (deviceId can be overridden for special cases)
|
|
478
|
-
// ============================================================================
|
|
479
|
-
const mergedData = {
|
|
480
|
-
ipAddress: clientInfo.ipAddress ?? null,
|
|
481
|
-
ipCountry: clientInfo.ipCountry ?? null,
|
|
482
|
-
ipCity: clientInfo.ipCity ?? null,
|
|
483
|
-
ipLatitude: clientInfo.ipLatitude ?? null,
|
|
484
|
-
ipLongitude: clientInfo.ipLongitude ?? null,
|
|
485
|
-
userAgent: clientInfo.userAgent ?? null,
|
|
486
|
-
platform: clientInfo.platform ?? null,
|
|
487
|
-
browser: clientInfo.browser ?? null,
|
|
488
|
-
deviceId: data.deviceId ?? clientInfo.deviceId ?? null, // Allow override for newly created device tokens
|
|
489
|
-
deviceName: clientInfo.deviceName ?? null,
|
|
490
|
-
deviceType: clientInfo.deviceType ?? null,
|
|
491
|
-
sessionId: data.sessionId ?? clientInfo.sessionId ?? null,
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
// ============================================================================
|
|
495
|
-
// Auto-populate performedBy from client info context (if available)
|
|
496
|
-
// ============================================================================
|
|
497
|
-
let performedBy: string | null = data.performedBy ?? null;
|
|
498
|
-
if (!performedBy && this.clientInfoService) {
|
|
499
|
-
try {
|
|
500
|
-
// Get userId from client info (extracted from JWT token by interceptors/handlers)
|
|
501
|
-
const clientInfo = this.clientInfoService.get();
|
|
502
|
-
if (clientInfo?.userId) {
|
|
503
|
-
// Use the userId from client info as performedBy
|
|
504
|
-
// This captures who performed the action (could be admin performing action on another user)
|
|
505
|
-
performedBy = String(clientInfo.userId);
|
|
506
|
-
}
|
|
507
|
-
} catch (error) {
|
|
508
|
-
// Non-blocking: If client info extraction fails, continue without performedBy
|
|
509
|
-
this.logger?.debug?.(
|
|
510
|
-
`Failed to get userId from client info for performedBy: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// If still no performedBy and we have userId, use it as fallback
|
|
516
|
-
// (most actions are performed by the user themselves)
|
|
517
|
-
if (!performedBy && userId) {
|
|
518
|
-
performedBy = String(userId);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Create audit record
|
|
522
|
-
const auditRecord = this.auditRepository.create({
|
|
523
|
-
userId,
|
|
524
|
-
eventType: data.eventType,
|
|
525
|
-
eventStatus: data.eventStatus,
|
|
526
|
-
riskFactor: data.riskFactor ?? null,
|
|
527
|
-
riskFactors: data.riskFactors ?? null,
|
|
528
|
-
adaptiveMfaTriggered: data.adaptiveMfaTriggered ?? null,
|
|
529
|
-
ipAddress: mergedData.ipAddress,
|
|
530
|
-
ipCountry: mergedData.ipCountry,
|
|
531
|
-
ipCity: mergedData.ipCity,
|
|
532
|
-
ipLatitude: mergedData.ipLatitude,
|
|
533
|
-
ipLongitude: mergedData.ipLongitude,
|
|
534
|
-
userAgent: mergedData.userAgent,
|
|
535
|
-
platform: mergedData.platform,
|
|
536
|
-
browser: mergedData.browser,
|
|
537
|
-
deviceId: mergedData.deviceId,
|
|
538
|
-
deviceName: mergedData.deviceName,
|
|
539
|
-
deviceType: mergedData.deviceType,
|
|
540
|
-
sessionId: mergedData.sessionId,
|
|
541
|
-
challengeSessionId: data.challengeSessionId ?? null,
|
|
542
|
-
authMethod: data.authMethod ?? null,
|
|
543
|
-
performedBy,
|
|
544
|
-
reason: data.reason ?? null,
|
|
545
|
-
description: data.description ?? null,
|
|
546
|
-
metadata: data.metadata ?? null,
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
const saved = await this.auditRepository.save(auditRecord);
|
|
550
|
-
return saved as unknown as IAuthAudit;
|
|
551
|
-
} catch (error) {
|
|
552
|
-
// Non-blocking: Log error but don't throw
|
|
553
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
554
|
-
this.logger?.error?.(`Failed to record audit event: ${errorMessage}`, { eventType: data.eventType, error });
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|