@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.
- package/build/cli.js +0 -0
- package/build/index.js +0 -0
- package/build/tools/group-tools.d.ts.map +1 -1
- package/build/tools/group-tools.js +3 -3
- package/build/tools/group-tools.js.map +1 -1
- package/build/tools/user-tools.d.ts.map +1 -1
- package/build/tools/user-tools.js +10 -8
- package/build/tools/user-tools.js.map +1 -1
- package/package.json +1 -1
- package/build/AzureB2CService.d.ts +0 -246
- package/build/AzureB2CService.d.ts.map +0 -1
- package/build/AzureB2CService.js +0 -876
- package/build/AzureB2CService.js.map +0 -1
package/build/AzureB2CService.js
DELETED
|
@@ -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
|