@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,238 +0,0 @@
|
|
|
1
|
-
import { SocialAuthService } from './social-auth.service';
|
|
2
|
-
import { ISocialAuthProviderService } from '../interfaces/social-auth-provider.interface';
|
|
3
|
-
import { NAuthException } from '../exceptions/nauth.exception';
|
|
4
|
-
import { AuthErrorCode } from '../enums/error-codes.enum';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Social Auth Service Unit Tests
|
|
8
|
-
*
|
|
9
|
-
* Tests social authentication provider registry functionality.
|
|
10
|
-
* Covers provider registration, lookup, and listing.
|
|
11
|
-
*
|
|
12
|
-
* Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
|
|
13
|
-
*/
|
|
14
|
-
describe('SocialAuthService', () => {
|
|
15
|
-
let service: SocialAuthService;
|
|
16
|
-
let mockProvider1: jest.Mocked<ISocialAuthProviderService>;
|
|
17
|
-
let mockProvider2: jest.Mocked<ISocialAuthProviderService>;
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
// Create mock providers
|
|
21
|
-
mockProvider1 = {
|
|
22
|
-
providerName: 'google',
|
|
23
|
-
getAuthUrl: jest.fn(),
|
|
24
|
-
handleCallback: jest.fn(),
|
|
25
|
-
verifyToken: jest.fn(),
|
|
26
|
-
linkAccount: jest.fn(),
|
|
27
|
-
getUserProfileFromCallback: jest.fn(),
|
|
28
|
-
} as any;
|
|
29
|
-
|
|
30
|
-
mockProvider2 = {
|
|
31
|
-
providerName: 'apple',
|
|
32
|
-
getAuthUrl: jest.fn(),
|
|
33
|
-
handleCallback: jest.fn(),
|
|
34
|
-
verifyToken: jest.fn(),
|
|
35
|
-
linkAccount: jest.fn(),
|
|
36
|
-
getUserProfileFromCallback: jest.fn(),
|
|
37
|
-
} as any;
|
|
38
|
-
|
|
39
|
-
// Instantiate service directly
|
|
40
|
-
service = new SocialAuthService();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(() => {
|
|
44
|
-
jest.clearAllMocks();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// ============================================================================
|
|
48
|
-
// Service Initialization
|
|
49
|
-
// ============================================================================
|
|
50
|
-
|
|
51
|
-
it('should be defined', () => {
|
|
52
|
-
expect(service).toBeDefined();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// ============================================================================
|
|
56
|
-
// registerProvider() Method
|
|
57
|
-
// ============================================================================
|
|
58
|
-
|
|
59
|
-
describe('registerProvider', () => {
|
|
60
|
-
it('should register provider successfully', () => {
|
|
61
|
-
service.registerProvider(mockProvider1);
|
|
62
|
-
|
|
63
|
-
expect(service.hasProvider('google')).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should throw error when provider already registered', () => {
|
|
67
|
-
service.registerProvider(mockProvider1);
|
|
68
|
-
|
|
69
|
-
expect(() => service.registerProvider(mockProvider1)).toThrow(NAuthException);
|
|
70
|
-
expect(() => service.registerProvider(mockProvider1)).toThrow('already registered');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should allow multiple different providers', () => {
|
|
74
|
-
service.registerProvider(mockProvider1);
|
|
75
|
-
service.registerProvider(mockProvider2);
|
|
76
|
-
|
|
77
|
-
expect(service.hasProvider('google')).toBe(true);
|
|
78
|
-
expect(service.hasProvider('apple')).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should register provider with correct name', () => {
|
|
82
|
-
service.registerProvider(mockProvider1);
|
|
83
|
-
|
|
84
|
-
const provider = service.getProvider('google');
|
|
85
|
-
expect(provider).toBe(mockProvider1);
|
|
86
|
-
expect(provider.providerName).toBe('google');
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// ============================================================================
|
|
91
|
-
// getProvider() Method
|
|
92
|
-
// ============================================================================
|
|
93
|
-
|
|
94
|
-
describe('getProvider', () => {
|
|
95
|
-
it('should return registered provider', () => {
|
|
96
|
-
service.registerProvider(mockProvider1);
|
|
97
|
-
|
|
98
|
-
const provider = service.getProvider('google');
|
|
99
|
-
|
|
100
|
-
expect(provider).toBe(mockProvider1);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should throw error when provider not registered', () => {
|
|
104
|
-
expect(() => service.getProvider('google')).toThrow(NAuthException);
|
|
105
|
-
expect(() => service.getProvider('google')).toThrow('not registered');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should throw error with helpful message suggesting module import', () => {
|
|
109
|
-
try {
|
|
110
|
-
service.getProvider('facebook');
|
|
111
|
-
fail('Should have thrown NAuthException');
|
|
112
|
-
} catch (error: any) {
|
|
113
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
114
|
-
expect(error.message).toContain('Import the provider module');
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should use correct error code when provider not found', () => {
|
|
119
|
-
try {
|
|
120
|
-
service.getProvider('google');
|
|
121
|
-
} catch (error) {
|
|
122
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
123
|
-
expect((error as NAuthException).code).toBe(AuthErrorCode.SOCIAL_CONFIG_MISSING);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ============================================================================
|
|
129
|
-
// hasProvider() Method
|
|
130
|
-
// ============================================================================
|
|
131
|
-
|
|
132
|
-
describe('hasProvider', () => {
|
|
133
|
-
it('should return true for registered provider', () => {
|
|
134
|
-
service.registerProvider(mockProvider1);
|
|
135
|
-
|
|
136
|
-
expect(service.hasProvider('google')).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should return false for unregistered provider', () => {
|
|
140
|
-
expect(service.hasProvider('google')).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should return false for provider that was never registered', () => {
|
|
144
|
-
service.registerProvider(mockProvider1);
|
|
145
|
-
|
|
146
|
-
expect(service.hasProvider('facebook')).toBe(false);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// ============================================================================
|
|
151
|
-
// listProviders() Method
|
|
152
|
-
// ============================================================================
|
|
153
|
-
|
|
154
|
-
describe('listProviders', () => {
|
|
155
|
-
it('should return empty array when no providers registered', () => {
|
|
156
|
-
expect(service.listProviders()).toEqual([]);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should return all registered provider names', () => {
|
|
160
|
-
service.registerProvider(mockProvider1);
|
|
161
|
-
service.registerProvider(mockProvider2);
|
|
162
|
-
|
|
163
|
-
const providers = service.listProviders();
|
|
164
|
-
|
|
165
|
-
expect(providers).toContain('google');
|
|
166
|
-
expect(providers).toContain('apple');
|
|
167
|
-
expect(providers.length).toBe(2);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should return provider names in registration order', () => {
|
|
171
|
-
service.registerProvider(mockProvider1);
|
|
172
|
-
service.registerProvider(mockProvider2);
|
|
173
|
-
|
|
174
|
-
const providers = service.listProviders();
|
|
175
|
-
|
|
176
|
-
expect(providers[0]).toBe('google');
|
|
177
|
-
expect(providers[1]).toBe('apple');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should return updated list after new provider registered', () => {
|
|
181
|
-
expect(service.listProviders()).toEqual([]);
|
|
182
|
-
|
|
183
|
-
service.registerProvider(mockProvider1);
|
|
184
|
-
expect(service.listProviders()).toEqual(['google']);
|
|
185
|
-
|
|
186
|
-
service.registerProvider(mockProvider2);
|
|
187
|
-
expect(service.listProviders()).toEqual(['google', 'apple']);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// ============================================================================
|
|
192
|
-
// Integration Tests
|
|
193
|
-
// ============================================================================
|
|
194
|
-
|
|
195
|
-
describe('Integration', () => {
|
|
196
|
-
it('should allow full provider lifecycle', () => {
|
|
197
|
-
// Register
|
|
198
|
-
service.registerProvider(mockProvider1);
|
|
199
|
-
expect(service.hasProvider('google')).toBe(true);
|
|
200
|
-
|
|
201
|
-
// Get
|
|
202
|
-
const provider = service.getProvider('google');
|
|
203
|
-
expect(provider).toBe(mockProvider1);
|
|
204
|
-
|
|
205
|
-
// List
|
|
206
|
-
const providers = service.listProviders();
|
|
207
|
-
expect(providers).toContain('google');
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should handle multiple providers independently', () => {
|
|
211
|
-
service.registerProvider(mockProvider1);
|
|
212
|
-
service.registerProvider(mockProvider2);
|
|
213
|
-
|
|
214
|
-
const googleProvider = service.getProvider('google');
|
|
215
|
-
const appleProvider = service.getProvider('apple');
|
|
216
|
-
|
|
217
|
-
expect(googleProvider).toBe(mockProvider1);
|
|
218
|
-
expect(appleProvider).toBe(mockProvider2);
|
|
219
|
-
expect(googleProvider).not.toBe(appleProvider);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should maintain provider registry across operations', () => {
|
|
223
|
-
service.registerProvider(mockProvider1);
|
|
224
|
-
service.registerProvider(mockProvider2);
|
|
225
|
-
|
|
226
|
-
// Verify both still registered
|
|
227
|
-
expect(service.hasProvider('google')).toBe(true);
|
|
228
|
-
expect(service.hasProvider('apple')).toBe(true);
|
|
229
|
-
|
|
230
|
-
// Get both
|
|
231
|
-
const google = service.getProvider('google');
|
|
232
|
-
const apple = service.getProvider('apple');
|
|
233
|
-
|
|
234
|
-
expect(google).toBe(mockProvider1);
|
|
235
|
-
expect(apple).toBe(mockProvider2);
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
});
|
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
import { Repository } from 'typeorm';
|
|
2
|
-
import { IUser, ISocialAccount } from '../interfaces/entities.interface';
|
|
3
|
-
import { BaseUser, BaseSocialAccount } from '../entities';
|
|
4
|
-
import { AuthService } from './auth.service';
|
|
5
|
-
import { InternalAuthAuditService as AuthAuditService } from './auth-audit.service';
|
|
6
|
-
import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
|
|
7
|
-
import { NAuthException } from '../exceptions/nauth.exception';
|
|
8
|
-
import { AuthErrorCode } from '../enums/error-codes.enum';
|
|
9
|
-
import { NAuthLogger } from '../utils/nauth-logger';
|
|
10
|
-
import { ChangePasswordRequestDTO } from '../dto/change-password-request.dto';
|
|
11
|
-
import { SocialProviderRegistry } from './social-provider-registry.service';
|
|
12
|
-
import { AuthResponseDTO } from '../dto/auth-response.dto';
|
|
13
|
-
import {
|
|
14
|
-
GetSocialAuthUrlDTO,
|
|
15
|
-
GetSocialAuthUrlResponseDTO,
|
|
16
|
-
HandleSocialCallbackDTO,
|
|
17
|
-
LinkSocialAccountDTO,
|
|
18
|
-
LinkSocialAccountResponseDTO,
|
|
19
|
-
GetLinkedAccountsDTO,
|
|
20
|
-
GetLinkedAccountsResponseDTO,
|
|
21
|
-
UnlinkSocialAccountDTO,
|
|
22
|
-
UnlinkSocialAccountResponseDTO,
|
|
23
|
-
CanSetPasswordDTO,
|
|
24
|
-
CanSetPasswordResponseDTO,
|
|
25
|
-
SetPasswordForSocialUserDTO,
|
|
26
|
-
SetPasswordForSocialUserResponseDTO,
|
|
27
|
-
} from '../dto/social-auth.dto';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Social Auth Service
|
|
31
|
-
*
|
|
32
|
-
* Complete API for social authentication (OAuth) and account management.
|
|
33
|
-
* This service provides:
|
|
34
|
-
* - OAuth authentication flows (login/signup via social providers)
|
|
35
|
-
* - Social account linking/unlinking
|
|
36
|
-
* - Account management for social users
|
|
37
|
-
* - Password management for social-only users
|
|
38
|
-
*
|
|
39
|
-
* **Optional Feature:** Only available when social auth provider modules are imported.
|
|
40
|
-
*
|
|
41
|
-
* **Usage:**
|
|
42
|
-
* ```typescript
|
|
43
|
-
* // NestJS
|
|
44
|
-
* imports: [
|
|
45
|
-
* AuthModule.forRoot(config),
|
|
46
|
-
* GoogleSocialAuthModule, // Enables Google OAuth
|
|
47
|
-
* AppleSocialAuthModule, // Enables Apple Sign In
|
|
48
|
-
* ]
|
|
49
|
-
*
|
|
50
|
-
* // Then inject and use
|
|
51
|
-
* constructor(private socialAuthService: SocialAuthService) {}
|
|
52
|
-
*
|
|
53
|
-
* const { url } = await this.socialAuthService.getSocialAuthUrl({ provider: 'google' });
|
|
54
|
-
* const result = await this.socialAuthService.handleSocialCallback({ provider: 'google', code, state });
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export class SocialAuthService {
|
|
58
|
-
constructor(
|
|
59
|
-
private readonly providerRegistry: SocialProviderRegistry,
|
|
60
|
-
private readonly userRepository: Repository<BaseUser>,
|
|
61
|
-
private readonly socialAccountRepository: Repository<BaseSocialAccount>,
|
|
62
|
-
private readonly authService: AuthService,
|
|
63
|
-
private readonly logger: NAuthLogger,
|
|
64
|
-
private readonly auditService?: AuthAuditService, // Optional - audit trail service (enabled via config.auditLogs.enabled)
|
|
65
|
-
) {}
|
|
66
|
-
|
|
67
|
-
// ============================================================================
|
|
68
|
-
// Social Authentication Methods
|
|
69
|
-
// ============================================================================
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get social authentication URL
|
|
73
|
-
*
|
|
74
|
-
* Generates OAuth authorization URL for the specified provider.
|
|
75
|
-
* This is the first step in the OAuth flow - redirect user to this URL.
|
|
76
|
-
*
|
|
77
|
-
* @param dto - Request DTO containing provider and optional state
|
|
78
|
-
* @returns Response DTO with OAuth authorization URL
|
|
79
|
-
* @throws {NAuthException} SOCIAL_CONFIG_MISSING if provider not registered or configured
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* ```typescript
|
|
83
|
-
* const dto = { provider: 'google', state: 'csrf-token-123' };
|
|
84
|
-
* const { url } = await socialAuthService.getSocialAuthUrl(dto);
|
|
85
|
-
* // Redirect user to url
|
|
86
|
-
* res.redirect(url);
|
|
87
|
-
* ```
|
|
88
|
-
*/
|
|
89
|
-
async getSocialAuthUrl(dto: GetSocialAuthUrlDTO): Promise<GetSocialAuthUrlResponseDTO> {
|
|
90
|
-
const { provider, state } = dto;
|
|
91
|
-
const providerInstance = this.providerRegistry.getProvider(provider);
|
|
92
|
-
const url = await providerInstance.getAuthUrl(state);
|
|
93
|
-
return { url };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Handle social authentication callback
|
|
98
|
-
*
|
|
99
|
-
* Processes OAuth callback and authenticates user (login or signup).
|
|
100
|
-
* This is called after the user is redirected back from the OAuth provider.
|
|
101
|
-
*
|
|
102
|
-
* @param dto - Request DTO containing provider, code, and state
|
|
103
|
-
* @returns Auth response (tokens or challenge if MFA/verification required)
|
|
104
|
-
* @throws {NAuthException} Various auth errors (SOCIAL_AUTH_FAILED, etc.)
|
|
105
|
-
*
|
|
106
|
-
* @example
|
|
107
|
-
* ```typescript
|
|
108
|
-
* const dto = {
|
|
109
|
-
* provider: 'google',
|
|
110
|
-
* code: req.query.code,
|
|
111
|
-
* state: req.query.state
|
|
112
|
-
* };
|
|
113
|
-
* const result = await socialAuthService.handleSocialCallback(dto);
|
|
114
|
-
* // Returns tokens or challenge
|
|
115
|
-
* ```
|
|
116
|
-
*/
|
|
117
|
-
async handleSocialCallback(dto: HandleSocialCallbackDTO): Promise<AuthResponseDTO> {
|
|
118
|
-
const { provider, code, state } = dto;
|
|
119
|
-
const providerInstance = this.providerRegistry.getProvider(provider);
|
|
120
|
-
return await providerInstance.handleCallback(code, state);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Link social account to existing authenticated user
|
|
125
|
-
*
|
|
126
|
-
* Connects a social provider to an already logged-in user's account.
|
|
127
|
-
* User must be authenticated before calling this method.
|
|
128
|
-
*
|
|
129
|
-
* @param dto - Request DTO containing userId, provider, code, and state
|
|
130
|
-
* @returns Response DTO with success message and provider name
|
|
131
|
-
* @throws {NAuthException} SOCIAL_ALREADY_LINKED, NOT_FOUND, etc.
|
|
132
|
-
*
|
|
133
|
-
* @example
|
|
134
|
-
* ```typescript
|
|
135
|
-
* const dto = {
|
|
136
|
-
* userId: user.sub,
|
|
137
|
-
* provider: 'apple',
|
|
138
|
-
* code: req.query.code,
|
|
139
|
-
* state: req.query.state
|
|
140
|
-
* };
|
|
141
|
-
* const result = await socialAuthService.linkSocialAccount(dto);
|
|
142
|
-
* ```
|
|
143
|
-
*/
|
|
144
|
-
async linkSocialAccount(dto: LinkSocialAccountDTO): Promise<LinkSocialAccountResponseDTO> {
|
|
145
|
-
const { userId, provider, code, state } = dto;
|
|
146
|
-
const providerInstance = this.providerRegistry.getProvider(provider);
|
|
147
|
-
const result = await providerInstance.linkAccount(userId, code, state);
|
|
148
|
-
return { ...result, provider };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* List available social auth providers
|
|
153
|
-
*
|
|
154
|
-
* Returns names of all registered and enabled social auth providers.
|
|
155
|
-
* Useful for displaying available login options in the UI.
|
|
156
|
-
*
|
|
157
|
-
* @returns Array of provider names (e.g., ['google', 'apple', 'facebook'])
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* ```typescript
|
|
161
|
-
* const providers = socialAuthService.listAvailableProviders();
|
|
162
|
-
* // Display social login buttons based on available providers
|
|
163
|
-
* ```
|
|
164
|
-
*/
|
|
165
|
-
listAvailableProviders(): string[] {
|
|
166
|
-
return this.providerRegistry.listProviders();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ============================================================================
|
|
170
|
-
// Social Account Management Methods
|
|
171
|
-
// ============================================================================
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Get linked social accounts for a user
|
|
175
|
-
*
|
|
176
|
-
* @param dto - Request DTO containing userId
|
|
177
|
-
* @returns Response DTO with array of linked social accounts
|
|
178
|
-
* @throws {NAuthException} NOT_FOUND when user is not found
|
|
179
|
-
*
|
|
180
|
-
* @example
|
|
181
|
-
* ```typescript
|
|
182
|
-
* const dto = { userId: 'user-uuid' };
|
|
183
|
-
* const accounts = await socialAuthService.getLinkedAccounts(dto);
|
|
184
|
-
* console.log(accounts.accounts); // [{ provider: 'google', ... }]
|
|
185
|
-
* ```
|
|
186
|
-
*/
|
|
187
|
-
async getLinkedAccounts(dto: GetLinkedAccountsDTO): Promise<GetLinkedAccountsResponseDTO> {
|
|
188
|
-
const { userId } = dto;
|
|
189
|
-
const user = (await this.userRepository.findOne({ where: { sub: userId } })) as IUser | null;
|
|
190
|
-
if (!user) {
|
|
191
|
-
throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const socialAccounts = (await this.socialAccountRepository.find({
|
|
195
|
-
where: { userId: user.id },
|
|
196
|
-
order: { linkedAt: 'DESC' },
|
|
197
|
-
})) as ISocialAccount[];
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
accounts: socialAccounts.map((account) => ({
|
|
201
|
-
provider: account.provider,
|
|
202
|
-
providerEmail: account.providerEmail || undefined,
|
|
203
|
-
linkedAt: account.linkedAt,
|
|
204
|
-
lastUsedAt: account.lastUsedAt || undefined,
|
|
205
|
-
})),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Unlink social account from user
|
|
211
|
-
*
|
|
212
|
-
* @param dto - Request DTO containing userId and provider
|
|
213
|
-
* @returns Response DTO with success message
|
|
214
|
-
* @throws {NAuthException} NOT_FOUND when user or account is not found
|
|
215
|
-
*
|
|
216
|
-
* @example
|
|
217
|
-
* ```typescript
|
|
218
|
-
* const dto = { userId: 'user-uuid', provider: 'google' };
|
|
219
|
-
* await socialAuthService.unlinkSocialAccount(dto);
|
|
220
|
-
* ```
|
|
221
|
-
*/
|
|
222
|
-
async unlinkSocialAccount(dto: UnlinkSocialAccountDTO): Promise<UnlinkSocialAccountResponseDTO> {
|
|
223
|
-
const { userId, provider } = dto;
|
|
224
|
-
const user = (await this.userRepository.findOne({ where: { sub: userId } })) as IUser | null;
|
|
225
|
-
if (!user) {
|
|
226
|
-
throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const socialAccount = (await this.socialAccountRepository.findOne({
|
|
230
|
-
where: { userId: user.id, provider },
|
|
231
|
-
})) as ISocialAccount | null;
|
|
232
|
-
|
|
233
|
-
if (!socialAccount) {
|
|
234
|
-
throw new NAuthException(
|
|
235
|
-
AuthErrorCode.SOCIAL_ACCOUNT_NOT_FOUND,
|
|
236
|
-
`${provider} account is not linked to this user`,
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Delete social account
|
|
241
|
-
await this.socialAccountRepository.remove(socialAccount);
|
|
242
|
-
|
|
243
|
-
// Update user's social auth flags
|
|
244
|
-
await this.updateUserSocialFlags(user.id as number);
|
|
245
|
-
|
|
246
|
-
// ============================================================================
|
|
247
|
-
// Audit: Record social account unlink
|
|
248
|
-
// ============================================================================
|
|
249
|
-
try {
|
|
250
|
-
await this.auditService?.recordEvent({
|
|
251
|
-
userId: user.id,
|
|
252
|
-
eventType: AuthAuditEventType.SOCIAL_ACCOUNT_UNLINKED,
|
|
253
|
-
eventStatus: 'INFO',
|
|
254
|
-
authMethod: provider,
|
|
255
|
-
// Client info automatically included from context
|
|
256
|
-
metadata: {
|
|
257
|
-
provider,
|
|
258
|
-
providerEmail: socialAccount.providerEmail || null,
|
|
259
|
-
},
|
|
260
|
-
});
|
|
261
|
-
} catch (auditError) {
|
|
262
|
-
// Non-blocking: Log but continue
|
|
263
|
-
const errorMessage = auditError instanceof Error ? auditError.message : 'Unknown error';
|
|
264
|
-
this.logger?.error?.(`Failed to record SOCIAL_ACCOUNT_UNLINKED audit event: ${errorMessage}`, {
|
|
265
|
-
error: auditError,
|
|
266
|
-
userId: user.id,
|
|
267
|
-
provider,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return { message: `${provider} account unlinked successfully` };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Check if user can set a password
|
|
276
|
-
* Users with social-only accounts can set passwords
|
|
277
|
-
*
|
|
278
|
-
* @param dto - Request DTO containing userId
|
|
279
|
-
* @returns Response DTO indicating whether user can set password
|
|
280
|
-
*
|
|
281
|
-
* @example
|
|
282
|
-
* ```typescript
|
|
283
|
-
* const dto = { userId: 'user-uuid' };
|
|
284
|
-
* const result = await socialAuthService.canSetPassword(dto);
|
|
285
|
-
* if (result.canSetPassword) {
|
|
286
|
-
* // Allow user to set password
|
|
287
|
-
* }
|
|
288
|
-
* ```
|
|
289
|
-
*/
|
|
290
|
-
async canSetPassword(dto: CanSetPasswordDTO): Promise<CanSetPasswordResponseDTO> {
|
|
291
|
-
const { userId } = dto;
|
|
292
|
-
const user = (await this.userRepository.findOne({ where: { sub: userId } })) as IUser | null;
|
|
293
|
-
if (!user) {
|
|
294
|
-
return { canSetPassword: false };
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// User can set password if they don't have one (social-only account)
|
|
298
|
-
return { canSetPassword: !user.passwordHash };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Set password for social-only user
|
|
303
|
-
*
|
|
304
|
-
* @param dto - Request DTO containing userId and password
|
|
305
|
-
* @returns Response DTO with success message
|
|
306
|
-
* @throws {NAuthException} NOT_FOUND when user is not found
|
|
307
|
-
* @throws {NAuthException} VALIDATION_FAILED when user already has a password
|
|
308
|
-
*
|
|
309
|
-
* @example
|
|
310
|
-
* ```typescript
|
|
311
|
-
* const dto = { userId: 'user-uuid', password: 'newpassword' };
|
|
312
|
-
* await socialAuthService.setPasswordForSocialUser(dto);
|
|
313
|
-
* ```
|
|
314
|
-
*/
|
|
315
|
-
async setPasswordForSocialUser(dto: SetPasswordForSocialUserDTO): Promise<SetPasswordForSocialUserResponseDTO> {
|
|
316
|
-
const { userId, password } = dto;
|
|
317
|
-
const user = await this.userRepository.findOne({ where: { sub: userId } });
|
|
318
|
-
if (!user) {
|
|
319
|
-
throw new NAuthException(AuthErrorCode.NOT_FOUND, 'User not found');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (user.passwordHash) {
|
|
323
|
-
throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, 'User already has a password', {
|
|
324
|
-
field: 'password',
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Use AuthService to set password (includes validation and hashing)
|
|
329
|
-
// For social-only users, we bypass old password validation since they don't have one
|
|
330
|
-
// Note: This requires type casting as ChangePasswordRequestDTO requires oldPassword, but
|
|
331
|
-
// the auth service will handle the case where user has no passwordHash
|
|
332
|
-
const changePasswordDto = new ChangePasswordRequestDTO();
|
|
333
|
-
changePasswordDto.sub = userId; // userId is the sub (external UUID) in this context
|
|
334
|
-
changePasswordDto.oldPassword = ''; // Social-only users don't have a password
|
|
335
|
-
changePasswordDto.newPassword = password;
|
|
336
|
-
await this.authService.changePassword(changePasswordDto);
|
|
337
|
-
|
|
338
|
-
return { message: 'Password set successfully' };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Find social account by provider and provider ID
|
|
343
|
-
*
|
|
344
|
-
* @param provider - Provider name (e.g., 'google', 'apple')
|
|
345
|
-
* @param providerId - Provider user ID
|
|
346
|
-
* @returns Social account with user relation, or null
|
|
347
|
-
* @internal - For use by BaseSocialAuthProviderService
|
|
348
|
-
*/
|
|
349
|
-
async findSocialAccountByProvider(provider: string, providerId: string): Promise<ISocialAccount | null> {
|
|
350
|
-
return (await this.socialAccountRepository.findOne({
|
|
351
|
-
where: { provider, providerId },
|
|
352
|
-
relations: ['user'],
|
|
353
|
-
})) as ISocialAccount | null;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Find social account by user ID and provider
|
|
358
|
-
*
|
|
359
|
-
* @param userId - User ID (internal)
|
|
360
|
-
* @param provider - Provider name
|
|
361
|
-
* @returns Social account or null
|
|
362
|
-
* @internal - For use by BaseSocialAuthProviderService
|
|
363
|
-
*/
|
|
364
|
-
async findSocialAccountByUser(userId: number, provider: string): Promise<ISocialAccount | null> {
|
|
365
|
-
return (await this.socialAccountRepository.findOne({
|
|
366
|
-
where: { userId, provider },
|
|
367
|
-
})) as ISocialAccount | null;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Create or update social account
|
|
372
|
-
*
|
|
373
|
-
* @param userId - User ID (internal)
|
|
374
|
-
* @param provider - Provider name
|
|
375
|
-
* @param providerId - Provider user ID
|
|
376
|
-
* @param providerEmail - Provider email
|
|
377
|
-
* @param metadata - Optional raw profile data
|
|
378
|
-
* @internal - For use by BaseSocialAuthProviderService
|
|
379
|
-
*/
|
|
380
|
-
async createOrUpdateSocialAccount(
|
|
381
|
-
userId: number,
|
|
382
|
-
provider: string,
|
|
383
|
-
providerId: string,
|
|
384
|
-
providerEmail?: string | null,
|
|
385
|
-
metadata?: any,
|
|
386
|
-
): Promise<void> {
|
|
387
|
-
const existingAccount = await this.findSocialAccountByUser(userId, provider);
|
|
388
|
-
|
|
389
|
-
if (existingAccount) {
|
|
390
|
-
// Update existing account
|
|
391
|
-
existingAccount.providerEmail = providerEmail || null;
|
|
392
|
-
existingAccount.lastUsedAt = new Date();
|
|
393
|
-
existingAccount.metadata = metadata || null;
|
|
394
|
-
await this.socialAccountRepository.save(existingAccount);
|
|
395
|
-
} else {
|
|
396
|
-
// Create new account
|
|
397
|
-
const socialAccount = this.socialAccountRepository.create({
|
|
398
|
-
userId,
|
|
399
|
-
provider,
|
|
400
|
-
providerId,
|
|
401
|
-
providerEmail: providerEmail || null,
|
|
402
|
-
linkedAt: new Date(),
|
|
403
|
-
lastUsedAt: new Date(),
|
|
404
|
-
metadata: metadata || null,
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
await this.socialAccountRepository.save(socialAccount);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Update user's social auth flags
|
|
411
|
-
await this.updateUserSocialFlags(userId);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Update user's social authentication flags
|
|
416
|
-
*
|
|
417
|
-
* @param userId - User ID (internal)
|
|
418
|
-
* @internal - For use by BaseSocialAuthProviderService
|
|
419
|
-
*/
|
|
420
|
-
async updateUserSocialFlags(userId: number): Promise<void> {
|
|
421
|
-
const socialAccounts = (await this.socialAccountRepository.find({
|
|
422
|
-
where: { userId },
|
|
423
|
-
})) as ISocialAccount[];
|
|
424
|
-
|
|
425
|
-
const providers = socialAccounts?.map((account) => account.provider) || [];
|
|
426
|
-
const hasSocialAuth = socialAccounts && socialAccounts.length > 0;
|
|
427
|
-
|
|
428
|
-
// Use save() instead of update() to ensure TypeORM properly serializes simple-array fields
|
|
429
|
-
const user = await this.userRepository.findOne({ where: { id: userId } });
|
|
430
|
-
if (user) {
|
|
431
|
-
user.hasSocialAuth = hasSocialAuth;
|
|
432
|
-
user.socialProviders = providers.length > 0 ? providers : null;
|
|
433
|
-
await this.userRepository.save(user);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|