@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,581 +0,0 @@
|
|
|
1
|
-
import * as crypto from 'crypto';
|
|
2
|
-
import { Repository } from 'typeorm';
|
|
3
|
-
import { BaseUser } from '../entities';
|
|
4
|
-
import { IUser } from '../interfaces/entities.interface';
|
|
5
|
-
import { AuthService } from './auth.service';
|
|
6
|
-
import { SocialAuthService } from './social-auth.service';
|
|
7
|
-
import { TrustedDeviceService } from './trusted-device.service';
|
|
8
|
-
import { JwtService } from './jwt.service';
|
|
9
|
-
import { SessionService } from './session.service';
|
|
10
|
-
import { AuthChallengeHelperService } from './auth-challenge-helper.service';
|
|
11
|
-
import { ClientInfoService } from './client-info.service';
|
|
12
|
-
import { PhoneVerificationService } from './phone-verification.service';
|
|
13
|
-
import { InternalAuthAuditService as AuthAuditService } from './auth-audit.service';
|
|
14
|
-
import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
|
|
15
|
-
import { NAuthConfig } from '../interfaces/config.interface';
|
|
16
|
-
import { NAuthLogger } from '../utils/nauth-logger';
|
|
17
|
-
import { AuthResponseDTO } from '../dto';
|
|
18
|
-
import { OAuthUserProfile } from '../interfaces/oauth.interface';
|
|
19
|
-
import { ISocialAuthProviderService } from '../interfaces/social-auth-provider.interface';
|
|
20
|
-
import { NAuthException } from '../exceptions/nauth.exception';
|
|
21
|
-
import { AuthErrorCode } from '../enums/error-codes.enum';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Base Social Auth Provider Service
|
|
25
|
-
*
|
|
26
|
-
* Abstract base class that provides common functionality for all social auth providers.
|
|
27
|
-
* Provider-specific services (Google, Apple, Facebook, GitHub, etc.) should extend this class
|
|
28
|
-
* and implement provider-specific OAuth client logic.
|
|
29
|
-
*
|
|
30
|
-
* This base class handles:
|
|
31
|
-
* - User creation/lookup
|
|
32
|
-
* - Social account linking
|
|
33
|
-
* - JWT token generation
|
|
34
|
-
* - Session management
|
|
35
|
-
* - Challenge system integration
|
|
36
|
-
*
|
|
37
|
-
* **Key Design:**
|
|
38
|
-
* - No hardcoded provider names - works with any provider
|
|
39
|
-
* - Provider config accessed dynamically via `providerName`
|
|
40
|
-
* - Future developers can add new providers without modifying this class
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* ```typescript
|
|
44
|
-
* @Injectable()
|
|
45
|
-
* export class GitHubSocialAuthService extends BaseSocialAuthProviderService {
|
|
46
|
-
* readonly providerName = 'github';
|
|
47
|
-
*
|
|
48
|
-
* constructor(
|
|
49
|
-
* // ... dependencies
|
|
50
|
-
* private readonly githubOAuthClient: GitHubOAuthClient,
|
|
51
|
-
* ) {
|
|
52
|
-
* super(/* ... base dependencies *\/);
|
|
53
|
-
* }
|
|
54
|
-
*
|
|
55
|
-
* protected async getOAuthProfile(code: string, state: string): Promise<OAuthUserProfile> {
|
|
56
|
-
* // Provider-specific implementation
|
|
57
|
-
* }
|
|
58
|
-
* }
|
|
59
|
-
* ```
|
|
60
|
-
*/
|
|
61
|
-
export abstract class BaseSocialAuthProviderService implements ISocialAuthProviderService {
|
|
62
|
-
abstract readonly providerName: string;
|
|
63
|
-
|
|
64
|
-
constructor(
|
|
65
|
-
protected readonly config: NAuthConfig,
|
|
66
|
-
protected readonly logger: NAuthLogger,
|
|
67
|
-
protected readonly authService: AuthService,
|
|
68
|
-
protected readonly socialAuthService: SocialAuthService,
|
|
69
|
-
protected readonly jwtService: JwtService,
|
|
70
|
-
protected readonly sessionService: SessionService,
|
|
71
|
-
protected readonly challengeHelper: AuthChallengeHelperService,
|
|
72
|
-
protected readonly clientInfoService: ClientInfoService,
|
|
73
|
-
// State store for CSRF protection - shared across all providers
|
|
74
|
-
protected readonly stateStore: Map<string, { timestamp: number; provider: string }>,
|
|
75
|
-
// User repository for creating social users
|
|
76
|
-
protected readonly userRepository: Repository<BaseUser>,
|
|
77
|
-
// Phone verification service (optional - only available when SMS provider is configured)
|
|
78
|
-
protected readonly phoneVerificationService?: PhoneVerificationService,
|
|
79
|
-
protected readonly auditService?: AuthAuditService, // Optional - audit trail service (enabled via config.auditLogs.enabled)
|
|
80
|
-
protected readonly trustedDeviceService?: TrustedDeviceService, // Optional - only available when rememberDevices is not 'never'
|
|
81
|
-
) {}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get provider configuration dynamically
|
|
85
|
-
*
|
|
86
|
-
* Accesses config.social[providerName] without hardcoding provider names.
|
|
87
|
-
* This allows any provider to work without modifying core code.
|
|
88
|
-
*
|
|
89
|
-
* @returns Provider configuration from NAuthConfig
|
|
90
|
-
* @protected
|
|
91
|
-
*/
|
|
92
|
-
protected getProviderConfig(): any {
|
|
93
|
-
const socialConfig = this.config.social;
|
|
94
|
-
if (!socialConfig) return null;
|
|
95
|
-
|
|
96
|
-
// Access config dynamically using providerName (no hardcoding)
|
|
97
|
-
return (socialConfig as Record<string, unknown>)[this.providerName] || null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Generate OAuth authorization URL for this provider
|
|
102
|
-
*
|
|
103
|
-
* Must be implemented by provider-specific services to generate the OAuth URL.
|
|
104
|
-
*
|
|
105
|
-
* @param state - Optional state parameter for CSRF protection
|
|
106
|
-
* @returns Authorization URL to redirect user to
|
|
107
|
-
* @throws {BadRequestException} When provider is not properly configured
|
|
108
|
-
*/
|
|
109
|
-
abstract getAuthUrl(state?: string): Promise<string>;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Get user profile from OAuth callback
|
|
113
|
-
*
|
|
114
|
-
* Must be implemented by provider-specific services to exchange code for tokens
|
|
115
|
-
* and fetch user profile.
|
|
116
|
-
*
|
|
117
|
-
* @param code - Authorization code from OAuth callback
|
|
118
|
-
* @param state - State parameter from OAuth callback
|
|
119
|
-
* @returns OAuth user profile
|
|
120
|
-
* @protected
|
|
121
|
-
*/
|
|
122
|
-
protected abstract getOAuthProfile(code: string, state: string): Promise<OAuthUserProfile>;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Verify social authentication token from native mobile apps
|
|
126
|
-
*
|
|
127
|
-
* Must be implemented by provider-specific services to verify ID tokens.
|
|
128
|
-
*
|
|
129
|
-
* @param idToken - ID token from native SDK
|
|
130
|
-
* @param accessToken - Optional access token from native SDK
|
|
131
|
-
* @param profileData - Optional profile data from native SDK
|
|
132
|
-
* @returns OAuth user profile
|
|
133
|
-
* @protected
|
|
134
|
-
*/
|
|
135
|
-
protected abstract verifyNativeToken(
|
|
136
|
-
idToken: string,
|
|
137
|
-
accessToken?: string,
|
|
138
|
-
profileData?: any,
|
|
139
|
-
): Promise<OAuthUserProfile>;
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Handle OAuth callback and authenticate user
|
|
143
|
-
*
|
|
144
|
-
* Uses the provider-specific getOAuthProfile method and then handles
|
|
145
|
-
* user creation, session management, and token generation.
|
|
146
|
-
*/
|
|
147
|
-
async handleCallback(code: string, state: string): Promise<AuthResponseDTO> {
|
|
148
|
-
const providerConfig = this.getProviderConfig();
|
|
149
|
-
if (!providerConfig) {
|
|
150
|
-
throw new NAuthException(
|
|
151
|
-
AuthErrorCode.SOCIAL_CONFIG_MISSING,
|
|
152
|
-
`Provider configuration not found: ${this.providerName}`,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Validate state (basic CSRF protection)
|
|
157
|
-
this.validateState(state);
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
// Get user profile from provider
|
|
161
|
-
const profile = await this.getOAuthProfile(code, state);
|
|
162
|
-
this.logger?.log?.(`[SocialAuth] ${this.providerName} callback verified (secure): ${profile.email}`);
|
|
163
|
-
|
|
164
|
-
// Find or create user
|
|
165
|
-
const user = await this.findOrCreateUser(profile, providerConfig);
|
|
166
|
-
|
|
167
|
-
// Create or update social account
|
|
168
|
-
await this.createOrUpdateSocialAccount(user, profile);
|
|
169
|
-
|
|
170
|
-
// Generate JWT tokens and session
|
|
171
|
-
return await this.createAuthResponse(user, 'web');
|
|
172
|
-
} catch (error) {
|
|
173
|
-
if (error instanceof NAuthException) {
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
if (error instanceof Error) {
|
|
177
|
-
throw new NAuthException(AuthErrorCode.SOCIAL_TOKEN_INVALID, `Social authentication failed: ${error.message}`);
|
|
178
|
-
}
|
|
179
|
-
throw new NAuthException(AuthErrorCode.SOCIAL_TOKEN_INVALID, 'Social authentication failed: Unknown error');
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Verify social authentication token from native mobile apps
|
|
185
|
-
*/
|
|
186
|
-
async verifyToken(idToken: string, accessToken?: string, profileData?: any): Promise<AuthResponseDTO> {
|
|
187
|
-
const providerConfig = this.getProviderConfig();
|
|
188
|
-
if (!providerConfig || !providerConfig.enabled) {
|
|
189
|
-
throw new NAuthException(
|
|
190
|
-
AuthErrorCode.SOCIAL_CONFIG_MISSING,
|
|
191
|
-
`Provider '${this.providerName}' is not configured or not enabled`,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
// Verify token and get profile
|
|
197
|
-
const profile = await this.verifyNativeToken(idToken, accessToken, profileData);
|
|
198
|
-
|
|
199
|
-
// Find or create user
|
|
200
|
-
const user = await this.findOrCreateUser(profile, providerConfig);
|
|
201
|
-
|
|
202
|
-
// Create or update social account
|
|
203
|
-
await this.createOrUpdateSocialAccount(user, profile);
|
|
204
|
-
|
|
205
|
-
// Generate JWT tokens and session
|
|
206
|
-
return await this.createAuthResponse(user, 'mobile');
|
|
207
|
-
} catch (error) {
|
|
208
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
209
|
-
this.logger?.error?.(`Native token verification failed for ${this.providerName}: ${errorMessage}`);
|
|
210
|
-
throw new NAuthException(AuthErrorCode.SOCIAL_TOKEN_INVALID, `Token verification failed: ${errorMessage}`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Link social account to existing user
|
|
216
|
-
*/
|
|
217
|
-
async linkAccount(userId: string, code: string, state: string): Promise<{ message: string }> {
|
|
218
|
-
// Get full user entity (need internal id for foreign keys)
|
|
219
|
-
const user = (await this.userRepository.findOne({ where: { sub: userId } })) as IUser | null;
|
|
220
|
-
if (!user) {
|
|
221
|
-
throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const providerConfig = this.getProviderConfig();
|
|
225
|
-
if (!providerConfig) {
|
|
226
|
-
throw new NAuthException(
|
|
227
|
-
AuthErrorCode.SOCIAL_CONFIG_MISSING,
|
|
228
|
-
`Provider configuration not found: ${this.providerName}`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Validate state
|
|
233
|
-
this.validateState(state);
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
// Get user profile from provider
|
|
237
|
-
const profile = await this.getOAuthProfile(code, state);
|
|
238
|
-
|
|
239
|
-
// Check if account is already linked
|
|
240
|
-
const existingAccount = await this.socialAuthService.findSocialAccountByProvider(this.providerName, profile.id);
|
|
241
|
-
|
|
242
|
-
if (existingAccount) {
|
|
243
|
-
throw new NAuthException(
|
|
244
|
-
AuthErrorCode.SOCIAL_ACCOUNT_LINKED,
|
|
245
|
-
'This social account is already linked to another user',
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Create social account using service
|
|
250
|
-
await this.socialAuthService.createOrUpdateSocialAccount(
|
|
251
|
-
user.id as number,
|
|
252
|
-
this.providerName,
|
|
253
|
-
profile.id,
|
|
254
|
-
profile.email,
|
|
255
|
-
profile.raw,
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
// ============================================================================
|
|
259
|
-
// Audit: Record social account link
|
|
260
|
-
// ============================================================================
|
|
261
|
-
try {
|
|
262
|
-
await this.auditService?.recordEvent({
|
|
263
|
-
userId: user.id,
|
|
264
|
-
eventType: AuthAuditEventType.SOCIAL_ACCOUNT_LINKED,
|
|
265
|
-
eventStatus: 'INFO',
|
|
266
|
-
authMethod: this.providerName,
|
|
267
|
-
// Client info automatically included from context
|
|
268
|
-
metadata: {
|
|
269
|
-
provider: this.providerName,
|
|
270
|
-
providerEmail: profile.email || null,
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
} catch (auditError) {
|
|
274
|
-
// Non-blocking: Log but continue
|
|
275
|
-
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
276
|
-
this.logger?.error?.(`Failed to record SOCIAL_ACCOUNT_LINKED audit event: ${errorMessage}`, {
|
|
277
|
-
error: auditError,
|
|
278
|
-
userId: user.id,
|
|
279
|
-
provider: this.providerName,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ============================================================================
|
|
284
|
-
// Audit: Record social account link
|
|
285
|
-
// ============================================================================
|
|
286
|
-
try {
|
|
287
|
-
await this.auditService?.recordEvent({
|
|
288
|
-
userId: user.id,
|
|
289
|
-
eventType: AuthAuditEventType.SOCIAL_ACCOUNT_LINKED,
|
|
290
|
-
eventStatus: 'SUCCESS',
|
|
291
|
-
authMethod: this.providerName.toLowerCase(),
|
|
292
|
-
// Client info automatically included from context
|
|
293
|
-
metadata: {
|
|
294
|
-
provider: this.providerName.toLowerCase(),
|
|
295
|
-
providerEmail: profile.email || null,
|
|
296
|
-
},
|
|
297
|
-
});
|
|
298
|
-
} catch (auditError) {
|
|
299
|
-
// Non-blocking: Log but continue
|
|
300
|
-
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
301
|
-
this.logger?.error?.(`Failed to record SOCIAL_ACCOUNT_LINKED audit event: ${errorMessage}`, {
|
|
302
|
-
error: auditError,
|
|
303
|
-
userId: user.id,
|
|
304
|
-
provider: this.providerName,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return { message: `${this.providerName} account linked successfully` };
|
|
309
|
-
} catch (error) {
|
|
310
|
-
if (error instanceof NAuthException) {
|
|
311
|
-
throw error;
|
|
312
|
-
}
|
|
313
|
-
if (error instanceof Error) {
|
|
314
|
-
throw new NAuthException(AuthErrorCode.SOCIAL_TOKEN_INVALID, `Social account linking failed: ${error.message}`);
|
|
315
|
-
}
|
|
316
|
-
throw new NAuthException(AuthErrorCode.SOCIAL_TOKEN_INVALID, 'Social account linking failed: Unknown error');
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Get OAuth user profile from callback
|
|
322
|
-
*
|
|
323
|
-
* Alias for getOAuthProfile for interface compliance.
|
|
324
|
-
*/
|
|
325
|
-
async getUserProfileFromCallback(code: string, state: string): Promise<OAuthUserProfile> {
|
|
326
|
-
return this.getOAuthProfile(code, state);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ============================================================================
|
|
330
|
-
// Protected Helper Methods
|
|
331
|
-
// ============================================================================
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Validate state parameter for CSRF protection
|
|
335
|
-
*/
|
|
336
|
-
protected validateState(state: string): void {
|
|
337
|
-
const stateData = this.stateStore.get(state);
|
|
338
|
-
if (!stateData) {
|
|
339
|
-
throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, 'Invalid state parameter', { field: 'state' });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (stateData.provider !== this.providerName) {
|
|
343
|
-
throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, 'State provider mismatch', { field: 'state' });
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Check if state is not too old (5 minutes)
|
|
347
|
-
if (Date.now() - stateData.timestamp > 5 * 60 * 1000) {
|
|
348
|
-
this.stateStore.delete(state);
|
|
349
|
-
throw new NAuthException(AuthErrorCode.CHALLENGE_EXPIRED, 'State parameter expired');
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Clean up used state
|
|
353
|
-
this.stateStore.delete(state);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Generate random state for CSRF protection
|
|
358
|
-
*/
|
|
359
|
-
protected generateState(): string {
|
|
360
|
-
const state = crypto.randomBytes(32).toString('hex');
|
|
361
|
-
this.stateStore.set(state, {
|
|
362
|
-
timestamp: Date.now(),
|
|
363
|
-
provider: this.providerName,
|
|
364
|
-
});
|
|
365
|
-
return state;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Find existing user or create new one
|
|
370
|
-
*/
|
|
371
|
-
protected async findOrCreateUser(profile: OAuthUserProfile, providerConfig: any): Promise<IUser> {
|
|
372
|
-
// First, try to find user by social account
|
|
373
|
-
const socialAccount = await this.socialAuthService.findSocialAccountByProvider(this.providerName, profile.id);
|
|
374
|
-
|
|
375
|
-
if (socialAccount) {
|
|
376
|
-
return (socialAccount as unknown as { user?: IUser }).user as IUser;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// If auto-link is enabled, try to find by email
|
|
380
|
-
if (providerConfig.autoLink && profile.email) {
|
|
381
|
-
// Get full user entity (need internal id for foreign keys)
|
|
382
|
-
const existingUser = (await this.userRepository.findOne({
|
|
383
|
-
where: { email: profile.email, isEmailVerified: true },
|
|
384
|
-
})) as IUser | null;
|
|
385
|
-
|
|
386
|
-
if (existingUser) {
|
|
387
|
-
return existingUser;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Create new user if allowSignup is enabled
|
|
392
|
-
if (providerConfig.allowSignup) {
|
|
393
|
-
this.logger?.log?.(
|
|
394
|
-
`[SocialAuth] Creating user: email=${profile.email}, isEmailVerified=${profile.verified || false}`,
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
const savedUser = await this.createSocialUser(
|
|
398
|
-
profile.email || '',
|
|
399
|
-
profile.firstName,
|
|
400
|
-
profile.lastName,
|
|
401
|
-
profile.verified || false,
|
|
402
|
-
this.providerName,
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
this.logger?.log?.(
|
|
406
|
-
`[SocialAuth] User created: email=${savedUser.email}, isEmailVerified=${savedUser.isEmailVerified}`,
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
return savedUser;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
throw new NAuthException(AuthErrorCode.SIGNUP_DISABLED, 'User not found and signup is disabled');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Create a social-only user (no password)
|
|
417
|
-
*
|
|
418
|
-
* @param email - User email
|
|
419
|
-
* @param firstName - Optional first name
|
|
420
|
-
* @param lastName - Optional last name
|
|
421
|
-
* @param isEmailVerified - Whether email is verified (default: true)
|
|
422
|
-
* @param socialProvider - Initial social provider name
|
|
423
|
-
* @returns Created user
|
|
424
|
-
* @protected
|
|
425
|
-
*/
|
|
426
|
-
protected async createSocialUser(
|
|
427
|
-
email: string,
|
|
428
|
-
firstName?: string | null,
|
|
429
|
-
lastName?: string | null,
|
|
430
|
-
isEmailVerified: boolean = true,
|
|
431
|
-
socialProvider?: string,
|
|
432
|
-
): Promise<IUser> {
|
|
433
|
-
const user = this.userRepository.create({
|
|
434
|
-
email,
|
|
435
|
-
firstName: firstName || null,
|
|
436
|
-
lastName: lastName || null,
|
|
437
|
-
isEmailVerified,
|
|
438
|
-
hasSocialAuth: true,
|
|
439
|
-
socialProviders: socialProvider ? [socialProvider] : null,
|
|
440
|
-
isActive: true,
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
const savedUser = (await this.userRepository.save(user)) as unknown as IUser;
|
|
444
|
-
this.logger?.log?.(`Social user created: ${email} (sub: ${savedUser.sub})`);
|
|
445
|
-
return savedUser;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Create or update social account
|
|
450
|
-
*/
|
|
451
|
-
protected async createOrUpdateSocialAccount(user: IUser, profile: OAuthUserProfile): Promise<void> {
|
|
452
|
-
await this.socialAuthService.createOrUpdateSocialAccount(
|
|
453
|
-
user.id as number,
|
|
454
|
-
this.providerName,
|
|
455
|
-
profile.id,
|
|
456
|
-
profile.email,
|
|
457
|
-
profile.raw,
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Create authentication response with tokens and user info
|
|
463
|
-
*/
|
|
464
|
-
protected async createAuthResponse(user: IUser, _deviceType: 'web' | 'mobile'): Promise<AuthResponseDTO> {
|
|
465
|
-
// Get actual client info from context (IP, userAgent, etc.)
|
|
466
|
-
const clientInfo = this.clientInfoService.get();
|
|
467
|
-
|
|
468
|
-
// ============================================================================
|
|
469
|
-
// Audit: Record login attempt for social authentication
|
|
470
|
-
// ============================================================================
|
|
471
|
-
try {
|
|
472
|
-
await this.auditService?.recordEvent({
|
|
473
|
-
userId: user.id,
|
|
474
|
-
eventType: AuthAuditEventType.LOGIN_ATTEMPT,
|
|
475
|
-
eventStatus: 'INFO',
|
|
476
|
-
authMethod: this.providerName.toLowerCase(),
|
|
477
|
-
description: `${this.providerName} OAuth token validated`,
|
|
478
|
-
});
|
|
479
|
-
} catch (auditError) {
|
|
480
|
-
// Non-blocking: Log but continue
|
|
481
|
-
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
482
|
-
this.logger?.error?.(`Failed to record LOGIN_ATTEMPT audit event for social login: ${errorMessage}`, {
|
|
483
|
-
error: auditError,
|
|
484
|
-
userId: user.id,
|
|
485
|
-
provider: this.providerName,
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// ============================================================================
|
|
490
|
-
// Check for Required Challenges BEFORE creating session
|
|
491
|
-
// ============================================================================
|
|
492
|
-
const response = await this.challengeHelper.determineAuthResponse({
|
|
493
|
-
user,
|
|
494
|
-
config: this.config,
|
|
495
|
-
deviceToken: clientInfo.deviceToken,
|
|
496
|
-
isSocialLogin: true,
|
|
497
|
-
skipMFAVerification: false,
|
|
498
|
-
authProvider: this.providerName.toLowerCase(), // e.g., 'google', 'facebook', 'apple'
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
if (response.challengeName) {
|
|
502
|
-
this.logger?.log?.(`Challenge required for social auth user ${user.sub}: ${response.challengeName}`);
|
|
503
|
-
// Record SOCIAL_LOGIN event even when challenge is required
|
|
504
|
-
try {
|
|
505
|
-
await this.auditService?.recordEvent({
|
|
506
|
-
userId: user.id,
|
|
507
|
-
eventType: AuthAuditEventType.SOCIAL_LOGIN,
|
|
508
|
-
eventStatus: 'INFO',
|
|
509
|
-
authMethod: this.providerName.toLowerCase(),
|
|
510
|
-
metadata: {
|
|
511
|
-
provider: this.providerName.toLowerCase(),
|
|
512
|
-
challengeRequired: response.challengeName,
|
|
513
|
-
},
|
|
514
|
-
});
|
|
515
|
-
} catch (auditError) {
|
|
516
|
-
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
517
|
-
this.logger?.error?.(`Failed to record SOCIAL_LOGIN audit event (challenge): ${errorMessage}`, {
|
|
518
|
-
error: auditError,
|
|
519
|
-
userId: user.id,
|
|
520
|
-
provider: this.providerName,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
return response;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// ============================================================================
|
|
527
|
-
// No challenges - determineAuthResponse already created session and tokens
|
|
528
|
-
// Just record SOCIAL_LOGIN audit event and return the response
|
|
529
|
-
// ============================================================================
|
|
530
|
-
// ============================================================================
|
|
531
|
-
// No challenges - determineAuthResponse already created session and tokens
|
|
532
|
-
// Just record SOCIAL_LOGIN audit event and return the response
|
|
533
|
-
// ============================================================================
|
|
534
|
-
|
|
535
|
-
// Check trusted device status (for audit metadata)
|
|
536
|
-
let isTrustedDevice = false;
|
|
537
|
-
if (
|
|
538
|
-
this.config.mfa?.rememberDevices &&
|
|
539
|
-
this.config.mfa?.rememberDevices !== 'never' &&
|
|
540
|
-
this.trustedDeviceService &&
|
|
541
|
-
clientInfo.deviceToken
|
|
542
|
-
) {
|
|
543
|
-
try {
|
|
544
|
-
isTrustedDevice = await this.trustedDeviceService.isDeviceTrusted(clientInfo.deviceToken, user.id);
|
|
545
|
-
} catch (error) {
|
|
546
|
-
// Non-blocking: Log but continue
|
|
547
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
548
|
-
this.logger?.warn?.(`Failed to check trusted device for social login: ${errorMessage}`, {
|
|
549
|
-
error,
|
|
550
|
-
userId: user.id,
|
|
551
|
-
provider: this.providerName,
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Record SOCIAL_LOGIN audit event
|
|
557
|
-
try {
|
|
558
|
-
await this.auditService?.recordEvent({
|
|
559
|
-
userId: user.id,
|
|
560
|
-
eventType: AuthAuditEventType.SOCIAL_LOGIN,
|
|
561
|
-
eventStatus: 'SUCCESS',
|
|
562
|
-
authMethod: this.providerName.toLowerCase(),
|
|
563
|
-
metadata: {
|
|
564
|
-
provider: this.providerName.toLowerCase(),
|
|
565
|
-
trustedDevice: isTrustedDevice,
|
|
566
|
-
},
|
|
567
|
-
});
|
|
568
|
-
} catch (auditError) {
|
|
569
|
-
// Non-blocking: Log but continue
|
|
570
|
-
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
571
|
-
this.logger?.error?.(`Failed to record SOCIAL_LOGIN audit event: ${errorMessage}`, {
|
|
572
|
-
error: auditError,
|
|
573
|
-
userId: user.id,
|
|
574
|
-
provider: this.providerName,
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Return the response with session and tokens already created by determineAuthResponse
|
|
579
|
-
return response;
|
|
580
|
-
}
|
|
581
|
-
}
|