@mcp-consultant-tools/azure-b2c 33.0.0 → 35.0.0-beta.1

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.
@@ -1,876 +0,0 @@
1
- /**
2
- * Azure AD B2C Integration
3
- *
4
- * Provides user management capabilities for Azure AD B2C tenants via Microsoft Graph API.
5
- * Supports user listing, search, password management, and group operations.
6
- *
7
- * Security Model:
8
- * - Read-only operations: Always enabled (list, get, search users/groups)
9
- * - Password operations: Requires AZURE_B2C_ENABLE_PASSWORD_RESET=true
10
- * - User creation: Requires AZURE_B2C_ENABLE_USER_CREATE=true
11
- * - User deletion: Requires AZURE_B2C_ENABLE_USER_DELETE=true
12
- *
13
- * Authentication:
14
- * - Uses Microsoft Graph API with client credentials flow
15
- * - Requires app registration with User.ReadWrite.All permission
16
- * - App must have "User Administrator" role for password operations
17
- */
18
- import { ClientSecretCredential } from '@azure/identity';
19
- import { Client } from '@microsoft/microsoft-graph-client';
20
- import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials/index.js';
21
- import { auditLogger } from '@mcp-consultant-tools/core';
22
- // ============================================================================
23
- // AzureB2CService Class
24
- // ============================================================================
25
- export class AzureB2CService {
26
- config;
27
- graphClient = null;
28
- credential = null;
29
- // Cache for user/group lists
30
- usersCache = null;
31
- groupsCache = null;
32
- constructor(config) {
33
- // Apply defaults
34
- this.config = {
35
- maxResults: 100,
36
- cacheUsersTTL: 300,
37
- ...config,
38
- };
39
- // Validate required fields
40
- if (!this.config.tenantId || !this.config.clientId || !this.config.clientSecret) {
41
- throw new Error('Azure B2C requires tenantId, clientId, and clientSecret configuration');
42
- }
43
- }
44
- // ==========================================================================
45
- // Authentication / Client Setup
46
- // ==========================================================================
47
- /**
48
- * Get or create the Microsoft Graph client
49
- */
50
- getClient() {
51
- if (!this.graphClient) {
52
- // Create credential
53
- this.credential = new ClientSecretCredential(this.config.tenantId, this.config.clientId, this.config.clientSecret);
54
- // Create auth provider
55
- const authProvider = new TokenCredentialAuthenticationProvider(this.credential, {
56
- scopes: ['https://graph.microsoft.com/.default'],
57
- });
58
- // Create Graph client
59
- this.graphClient = Client.initWithMiddleware({
60
- authProvider,
61
- });
62
- }
63
- return this.graphClient;
64
- }
65
- // ==========================================================================
66
- // Read-Only User Operations (Always Enabled)
67
- // ==========================================================================
68
- /**
69
- * List all users with pagination
70
- * @param top Maximum number of users to return
71
- * @param filter OData filter expression
72
- * @param skipCache Skip the cache and fetch fresh data
73
- * @param includeAllFields Return raw Graph API response with all fields (including extension attributes)
74
- */
75
- async listUsers(top = 50, filter, skipCache = false, includeAllFields = false) {
76
- const timer = auditLogger.startTimer();
77
- // Check cache if not skipping (only for mapped responses)
78
- if (!skipCache && !filter && !includeAllFields && this.usersCache && this.usersCache.expires > Date.now()) {
79
- console.error('Returning cached user list');
80
- return this.usersCache.data.slice(0, top);
81
- }
82
- try {
83
- const client = this.getClient();
84
- const limit = Math.min(top, this.config.maxResults);
85
- let request = client
86
- .api('/users')
87
- .top(limit);
88
- // Only apply .select() if not requesting all fields
89
- if (!includeAllFields) {
90
- request = request.select([
91
- 'id',
92
- 'displayName',
93
- 'givenName',
94
- 'surname',
95
- 'userPrincipalName',
96
- 'mail',
97
- 'otherMails',
98
- 'identities',
99
- 'accountEnabled',
100
- 'createdDateTime',
101
- 'jobTitle',
102
- 'department',
103
- 'mobilePhone',
104
- 'city',
105
- 'country',
106
- ]);
107
- }
108
- if (filter) {
109
- request = request.filter(filter);
110
- }
111
- const response = await request.get();
112
- // Return raw or mapped based on flag
113
- if (includeAllFields) {
114
- auditLogger.log({
115
- operation: 'list-users',
116
- operationType: 'READ',
117
- componentType: 'User',
118
- parameters: { top: limit, filter: filter || 'none', includeAllFields: true },
119
- success: true,
120
- executionTimeMs: timer(),
121
- });
122
- return response.value;
123
- }
124
- const users = this.mapUsersResponse(response.value);
125
- // Cache if no filter
126
- if (!filter) {
127
- this.usersCache = {
128
- data: users,
129
- expires: Date.now() + this.config.cacheUsersTTL * 1000,
130
- };
131
- }
132
- auditLogger.log({
133
- operation: 'list-users',
134
- operationType: 'READ',
135
- componentType: 'User',
136
- parameters: { top: limit, filter: filter || 'none', includeAllFields: false },
137
- success: true,
138
- executionTimeMs: timer(),
139
- });
140
- return users;
141
- }
142
- catch (error) {
143
- auditLogger.log({
144
- operation: 'list-users',
145
- operationType: 'READ',
146
- componentType: 'User',
147
- success: false,
148
- error: error.message,
149
- executionTimeMs: timer(),
150
- });
151
- throw this.enhanceError(error, 'list users');
152
- }
153
- }
154
- /**
155
- * Get user by ID or email
156
- * @param userIdOrEmail User ID (GUID) or email address
157
- * @param includeAllFields Return raw Graph API response with all fields (including extension attributes)
158
- */
159
- async getUser(userIdOrEmail, includeAllFields = false) {
160
- const timer = auditLogger.startTimer();
161
- try {
162
- const client = this.getClient();
163
- let request = client.api(`/users/${userIdOrEmail}`);
164
- // Only apply .select() if not requesting all fields
165
- if (!includeAllFields) {
166
- request = request.select([
167
- 'id',
168
- 'displayName',
169
- 'givenName',
170
- 'surname',
171
- 'userPrincipalName',
172
- 'mail',
173
- 'otherMails',
174
- 'identities',
175
- 'accountEnabled',
176
- 'createdDateTime',
177
- 'jobTitle',
178
- 'department',
179
- 'mobilePhone',
180
- 'city',
181
- 'country',
182
- ]);
183
- }
184
- const response = await request.get();
185
- // Return raw or mapped based on flag
186
- if (includeAllFields) {
187
- auditLogger.log({
188
- operation: 'get-user',
189
- operationType: 'READ',
190
- componentType: 'User',
191
- componentName: userIdOrEmail,
192
- parameters: { includeAllFields: true },
193
- success: true,
194
- executionTimeMs: timer(),
195
- });
196
- return response;
197
- }
198
- const user = this.mapUserResponse(response);
199
- auditLogger.log({
200
- operation: 'get-user',
201
- operationType: 'READ',
202
- componentType: 'User',
203
- componentName: userIdOrEmail,
204
- parameters: { includeAllFields: false },
205
- success: true,
206
- executionTimeMs: timer(),
207
- });
208
- return user;
209
- }
210
- catch (error) {
211
- auditLogger.log({
212
- operation: 'get-user',
213
- operationType: 'READ',
214
- componentType: 'User',
215
- componentName: userIdOrEmail,
216
- success: false,
217
- error: error.message,
218
- executionTimeMs: timer(),
219
- });
220
- throw this.enhanceError(error, 'get user');
221
- }
222
- }
223
- /**
224
- * Search users by display name, email, or other criteria
225
- * @param searchTerm Term to search for
226
- * @param searchFields Fields to search in
227
- * @param top Maximum results to return
228
- * @param includeAllFields Return raw Graph API response with all fields (including extension attributes)
229
- */
230
- async searchUsers(searchTerm, searchFields = ['displayName', 'mail'], top = 25, includeAllFields = false) {
231
- const timer = auditLogger.startTimer();
232
- try {
233
- const client = this.getClient();
234
- const limit = Math.min(top, this.config.maxResults);
235
- // Build filter using startswith for each field
236
- const filters = searchFields.map((field) => `startswith(${field}, '${searchTerm.replace(/'/g, "''")}')`);
237
- const filterString = filters.join(' or ');
238
- let request = client
239
- .api('/users')
240
- .top(limit)
241
- .filter(filterString);
242
- // Only apply .select() if not requesting all fields
243
- if (!includeAllFields) {
244
- request = request.select([
245
- 'id',
246
- 'displayName',
247
- 'givenName',
248
- 'surname',
249
- 'userPrincipalName',
250
- 'mail',
251
- 'otherMails',
252
- 'identities',
253
- 'accountEnabled',
254
- 'createdDateTime',
255
- ]);
256
- }
257
- const response = await request.get();
258
- // Return raw or mapped based on flag
259
- if (includeAllFields) {
260
- auditLogger.log({
261
- operation: 'search-users',
262
- operationType: 'READ',
263
- componentType: 'User',
264
- parameters: { searchTerm, searchFields, resultCount: response.value.length, includeAllFields: true },
265
- success: true,
266
- executionTimeMs: timer(),
267
- });
268
- return response.value;
269
- }
270
- const users = this.mapUsersResponse(response.value);
271
- auditLogger.log({
272
- operation: 'search-users',
273
- operationType: 'READ',
274
- componentType: 'User',
275
- parameters: { searchTerm, searchFields, resultCount: users.length, includeAllFields: false },
276
- success: true,
277
- executionTimeMs: timer(),
278
- });
279
- return users;
280
- }
281
- catch (error) {
282
- auditLogger.log({
283
- operation: 'search-users',
284
- operationType: 'READ',
285
- componentType: 'User',
286
- parameters: { searchTerm },
287
- success: false,
288
- error: error.message,
289
- executionTimeMs: timer(),
290
- });
291
- throw this.enhanceError(error, 'search users');
292
- }
293
- }
294
- // ==========================================================================
295
- // Read-Only Group Operations (Always Enabled)
296
- // ==========================================================================
297
- /**
298
- * List all groups
299
- */
300
- async listGroups(top = 50) {
301
- const timer = auditLogger.startTimer();
302
- // Check cache
303
- if (this.groupsCache && this.groupsCache.expires > Date.now()) {
304
- console.error('Returning cached group list');
305
- return this.groupsCache.data.slice(0, top);
306
- }
307
- try {
308
- const client = this.getClient();
309
- const limit = Math.min(top, this.config.maxResults);
310
- const response = await client
311
- .api('/groups')
312
- .top(limit)
313
- .select(['id', 'displayName', 'description', 'mailEnabled', 'securityEnabled'])
314
- .get();
315
- const groups = this.mapGroupsResponse(response.value);
316
- // Cache the result
317
- this.groupsCache = {
318
- data: groups,
319
- expires: Date.now() + this.config.cacheUsersTTL * 1000,
320
- };
321
- auditLogger.log({
322
- operation: 'list-groups',
323
- operationType: 'READ',
324
- componentType: 'Group',
325
- parameters: { top: limit, groupCount: groups.length },
326
- success: true,
327
- executionTimeMs: timer(),
328
- });
329
- return groups;
330
- }
331
- catch (error) {
332
- auditLogger.log({
333
- operation: 'list-groups',
334
- operationType: 'READ',
335
- componentType: 'Group',
336
- success: false,
337
- error: error.message,
338
- executionTimeMs: timer(),
339
- });
340
- throw this.enhanceError(error, 'list groups');
341
- }
342
- }
343
- /**
344
- * Get groups a user belongs to
345
- */
346
- async getUserGroups(userId) {
347
- const timer = auditLogger.startTimer();
348
- try {
349
- const client = this.getClient();
350
- const response = await client
351
- .api(`/users/${userId}/memberOf`)
352
- .select(['id', 'displayName', 'description', 'mailEnabled', 'securityEnabled'])
353
- .get();
354
- // Filter to only groups (memberOf can include roles too)
355
- const groups = response.value
356
- .filter((item) => item['@odata.type'] === '#microsoft.graph.group')
357
- .map((g) => this.mapGroupResponse(g));
358
- auditLogger.log({
359
- operation: 'get-user-groups',
360
- operationType: 'READ',
361
- componentType: 'Group',
362
- componentName: userId,
363
- parameters: { groupCount: groups.length },
364
- success: true,
365
- executionTimeMs: timer(),
366
- });
367
- return groups;
368
- }
369
- catch (error) {
370
- auditLogger.log({
371
- operation: 'get-user-groups',
372
- operationType: 'READ',
373
- componentType: 'Group',
374
- componentName: userId,
375
- success: false,
376
- error: error.message,
377
- executionTimeMs: timer(),
378
- });
379
- throw this.enhanceError(error, 'get user groups');
380
- }
381
- }
382
- /**
383
- * Get members of a group
384
- * @param groupId Group ID (GUID)
385
- * @param top Maximum members to return
386
- * @param includeAllFields Return raw Graph API response with all fields (including extension attributes)
387
- */
388
- async getGroupMembers(groupId, top = 50, includeAllFields = false) {
389
- const timer = auditLogger.startTimer();
390
- try {
391
- const client = this.getClient();
392
- const limit = Math.min(top, this.config.maxResults);
393
- let request = client
394
- .api(`/groups/${groupId}/members`)
395
- .top(limit);
396
- // Only apply .select() if not requesting all fields
397
- if (!includeAllFields) {
398
- request = request.select([
399
- 'id',
400
- 'displayName',
401
- 'givenName',
402
- 'surname',
403
- 'userPrincipalName',
404
- 'mail',
405
- 'accountEnabled',
406
- ]);
407
- }
408
- const response = await request.get();
409
- // Filter to only users
410
- const userItems = response.value.filter((item) => item['@odata.type'] === '#microsoft.graph.user');
411
- // Return raw or mapped based on flag
412
- if (includeAllFields) {
413
- auditLogger.log({
414
- operation: 'get-group-members',
415
- operationType: 'READ',
416
- componentType: 'Group',
417
- componentName: groupId,
418
- parameters: { memberCount: userItems.length, includeAllFields: true },
419
- success: true,
420
- executionTimeMs: timer(),
421
- });
422
- return userItems;
423
- }
424
- const users = userItems.map((u) => this.mapUserResponse(u));
425
- auditLogger.log({
426
- operation: 'get-group-members',
427
- operationType: 'READ',
428
- componentType: 'Group',
429
- componentName: groupId,
430
- parameters: { memberCount: users.length, includeAllFields: false },
431
- success: true,
432
- executionTimeMs: timer(),
433
- });
434
- return users;
435
- }
436
- catch (error) {
437
- auditLogger.log({
438
- operation: 'get-group-members',
439
- operationType: 'READ',
440
- componentType: 'Group',
441
- componentName: groupId,
442
- success: false,
443
- error: error.message,
444
- executionTimeMs: timer(),
445
- });
446
- throw this.enhanceError(error, 'get group members');
447
- }
448
- }
449
- // ==========================================================================
450
- // Password Operations (Requires enablePasswordReset=true)
451
- // ==========================================================================
452
- /**
453
- * Reset user password
454
- * Requires: AZURE_B2C_ENABLE_PASSWORD_RESET=true
455
- */
456
- async resetUserPassword(userId, newPassword, forceChangeOnNextLogin = false) {
457
- this.checkPermission('password reset', this.config.enablePasswordReset);
458
- const timer = auditLogger.startTimer();
459
- try {
460
- const client = this.getClient();
461
- await client.api(`/users/${userId}`).update({
462
- passwordProfile: {
463
- password: newPassword,
464
- forceChangePasswordNextSignIn: forceChangeOnNextLogin,
465
- },
466
- });
467
- auditLogger.log({
468
- operation: 'reset-password',
469
- operationType: 'UPDATE',
470
- componentType: 'User',
471
- componentName: userId,
472
- parameters: { forceChangeOnNextLogin },
473
- success: true,
474
- executionTimeMs: timer(),
475
- });
476
- // Invalidate user cache
477
- this.usersCache = null;
478
- }
479
- catch (error) {
480
- auditLogger.log({
481
- operation: 'reset-password',
482
- operationType: 'UPDATE',
483
- componentType: 'User',
484
- componentName: userId,
485
- success: false,
486
- error: error.message,
487
- executionTimeMs: timer(),
488
- });
489
- throw this.enhanceError(error, 'reset password');
490
- }
491
- }
492
- /**
493
- * Force password change on next login
494
- * Requires: AZURE_B2C_ENABLE_PASSWORD_RESET=true
495
- */
496
- async forcePasswordChange(userId) {
497
- this.checkPermission('password reset', this.config.enablePasswordReset);
498
- const timer = auditLogger.startTimer();
499
- try {
500
- const client = this.getClient();
501
- await client.api(`/users/${userId}`).update({
502
- passwordProfile: {
503
- forceChangePasswordNextSignIn: true,
504
- },
505
- });
506
- auditLogger.log({
507
- operation: 'force-password-change',
508
- operationType: 'UPDATE',
509
- componentType: 'User',
510
- componentName: userId,
511
- success: true,
512
- executionTimeMs: timer(),
513
- });
514
- // Invalidate user cache
515
- this.usersCache = null;
516
- }
517
- catch (error) {
518
- auditLogger.log({
519
- operation: 'force-password-change',
520
- operationType: 'UPDATE',
521
- componentType: 'User',
522
- componentName: userId,
523
- success: false,
524
- error: error.message,
525
- executionTimeMs: timer(),
526
- });
527
- throw this.enhanceError(error, 'force password change');
528
- }
529
- }
530
- // ==========================================================================
531
- // User Creation Operations (Requires enableUserCreate=true)
532
- // ==========================================================================
533
- /**
534
- * Create a new local account user
535
- * Requires: AZURE_B2C_ENABLE_USER_CREATE=true
536
- */
537
- async createUser(request) {
538
- this.checkPermission('user creation', this.config.enableUserCreate);
539
- const timer = auditLogger.startTimer();
540
- try {
541
- const client = this.getClient();
542
- const response = await client.api('/users').post({
543
- displayName: request.displayName,
544
- identities: request.identities,
545
- passwordProfile: request.passwordProfile,
546
- givenName: request.givenName,
547
- surname: request.surname,
548
- jobTitle: request.jobTitle,
549
- department: request.department,
550
- mobilePhone: request.mobilePhone,
551
- city: request.city,
552
- country: request.country,
553
- });
554
- const user = this.mapUserResponse(response);
555
- auditLogger.log({
556
- operation: 'create-user',
557
- operationType: 'CREATE',
558
- componentType: 'User',
559
- componentName: request.displayName,
560
- success: true,
561
- executionTimeMs: timer(),
562
- });
563
- // Invalidate user cache
564
- this.usersCache = null;
565
- return user;
566
- }
567
- catch (error) {
568
- auditLogger.log({
569
- operation: 'create-user',
570
- operationType: 'CREATE',
571
- componentType: 'User',
572
- componentName: request.displayName,
573
- success: false,
574
- error: error.message,
575
- executionTimeMs: timer(),
576
- });
577
- throw this.enhanceError(error, 'create user');
578
- }
579
- }
580
- /**
581
- * Update user profile (non-password fields)
582
- * Requires: AZURE_B2C_ENABLE_USER_UPDATE=true
583
- */
584
- async updateUser(userId, updates) {
585
- this.checkPermission('user update', this.config.enableUserUpdate);
586
- const timer = auditLogger.startTimer();
587
- try {
588
- const client = this.getClient();
589
- await client.api(`/users/${userId}`).update(updates);
590
- // Fetch updated user
591
- const user = await this.getUser(userId);
592
- auditLogger.log({
593
- operation: 'update-user',
594
- operationType: 'UPDATE',
595
- componentType: 'User',
596
- componentName: userId,
597
- parameters: { updatedFields: Object.keys(updates) },
598
- success: true,
599
- executionTimeMs: timer(),
600
- });
601
- // Invalidate user cache
602
- this.usersCache = null;
603
- return user;
604
- }
605
- catch (error) {
606
- auditLogger.log({
607
- operation: 'update-user',
608
- operationType: 'UPDATE',
609
- componentType: 'User',
610
- componentName: userId,
611
- success: false,
612
- error: error.message,
613
- executionTimeMs: timer(),
614
- });
615
- throw this.enhanceError(error, 'update user');
616
- }
617
- }
618
- // ==========================================================================
619
- // User Deletion Operations (Requires enableUserDelete=true)
620
- // ==========================================================================
621
- /**
622
- * Delete a user (irreversible)
623
- * Requires: AZURE_B2C_ENABLE_USER_DELETE=true
624
- */
625
- async deleteUser(userId) {
626
- this.checkPermission('user deletion', this.config.enableUserDelete);
627
- const timer = auditLogger.startTimer();
628
- try {
629
- const client = this.getClient();
630
- await client.api(`/users/${userId}`).delete();
631
- auditLogger.log({
632
- operation: 'delete-user',
633
- operationType: 'DELETE',
634
- componentType: 'User',
635
- componentName: userId,
636
- success: true,
637
- executionTimeMs: timer(),
638
- });
639
- // Invalidate user cache
640
- this.usersCache = null;
641
- }
642
- catch (error) {
643
- auditLogger.log({
644
- operation: 'delete-user',
645
- operationType: 'DELETE',
646
- componentType: 'User',
647
- componentName: userId,
648
- success: false,
649
- error: error.message,
650
- executionTimeMs: timer(),
651
- });
652
- throw this.enhanceError(error, 'delete user');
653
- }
654
- }
655
- // ==========================================================================
656
- // Utility Methods
657
- // ==========================================================================
658
- /**
659
- * Get tenant summary (user/group counts)
660
- */
661
- async getTenantSummary() {
662
- const timer = auditLogger.startTimer();
663
- try {
664
- const [users, groups] = await Promise.all([
665
- this.listUsers(1000), // Get up to 1000 users for counting
666
- this.listGroups(1000),
667
- ]);
668
- const enabledUsers = users.filter((u) => u.accountEnabled);
669
- const disabledUsers = users.filter((u) => !u.accountEnabled);
670
- // Count by identity type
671
- let localAccountCount = 0;
672
- let federatedAccountCount = 0;
673
- for (const user of users) {
674
- if (user.identities) {
675
- const hasLocal = user.identities.some((i) => i.signInType === 'emailAddress' || i.signInType === 'userName');
676
- const hasFederated = user.identities.some((i) => i.signInType === 'federated');
677
- if (hasLocal)
678
- localAccountCount++;
679
- if (hasFederated)
680
- federatedAccountCount++;
681
- }
682
- }
683
- const summary = {
684
- tenantId: this.config.tenantId,
685
- userCount: users.length,
686
- groupCount: groups.length,
687
- enabledUserCount: enabledUsers.length,
688
- disabledUserCount: disabledUsers.length,
689
- localAccountCount,
690
- federatedAccountCount,
691
- };
692
- auditLogger.log({
693
- operation: 'get-tenant-summary',
694
- operationType: 'READ',
695
- componentType: 'Tenant',
696
- success: true,
697
- executionTimeMs: timer(),
698
- });
699
- return summary;
700
- }
701
- catch (error) {
702
- auditLogger.log({
703
- operation: 'get-tenant-summary',
704
- operationType: 'READ',
705
- componentType: 'Tenant',
706
- success: false,
707
- error: error.message,
708
- executionTimeMs: timer(),
709
- });
710
- throw this.enhanceError(error, 'get tenant summary');
711
- }
712
- }
713
- /**
714
- * Test connection to the B2C tenant
715
- */
716
- async testConnection() {
717
- const timer = auditLogger.startTimer();
718
- try {
719
- let canReadUsers = false;
720
- let canReadGroups = false;
721
- // Test user read
722
- try {
723
- await this.listUsers(1);
724
- canReadUsers = true;
725
- }
726
- catch (e) {
727
- console.error(`Cannot read users: ${e.message}`);
728
- }
729
- // Test group read
730
- try {
731
- await this.listGroups(1);
732
- canReadGroups = true;
733
- }
734
- catch (e) {
735
- console.error(`Cannot read groups: ${e.message}`);
736
- }
737
- auditLogger.log({
738
- operation: 'test-connection',
739
- operationType: 'READ',
740
- componentType: 'Tenant',
741
- success: true,
742
- executionTimeMs: timer(),
743
- });
744
- return {
745
- connected: canReadUsers || canReadGroups,
746
- tenantId: this.config.tenantId,
747
- canReadUsers,
748
- canReadGroups,
749
- };
750
- }
751
- catch (error) {
752
- auditLogger.log({
753
- operation: 'test-connection',
754
- operationType: 'READ',
755
- componentType: 'Tenant',
756
- success: false,
757
- error: error.message,
758
- executionTimeMs: timer(),
759
- });
760
- return {
761
- connected: false,
762
- tenantId: this.config.tenantId,
763
- canReadUsers: false,
764
- canReadGroups: false,
765
- error: error.message,
766
- };
767
- }
768
- }
769
- /**
770
- * Get current configuration status
771
- */
772
- getConfigStatus() {
773
- return {
774
- tenantId: this.config.tenantId,
775
- enablePasswordReset: this.config.enablePasswordReset,
776
- enableUserCreate: this.config.enableUserCreate,
777
- enableUserUpdate: this.config.enableUserUpdate,
778
- enableUserDelete: this.config.enableUserDelete,
779
- };
780
- }
781
- // ==========================================================================
782
- // Helper Methods
783
- // ==========================================================================
784
- /**
785
- * Check if operation is permitted
786
- */
787
- checkPermission(operation, enabled) {
788
- if (!enabled) {
789
- throw new Error(`${operation} is not enabled. ` +
790
- `Set the appropriate environment variable to enable this operation.`);
791
- }
792
- }
793
- /**
794
- * Map Graph API user response to B2CUser
795
- */
796
- mapUserResponse(response) {
797
- return {
798
- id: response.id,
799
- displayName: response.displayName,
800
- givenName: response.givenName,
801
- surname: response.surname,
802
- userPrincipalName: response.userPrincipalName,
803
- mail: response.mail,
804
- otherMails: response.otherMails,
805
- identities: response.identities,
806
- accountEnabled: response.accountEnabled,
807
- createdDateTime: response.createdDateTime,
808
- jobTitle: response.jobTitle,
809
- department: response.department,
810
- mobilePhone: response.mobilePhone,
811
- city: response.city,
812
- country: response.country,
813
- };
814
- }
815
- /**
816
- * Map array of Graph API user responses
817
- */
818
- mapUsersResponse(responses) {
819
- return responses.map((r) => this.mapUserResponse(r));
820
- }
821
- /**
822
- * Map Graph API group response to B2CGroup
823
- */
824
- mapGroupResponse(response) {
825
- return {
826
- id: response.id,
827
- displayName: response.displayName,
828
- description: response.description,
829
- mailEnabled: response.mailEnabled,
830
- securityEnabled: response.securityEnabled,
831
- };
832
- }
833
- /**
834
- * Map array of Graph API group responses
835
- */
836
- mapGroupsResponse(responses) {
837
- return responses.map((r) => this.mapGroupResponse(r));
838
- }
839
- /**
840
- * Enhance error with helpful context
841
- */
842
- enhanceError(error, operation) {
843
- const message = error.message || String(error);
844
- // Handle common Graph API errors
845
- if (error.statusCode === 401 || message.includes('Unauthorized')) {
846
- return new Error(`Unauthorized to ${operation}. ` +
847
- `Verify app registration has correct API permissions (User.ReadWrite.All) ` +
848
- `and "User Administrator" role assignment. ` +
849
- `Original error: ${message}`);
850
- }
851
- if (error.statusCode === 403 || message.includes('Forbidden')) {
852
- return new Error(`Forbidden to ${operation}. ` +
853
- `The app may lack required permissions or role assignments. ` +
854
- `Original error: ${message}`);
855
- }
856
- if (error.statusCode === 404 || message.includes('Request_ResourceNotFound')) {
857
- return new Error(`Resource not found when attempting to ${operation}. ` +
858
- `Verify the user/group ID is correct. ` +
859
- `Original error: ${message}`);
860
- }
861
- if (message.includes('Invalid password')) {
862
- return new Error(`Invalid password format. Password must meet Azure AD B2C complexity requirements: ` +
863
- `8-256 characters, at least 3 of: lowercase, uppercase, digit, symbol. ` +
864
- `Original error: ${message}`);
865
- }
866
- return new Error(`Failed to ${operation}: ${message}`);
867
- }
868
- /**
869
- * Clear all caches
870
- */
871
- clearCache() {
872
- this.usersCache = null;
873
- this.groupsCache = null;
874
- }
875
- }
876
- //# sourceMappingURL=AzureB2CService.js.map