elsabro 2.2.0 → 3.7.0

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.
Files changed (88) hide show
  1. package/README.md +668 -20
  2. package/agents/elsabro-orchestrator.md +113 -0
  3. package/bin/install.js +0 -0
  4. package/commands/elsabro/execute.md +223 -46
  5. package/commands/elsabro/start.md +34 -0
  6. package/commands/elsabro/verify-work.md +29 -0
  7. package/flows/development-flow.json +452 -0
  8. package/flows/quick-flow.json +118 -0
  9. package/hooks/confirm-destructive.sh +145 -0
  10. package/hooks/hooks-config.json +81 -0
  11. package/hooks/lint-check.sh +238 -0
  12. package/hooks/post-edit-test.sh +189 -0
  13. package/package.json +5 -3
  14. package/references/SYSTEM_INDEX.md +379 -5
  15. package/references/agent-marketplace.md +2274 -0
  16. package/references/agent-protocol.md +1126 -0
  17. package/references/ai-code-suggestions.md +2413 -0
  18. package/references/checkpointing.md +595 -0
  19. package/references/collaboration-patterns.md +851 -0
  20. package/references/collaborative-sessions.md +1081 -0
  21. package/references/configuration-management.md +1810 -0
  22. package/references/cost-tracking.md +1095 -0
  23. package/references/enterprise-sso.md +2001 -0
  24. package/references/error-contracts-tests.md +1171 -0
  25. package/references/error-contracts-v2.md +968 -0
  26. package/references/error-contracts.md +3102 -0
  27. package/references/event-driven.md +1031 -0
  28. package/references/flow-orchestration.md +940 -0
  29. package/references/flow-visualization.md +1557 -0
  30. package/references/ide-integrations.md +3513 -0
  31. package/references/interrupt-system.md +681 -0
  32. package/references/kubernetes-deployment.md +3099 -0
  33. package/references/memory-system.md +683 -0
  34. package/references/mobile-companion.md +3236 -0
  35. package/references/multi-llm-providers.md +2494 -0
  36. package/references/multi-project-memory.md +1182 -0
  37. package/references/observability.md +793 -0
  38. package/references/output-schemas.md +858 -0
  39. package/references/parallel-worktrees.md +293 -0
  40. package/references/performance-profiler.md +955 -0
  41. package/references/plugin-system.md +1526 -0
  42. package/references/prompt-management.md +292 -0
  43. package/references/sandbox-execution.md +303 -0
  44. package/references/security-system.md +1253 -0
  45. package/references/streaming.md +696 -0
  46. package/references/testing-framework.md +1151 -0
  47. package/references/time-travel.md +802 -0
  48. package/references/tool-registry.md +886 -0
  49. package/references/voice-commands.md +3296 -0
  50. package/scripts/setup-parallel-worktrees.sh +319 -0
  51. package/skills/memory-update.md +207 -0
  52. package/skills/review.md +331 -0
  53. package/skills/techdebt.md +289 -0
  54. package/skills/tutor.md +219 -0
  55. package/templates/.planning/notes/.gitkeep +0 -0
  56. package/templates/CLAUDE.md.template +48 -0
  57. package/templates/agent-marketplace-config.json +220 -0
  58. package/templates/agent-protocol-config.json +136 -0
  59. package/templates/ai-suggestions-config.json +100 -0
  60. package/templates/checkpoint-state.json +61 -0
  61. package/templates/collaboration-config.json +157 -0
  62. package/templates/collaborative-sessions-config.json +153 -0
  63. package/templates/configuration-config.json +245 -0
  64. package/templates/cost-tracking-config.json +148 -0
  65. package/templates/enterprise-sso-config.json +438 -0
  66. package/templates/error-handling-config.json +79 -2
  67. package/templates/events-config.json +148 -0
  68. package/templates/flow-visualization-config.json +196 -0
  69. package/templates/ide-integrations-config.json +442 -0
  70. package/templates/kubernetes-config.json +764 -0
  71. package/templates/memory-state.json +84 -0
  72. package/templates/mistakes.md.template +52 -0
  73. package/templates/mobile-companion-config.json +600 -0
  74. package/templates/multi-llm-config.json +544 -0
  75. package/templates/multi-project-memory-config.json +145 -0
  76. package/templates/observability-config.json +109 -0
  77. package/templates/patterns.md.template +114 -0
  78. package/templates/performance-profiler-config.json +125 -0
  79. package/templates/plugin-config.json +170 -0
  80. package/templates/prompt-management-config.json +86 -0
  81. package/templates/sandbox-config.json +185 -0
  82. package/templates/schemas-config.json +65 -0
  83. package/templates/security-config.json +120 -0
  84. package/templates/streaming-config.json +72 -0
  85. package/templates/testing-config.json +81 -0
  86. package/templates/timetravel-config.json +62 -0
  87. package/templates/tool-registry-config.json +109 -0
  88. package/templates/voice-commands-config.json +658 -0
@@ -0,0 +1,2001 @@
1
+ # Enterprise SSO System (v3.6)
2
+
3
+ Sistema de Single Sign-On empresarial con soporte para SAML 2.0, OpenID Connect, y multiples Identity Providers.
4
+
5
+ ## Arquitectura
6
+
7
+ ```
8
+ +-----------------------------------------------------------------------------+
9
+ | ENTERPRISE SSO SYSTEM |
10
+ +-----------------------------------------------------------------------------+
11
+ | |
12
+ | +---------------------------------------------------------------------+ |
13
+ | | SSO MANAGER | |
14
+ | | +---------------+ +---------------+ +---------------+ | |
15
+ | | | Session | | Token | | Single | | |
16
+ | | | Manager | | Service | | Logout | | |
17
+ | | +---------------+ +---------------+ +---------------+ | |
18
+ | +---------------------------------------------------------------------+ |
19
+ | | |
20
+ | v |
21
+ | +---------------------------------------------------------------------+ |
22
+ | | PROTOCOL PROVIDERS | |
23
+ | | +---------------------------+ +---------------------------+ | |
24
+ | | | SAML PROVIDER | | OIDC PROVIDER | | |
25
+ | | | +-------+ +----------+ | | +-------+ +----------+ | | |
26
+ | | | | SP | | Assertion| | | | Auth | | Token | | | |
27
+ | | | | Meta | | Validate | | | | Code | | Refresh | | | |
28
+ | | | +-------+ +----------+ | | +-------+ +----------+ | | |
29
+ | | +---------------------------+ +---------------------------+ | |
30
+ | +---------------------------------------------------------------------+ |
31
+ | | |
32
+ | v |
33
+ | +---------------------------------------------------------------------+ |
34
+ | | IDENTITY PROVIDERS | |
35
+ | | +--------+ +--------+ +--------+ +--------+ +--------+ | |
36
+ | | | Okta | | Azure | | Google | | Auth0 | |Keycloak| | |
37
+ | | | | | AD | | Work | | | | | | |
38
+ | | +--------+ +--------+ +--------+ +--------+ +--------+ | |
39
+ | +---------------------------------------------------------------------+ |
40
+ | | |
41
+ | v |
42
+ | +---------------------------------------------------------------------+ |
43
+ | | AUTHORIZATION INTEGRATION | |
44
+ | | +---------------+ +---------------+ +---------------+ | |
45
+ | | | Group/Role | | JIT | | SCIM 2.0 | | |
46
+ | | | Mapping | | Provisioning | | User Sync | | |
47
+ | | +---------------+ +---------------+ +---------------+ | |
48
+ | +---------------------------------------------------------------------+ |
49
+ | |
50
+ +-----------------------------------------------------------------------------+
51
+ ```
52
+
53
+ ---
54
+
55
+ ## SSOManager
56
+
57
+ ### API Principal
58
+
59
+ ```typescript
60
+ interface SSOConfig {
61
+ enabled: boolean;
62
+ defaultProvider?: string;
63
+ sessionTimeout: number;
64
+ tokenEncryption: TokenEncryptionConfig;
65
+ singleLogout: boolean;
66
+ mfaRequired: boolean;
67
+ allowedDomains?: string[];
68
+ ipAllowlist?: string[];
69
+ }
70
+
71
+ interface SSOSession {
72
+ id: string;
73
+ userId: string;
74
+ providerId: string;
75
+ protocol: 'saml' | 'oidc';
76
+ accessToken?: string;
77
+ refreshToken?: string;
78
+ idToken?: string;
79
+ expiresAt: string;
80
+ createdAt: string;
81
+ lastActivityAt: string;
82
+ attributes: UserAttributes;
83
+ mfaVerified: boolean;
84
+ ipAddress: string;
85
+ userAgent: string;
86
+ }
87
+
88
+ interface UserAttributes {
89
+ email: string;
90
+ name?: string;
91
+ firstName?: string;
92
+ lastName?: string;
93
+ groups: string[];
94
+ roles: string[];
95
+ department?: string;
96
+ organization?: string;
97
+ customAttributes?: Record<string, unknown>;
98
+ }
99
+
100
+ interface TokenEncryptionConfig {
101
+ algorithm: 'RS256' | 'RS384' | 'RS512' | 'ES256' | 'ES384' | 'ES512';
102
+ publicKey: string;
103
+ privateKey: string;
104
+ keyRotationDays: number;
105
+ }
106
+
107
+ interface AuthenticationResult {
108
+ success: boolean;
109
+ session?: SSOSession;
110
+ redirectUrl?: string;
111
+ error?: SSOError;
112
+ mfaRequired?: boolean;
113
+ mfaChallenge?: MFAChallenge;
114
+ }
115
+
116
+ interface SSOError {
117
+ code: string;
118
+ message: string;
119
+ providerId?: string;
120
+ details?: Record<string, unknown>;
121
+ }
122
+
123
+ interface MFAChallenge {
124
+ type: 'totp' | 'sms' | 'email' | 'push';
125
+ challengeId: string;
126
+ expiresAt: string;
127
+ }
128
+
129
+ class SSOManager {
130
+ private config: SSOConfig;
131
+ private providers: Map<string, IdentityProvider>;
132
+ private sessions: Map<string, SSOSession>;
133
+ private tokenService: TokenService;
134
+
135
+ constructor(config: SSOConfig) {
136
+ this.config = config;
137
+ this.providers = new Map();
138
+ this.sessions = new Map();
139
+ this.tokenService = new TokenService(config.tokenEncryption);
140
+ }
141
+
142
+ // Register identity provider
143
+ registerProvider(provider: IdentityProvider): void {
144
+ this.providers.set(provider.id, provider);
145
+
146
+ AuditLogger.log({
147
+ action: 'sso.provider.registered',
148
+ resource: provider.id,
149
+ details: {
150
+ protocol: provider.protocol,
151
+ name: provider.name
152
+ }
153
+ });
154
+ }
155
+
156
+ // Unregister identity provider
157
+ unregisterProvider(providerId: string): boolean {
158
+ const removed = this.providers.delete(providerId);
159
+
160
+ if (removed) {
161
+ AuditLogger.log({
162
+ action: 'sso.provider.unregistered',
163
+ resource: providerId
164
+ });
165
+ }
166
+
167
+ return removed;
168
+ }
169
+
170
+ // Initiate SSO authentication
171
+ async initiateAuth(
172
+ providerId: string,
173
+ options?: AuthInitOptions
174
+ ): Promise<AuthInitResult> {
175
+ const provider = this.providers.get(providerId);
176
+ if (!provider) {
177
+ throw new SSOError('PROVIDER_NOT_FOUND', `Provider not found: ${providerId}`);
178
+ }
179
+
180
+ // Check IP allowlist
181
+ if (this.config.ipAllowlist && options?.ipAddress) {
182
+ if (!this.isIPAllowed(options.ipAddress)) {
183
+ AuditLogger.denied('sso.auth.ip_blocked', {
184
+ details: { ip: options.ipAddress, providerId }
185
+ });
186
+ throw new SSOError('IP_NOT_ALLOWED', 'IP address not in allowlist');
187
+ }
188
+ }
189
+
190
+ // Generate state for CSRF protection
191
+ const state = this.generateState();
192
+ const nonce = this.generateNonce();
193
+
194
+ // Get auth URL from provider
195
+ const authUrl = await provider.getAuthorizationUrl({
196
+ state,
197
+ nonce,
198
+ redirectUri: options?.redirectUri,
199
+ scope: options?.scope,
200
+ prompt: options?.prompt
201
+ });
202
+
203
+ // Store state for validation
204
+ await this.storeAuthState(state, {
205
+ providerId,
206
+ nonce,
207
+ initiatedAt: new Date().toISOString(),
208
+ ipAddress: options?.ipAddress,
209
+ userAgent: options?.userAgent
210
+ });
211
+
212
+ AuditLogger.log({
213
+ action: 'sso.auth.initiated',
214
+ details: { providerId, protocol: provider.protocol }
215
+ });
216
+
217
+ return {
218
+ authUrl,
219
+ state,
220
+ expiresIn: 600 // 10 minutes
221
+ };
222
+ }
223
+
224
+ // Handle SSO callback
225
+ async handleCallback(
226
+ providerId: string,
227
+ callbackData: CallbackData
228
+ ): Promise<AuthenticationResult> {
229
+ const provider = this.providers.get(providerId);
230
+ if (!provider) {
231
+ return {
232
+ success: false,
233
+ error: { code: 'PROVIDER_NOT_FOUND', message: `Provider not found: ${providerId}` }
234
+ };
235
+ }
236
+
237
+ // Validate state
238
+ const authState = await this.getAuthState(callbackData.state);
239
+ if (!authState || authState.providerId !== providerId) {
240
+ AuditLogger.denied('sso.auth.invalid_state', {
241
+ details: { providerId, state: callbackData.state }
242
+ });
243
+ return {
244
+ success: false,
245
+ error: { code: 'INVALID_STATE', message: 'Invalid or expired state' }
246
+ };
247
+ }
248
+
249
+ try {
250
+ // Process callback based on protocol
251
+ const authResult = await provider.processCallback(callbackData, authState.nonce);
252
+
253
+ // Check domain allowlist
254
+ if (this.config.allowedDomains && authResult.attributes.email) {
255
+ const domain = authResult.attributes.email.split('@')[1];
256
+ if (!this.config.allowedDomains.includes(domain)) {
257
+ AuditLogger.denied('sso.auth.domain_blocked', {
258
+ details: { domain, providerId }
259
+ });
260
+ return {
261
+ success: false,
262
+ error: { code: 'DOMAIN_NOT_ALLOWED', message: 'Email domain not allowed' }
263
+ };
264
+ }
265
+ }
266
+
267
+ // Check if MFA is required
268
+ if (this.config.mfaRequired && !authResult.mfaVerified) {
269
+ const challenge = await this.initiateMFA(authResult);
270
+ return {
271
+ success: false,
272
+ mfaRequired: true,
273
+ mfaChallenge: challenge
274
+ };
275
+ }
276
+
277
+ // Create session
278
+ const session = await this.createSession(providerId, authResult, authState);
279
+
280
+ // Map IdP groups to ELSABRO roles
281
+ await this.mapGroupsToRoles(session);
282
+
283
+ // Just-in-time provisioning
284
+ await this.provisionUser(session);
285
+
286
+ AuditLogger.success('sso.auth.completed', {
287
+ principal: session.userId,
288
+ details: { providerId, protocol: provider.protocol }
289
+ });
290
+
291
+ return {
292
+ success: true,
293
+ session,
294
+ redirectUrl: callbackData.redirectUri || '/dashboard'
295
+ };
296
+ } catch (error) {
297
+ AuditLogger.failure('sso.auth.failed', {
298
+ details: {
299
+ providerId,
300
+ error: error instanceof Error ? error.message : 'Unknown error'
301
+ }
302
+ });
303
+
304
+ return {
305
+ success: false,
306
+ error: {
307
+ code: 'AUTH_FAILED',
308
+ message: error instanceof Error ? error.message : 'Authentication failed',
309
+ providerId
310
+ }
311
+ };
312
+ } finally {
313
+ // Clean up auth state
314
+ await this.deleteAuthState(callbackData.state);
315
+ }
316
+ }
317
+
318
+ // Get session by ID
319
+ async getSession(sessionId: string): Promise<SSOSession | null> {
320
+ const session = this.sessions.get(sessionId);
321
+ if (!session) return null;
322
+
323
+ // Check expiration
324
+ if (new Date(session.expiresAt) < new Date()) {
325
+ await this.destroySession(sessionId);
326
+ return null;
327
+ }
328
+
329
+ // Check timeout
330
+ const lastActivity = new Date(session.lastActivityAt);
331
+ const timeout = this.config.sessionTimeout * 1000;
332
+ if (Date.now() - lastActivity.getTime() > timeout) {
333
+ await this.destroySession(sessionId);
334
+ return null;
335
+ }
336
+
337
+ // Update last activity
338
+ session.lastActivityAt = new Date().toISOString();
339
+ this.sessions.set(sessionId, session);
340
+
341
+ return session;
342
+ }
343
+
344
+ // Refresh session tokens
345
+ async refreshSession(sessionId: string): Promise<SSOSession | null> {
346
+ const session = this.sessions.get(sessionId);
347
+ if (!session || !session.refreshToken) return null;
348
+
349
+ const provider = this.providers.get(session.providerId);
350
+ if (!provider) return null;
351
+
352
+ try {
353
+ const refreshed = await provider.refreshTokens(session.refreshToken);
354
+
355
+ session.accessToken = refreshed.accessToken;
356
+ session.refreshToken = refreshed.refreshToken || session.refreshToken;
357
+ session.idToken = refreshed.idToken;
358
+ session.expiresAt = refreshed.expiresAt;
359
+ session.lastActivityAt = new Date().toISOString();
360
+
361
+ this.sessions.set(sessionId, session);
362
+
363
+ AuditLogger.log({
364
+ action: 'sso.session.refreshed',
365
+ principal: session.userId,
366
+ resource: sessionId
367
+ });
368
+
369
+ return session;
370
+ } catch (error) {
371
+ AuditLogger.failure('sso.session.refresh_failed', {
372
+ principal: session.userId,
373
+ resource: sessionId
374
+ });
375
+ return null;
376
+ }
377
+ }
378
+
379
+ // Single logout
380
+ async logout(sessionId: string): Promise<LogoutResult> {
381
+ const session = this.sessions.get(sessionId);
382
+ if (!session) {
383
+ return { success: true, logoutUrls: [] };
384
+ }
385
+
386
+ const logoutUrls: string[] = [];
387
+
388
+ if (this.config.singleLogout) {
389
+ const provider = this.providers.get(session.providerId);
390
+ if (provider && provider.getLogoutUrl) {
391
+ const logoutUrl = await provider.getLogoutUrl(session);
392
+ if (logoutUrl) {
393
+ logoutUrls.push(logoutUrl);
394
+ }
395
+ }
396
+ }
397
+
398
+ await this.destroySession(sessionId);
399
+
400
+ AuditLogger.log({
401
+ action: 'sso.logout',
402
+ principal: session.userId,
403
+ details: { providerId: session.providerId, singleLogout: this.config.singleLogout }
404
+ });
405
+
406
+ return {
407
+ success: true,
408
+ logoutUrls
409
+ };
410
+ }
411
+
412
+ // Destroy session
413
+ async destroySession(sessionId: string): Promise<void> {
414
+ const session = this.sessions.get(sessionId);
415
+ if (session) {
416
+ // Revoke tokens if possible
417
+ const provider = this.providers.get(session.providerId);
418
+ if (provider && session.accessToken) {
419
+ await provider.revokeToken?.(session.accessToken).catch(() => {});
420
+ }
421
+ }
422
+ this.sessions.delete(sessionId);
423
+ }
424
+
425
+ // List active sessions for user
426
+ listUserSessions(userId: string): SSOSession[] {
427
+ return Array.from(this.sessions.values())
428
+ .filter(s => s.userId === userId);
429
+ }
430
+
431
+ // Terminate all user sessions
432
+ async terminateUserSessions(userId: string): Promise<number> {
433
+ const sessions = this.listUserSessions(userId);
434
+ for (const session of sessions) {
435
+ await this.destroySession(session.id);
436
+ }
437
+ return sessions.length;
438
+ }
439
+
440
+ // Private methods
441
+ private async createSession(
442
+ providerId: string,
443
+ authResult: ProviderAuthResult,
444
+ authState: AuthState
445
+ ): Promise<SSOSession> {
446
+ const sessionId = this.generateSessionId();
447
+ const expiresAt = new Date(Date.now() + this.config.sessionTimeout * 1000);
448
+
449
+ const session: SSOSession = {
450
+ id: sessionId,
451
+ userId: authResult.userId || authResult.attributes.email,
452
+ providerId,
453
+ protocol: authResult.protocol,
454
+ accessToken: authResult.accessToken,
455
+ refreshToken: authResult.refreshToken,
456
+ idToken: authResult.idToken,
457
+ expiresAt: expiresAt.toISOString(),
458
+ createdAt: new Date().toISOString(),
459
+ lastActivityAt: new Date().toISOString(),
460
+ attributes: authResult.attributes,
461
+ mfaVerified: authResult.mfaVerified || false,
462
+ ipAddress: authState.ipAddress || '',
463
+ userAgent: authState.userAgent || ''
464
+ };
465
+
466
+ // Encrypt sensitive tokens
467
+ if (session.accessToken) {
468
+ session.accessToken = await this.tokenService.encrypt(session.accessToken);
469
+ }
470
+ if (session.refreshToken) {
471
+ session.refreshToken = await this.tokenService.encrypt(session.refreshToken);
472
+ }
473
+
474
+ this.sessions.set(sessionId, session);
475
+ return session;
476
+ }
477
+
478
+ private async mapGroupsToRoles(session: SSOSession): Promise<void> {
479
+ // Get role mapping configuration
480
+ const provider = this.providers.get(session.providerId);
481
+ if (!provider?.roleMapping) return;
482
+
483
+ const mappedRoles: string[] = [];
484
+ for (const group of session.attributes.groups) {
485
+ const role = provider.roleMapping[group];
486
+ if (role) {
487
+ mappedRoles.push(role);
488
+ }
489
+ }
490
+
491
+ session.attributes.roles = [...new Set([...session.attributes.roles, ...mappedRoles])];
492
+ }
493
+
494
+ private async provisionUser(session: SSOSession): Promise<void> {
495
+ // JIT provisioning - create or update user in ELSABRO
496
+ EventBus.publish('sso.user.provisioned', {
497
+ userId: session.userId,
498
+ attributes: session.attributes,
499
+ providerId: session.providerId,
500
+ timestamp: new Date().toISOString()
501
+ });
502
+ }
503
+
504
+ private isIPAllowed(ip: string): boolean {
505
+ if (!this.config.ipAllowlist) return true;
506
+ return this.config.ipAllowlist.some(allowed => {
507
+ if (allowed.includes('/')) {
508
+ return this.matchCIDR(allowed, ip);
509
+ }
510
+ return allowed === ip;
511
+ });
512
+ }
513
+
514
+ private matchCIDR(cidr: string, ip: string): boolean {
515
+ const [network, bits] = cidr.split('/');
516
+ const mask = ~(Math.pow(2, 32 - parseInt(bits)) - 1);
517
+ const ipNum = this.ipToNum(ip);
518
+ const networkNum = this.ipToNum(network);
519
+ return (ipNum & mask) === (networkNum & mask);
520
+ }
521
+
522
+ private ipToNum(ip: string): number {
523
+ return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0);
524
+ }
525
+
526
+ private generateSessionId(): string {
527
+ return `sso_${Date.now()}_${crypto.randomBytes(16).toString('hex')}`;
528
+ }
529
+
530
+ private generateState(): string {
531
+ return crypto.randomBytes(32).toString('base64url');
532
+ }
533
+
534
+ private generateNonce(): string {
535
+ return crypto.randomBytes(16).toString('base64url');
536
+ }
537
+
538
+ private async storeAuthState(state: string, data: AuthState): Promise<void> {
539
+ // Store in memory or cache with TTL
540
+ }
541
+
542
+ private async getAuthState(state: string): Promise<AuthState | null> {
543
+ // Retrieve from memory or cache
544
+ return null;
545
+ }
546
+
547
+ private async deleteAuthState(state: string): Promise<void> {
548
+ // Delete from memory or cache
549
+ }
550
+
551
+ private async initiateMFA(authResult: ProviderAuthResult): Promise<MFAChallenge> {
552
+ // Initiate MFA challenge
553
+ return {
554
+ type: 'totp',
555
+ challengeId: crypto.randomBytes(16).toString('hex'),
556
+ expiresAt: new Date(Date.now() + 300000).toISOString()
557
+ };
558
+ }
559
+ }
560
+ ```
561
+
562
+ ---
563
+
564
+ ## SAMLProvider
565
+
566
+ ```typescript
567
+ interface SAMLConfig {
568
+ entityId: string;
569
+ assertionConsumerServiceUrl: string;
570
+ singleLogoutServiceUrl?: string;
571
+ certificate: string;
572
+ privateKey: string;
573
+ signAuthnRequests: boolean;
574
+ wantAssertionsSigned: boolean;
575
+ wantResponseSigned: boolean;
576
+ signatureAlgorithm: 'sha256' | 'sha384' | 'sha512';
577
+ identityProviderMetadataUrl?: string;
578
+ identityProviderMetadata?: SAMLIdPMetadata;
579
+ attributeMapping: SAMLAttributeMapping;
580
+ allowedClockSkewSeconds: number;
581
+ }
582
+
583
+ interface SAMLIdPMetadata {
584
+ entityId: string;
585
+ singleSignOnUrl: string;
586
+ singleLogoutUrl?: string;
587
+ certificate: string;
588
+ nameIdFormat: string;
589
+ }
590
+
591
+ interface SAMLAttributeMapping {
592
+ email: string;
593
+ name?: string;
594
+ firstName?: string;
595
+ lastName?: string;
596
+ groups?: string;
597
+ department?: string;
598
+ customMappings?: Record<string, string>;
599
+ }
600
+
601
+ interface SAMLAssertion {
602
+ issuer: string;
603
+ subject: {
604
+ nameId: string;
605
+ nameIdFormat: string;
606
+ };
607
+ conditions: {
608
+ notBefore: string;
609
+ notOnOrAfter: string;
610
+ audienceRestriction: string[];
611
+ };
612
+ authnStatement: {
613
+ authnInstant: string;
614
+ sessionIndex: string;
615
+ authnContext: string;
616
+ };
617
+ attributes: Record<string, string | string[]>;
618
+ signature?: SAMLSignature;
619
+ }
620
+
621
+ interface SAMLSignature {
622
+ algorithm: string;
623
+ value: string;
624
+ certificate: string;
625
+ valid: boolean;
626
+ }
627
+
628
+ class SAMLProvider implements IdentityProvider {
629
+ id: string;
630
+ name: string;
631
+ protocol: 'saml' = 'saml';
632
+
633
+ private config: SAMLConfig;
634
+ private idpMetadata: SAMLIdPMetadata;
635
+ roleMapping?: Record<string, string>;
636
+
637
+ constructor(id: string, name: string, config: SAMLConfig) {
638
+ this.id = id;
639
+ this.name = name;
640
+ this.config = config;
641
+ this.loadIdPMetadata();
642
+ }
643
+
644
+ // Load IdP metadata from URL or config
645
+ private async loadIdPMetadata(): Promise<void> {
646
+ if (this.config.identityProviderMetadataUrl) {
647
+ const response = await fetch(this.config.identityProviderMetadataUrl);
648
+ const xml = await response.text();
649
+ this.idpMetadata = this.parseMetadataXML(xml);
650
+ } else if (this.config.identityProviderMetadata) {
651
+ this.idpMetadata = this.config.identityProviderMetadata;
652
+ } else {
653
+ throw new Error('IdP metadata required');
654
+ }
655
+ }
656
+
657
+ // Generate SP metadata XML
658
+ generateMetadata(): string {
659
+ return `<?xml version="1.0" encoding="UTF-8"?>
660
+ <md:EntityDescriptor
661
+ xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
662
+ entityID="${this.config.entityId}">
663
+ <md:SPSSODescriptor
664
+ AuthnRequestsSigned="${this.config.signAuthnRequests}"
665
+ WantAssertionsSigned="${this.config.wantAssertionsSigned}"
666
+ protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
667
+ <md:KeyDescriptor use="signing">
668
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
669
+ <ds:X509Data>
670
+ <ds:X509Certificate>${this.config.certificate}</ds:X509Certificate>
671
+ </ds:X509Data>
672
+ </ds:KeyInfo>
673
+ </md:KeyDescriptor>
674
+ <md:KeyDescriptor use="encryption">
675
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
676
+ <ds:X509Data>
677
+ <ds:X509Certificate>${this.config.certificate}</ds:X509Certificate>
678
+ </ds:X509Data>
679
+ </ds:KeyInfo>
680
+ </md:KeyDescriptor>
681
+ <md:SingleLogoutService
682
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
683
+ Location="${this.config.singleLogoutServiceUrl}"/>
684
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
685
+ <md:AssertionConsumerService
686
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
687
+ Location="${this.config.assertionConsumerServiceUrl}"
688
+ index="0"
689
+ isDefault="true"/>
690
+ </md:SPSSODescriptor>
691
+ </md:EntityDescriptor>`;
692
+ }
693
+
694
+ // Get authorization URL (SAML AuthnRequest)
695
+ async getAuthorizationUrl(options: AuthOptions): Promise<string> {
696
+ const authnRequest = this.buildAuthnRequest(options);
697
+ const signedRequest = this.config.signAuthnRequests
698
+ ? await this.signRequest(authnRequest)
699
+ : authnRequest;
700
+
701
+ const encodedRequest = Buffer.from(signedRequest).toString('base64');
702
+ const deflatedRequest = await this.deflate(encodedRequest);
703
+
704
+ const params = new URLSearchParams({
705
+ SAMLRequest: deflatedRequest,
706
+ RelayState: options.state || ''
707
+ });
708
+
709
+ return `${this.idpMetadata.singleSignOnUrl}?${params.toString()}`;
710
+ }
711
+
712
+ // Process SAML callback (Response)
713
+ async processCallback(
714
+ callbackData: CallbackData,
715
+ expectedNonce?: string
716
+ ): Promise<ProviderAuthResult> {
717
+ const samlResponse = callbackData.SAMLResponse;
718
+ if (!samlResponse) {
719
+ throw new Error('Missing SAML Response');
720
+ }
721
+
722
+ // Decode response
723
+ const decodedResponse = Buffer.from(samlResponse, 'base64').toString('utf8');
724
+
725
+ // Validate signature
726
+ if (this.config.wantResponseSigned) {
727
+ const signatureValid = await this.validateSignature(
728
+ decodedResponse,
729
+ this.idpMetadata.certificate
730
+ );
731
+ if (!signatureValid) {
732
+ throw new Error('Invalid SAML Response signature');
733
+ }
734
+ }
735
+
736
+ // Parse assertion
737
+ const assertion = this.parseAssertion(decodedResponse);
738
+
739
+ // Validate assertion
740
+ this.validateAssertion(assertion);
741
+
742
+ // Map attributes
743
+ const attributes = this.mapAttributes(assertion);
744
+
745
+ return {
746
+ protocol: 'saml',
747
+ userId: assertion.subject.nameId,
748
+ attributes,
749
+ sessionIndex: assertion.authnStatement.sessionIndex,
750
+ mfaVerified: this.checkMFAContext(assertion.authnStatement.authnContext)
751
+ };
752
+ }
753
+
754
+ // Get logout URL
755
+ async getLogoutUrl(session: SSOSession): Promise<string | null> {
756
+ if (!this.idpMetadata.singleLogoutUrl || !this.config.singleLogoutServiceUrl) {
757
+ return null;
758
+ }
759
+
760
+ const logoutRequest = this.buildLogoutRequest(session);
761
+ const signedRequest = await this.signRequest(logoutRequest);
762
+ const encodedRequest = Buffer.from(signedRequest).toString('base64');
763
+
764
+ const params = new URLSearchParams({
765
+ SAMLRequest: encodedRequest
766
+ });
767
+
768
+ return `${this.idpMetadata.singleLogoutUrl}?${params.toString()}`;
769
+ }
770
+
771
+ // Validate SAML assertion
772
+ private validateAssertion(assertion: SAMLAssertion): void {
773
+ const now = new Date();
774
+ const clockSkew = this.config.allowedClockSkewSeconds * 1000;
775
+
776
+ // Check time conditions
777
+ const notBefore = new Date(assertion.conditions.notBefore);
778
+ const notOnOrAfter = new Date(assertion.conditions.notOnOrAfter);
779
+
780
+ if (now.getTime() < notBefore.getTime() - clockSkew) {
781
+ throw new Error('Assertion not yet valid');
782
+ }
783
+
784
+ if (now.getTime() > notOnOrAfter.getTime() + clockSkew) {
785
+ throw new Error('Assertion expired');
786
+ }
787
+
788
+ // Check audience
789
+ if (!assertion.conditions.audienceRestriction.includes(this.config.entityId)) {
790
+ throw new Error('Invalid audience');
791
+ }
792
+
793
+ // Validate signature if required
794
+ if (this.config.wantAssertionsSigned && assertion.signature) {
795
+ if (!assertion.signature.valid) {
796
+ throw new Error('Invalid assertion signature');
797
+ }
798
+ }
799
+ }
800
+
801
+ // Map SAML attributes to UserAttributes
802
+ private mapAttributes(assertion: SAMLAssertion): UserAttributes {
803
+ const mapping = this.config.attributeMapping;
804
+ const attrs = assertion.attributes;
805
+
806
+ const getValue = (key: string): string | undefined => {
807
+ const value = attrs[key];
808
+ return Array.isArray(value) ? value[0] : value;
809
+ };
810
+
811
+ const getArrayValue = (key: string): string[] => {
812
+ const value = attrs[key];
813
+ return Array.isArray(value) ? value : value ? [value] : [];
814
+ };
815
+
816
+ const attributes: UserAttributes = {
817
+ email: getValue(mapping.email) || assertion.subject.nameId,
818
+ name: mapping.name ? getValue(mapping.name) : undefined,
819
+ firstName: mapping.firstName ? getValue(mapping.firstName) : undefined,
820
+ lastName: mapping.lastName ? getValue(mapping.lastName) : undefined,
821
+ groups: mapping.groups ? getArrayValue(mapping.groups) : [],
822
+ roles: [],
823
+ department: mapping.department ? getValue(mapping.department) : undefined
824
+ };
825
+
826
+ // Map custom attributes
827
+ if (mapping.customMappings) {
828
+ attributes.customAttributes = {};
829
+ for (const [target, source] of Object.entries(mapping.customMappings)) {
830
+ attributes.customAttributes[target] = getValue(source);
831
+ }
832
+ }
833
+
834
+ return attributes;
835
+ }
836
+
837
+ private checkMFAContext(authnContext: string): boolean {
838
+ const mfaContexts = [
839
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract',
840
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS',
841
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN'
842
+ ];
843
+ return mfaContexts.includes(authnContext);
844
+ }
845
+
846
+ private buildAuthnRequest(options: AuthOptions): string {
847
+ const id = `_${crypto.randomBytes(16).toString('hex')}`;
848
+ const issueInstant = new Date().toISOString();
849
+
850
+ return `<?xml version="1.0" encoding="UTF-8"?>
851
+ <samlp:AuthnRequest
852
+ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
853
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
854
+ ID="${id}"
855
+ Version="2.0"
856
+ IssueInstant="${issueInstant}"
857
+ Destination="${this.idpMetadata.singleSignOnUrl}"
858
+ AssertionConsumerServiceURL="${this.config.assertionConsumerServiceUrl}"
859
+ ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
860
+ <saml:Issuer>${this.config.entityId}</saml:Issuer>
861
+ <samlp:NameIDPolicy
862
+ Format="${this.idpMetadata.nameIdFormat}"
863
+ AllowCreate="true"/>
864
+ </samlp:AuthnRequest>`;
865
+ }
866
+
867
+ private buildLogoutRequest(session: SSOSession): string {
868
+ const id = `_${crypto.randomBytes(16).toString('hex')}`;
869
+ const issueInstant = new Date().toISOString();
870
+
871
+ return `<?xml version="1.0" encoding="UTF-8"?>
872
+ <samlp:LogoutRequest
873
+ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
874
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
875
+ ID="${id}"
876
+ Version="2.0"
877
+ IssueInstant="${issueInstant}"
878
+ Destination="${this.idpMetadata.singleLogoutUrl}">
879
+ <saml:Issuer>${this.config.entityId}</saml:Issuer>
880
+ <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
881
+ ${session.userId}
882
+ </saml:NameID>
883
+ </samlp:LogoutRequest>`;
884
+ }
885
+
886
+ private async signRequest(xml: string): Promise<string> {
887
+ // Sign XML with private key
888
+ // Implementation uses xmldsig
889
+ return xml;
890
+ }
891
+
892
+ private async validateSignature(xml: string, certificate: string): Promise<boolean> {
893
+ // Validate XML signature
894
+ return true;
895
+ }
896
+
897
+ private parseAssertion(xml: string): SAMLAssertion {
898
+ // Parse SAML Response XML and extract assertion
899
+ // Implementation uses xml2js or similar
900
+ return {} as SAMLAssertion;
901
+ }
902
+
903
+ private parseMetadataXML(xml: string): SAMLIdPMetadata {
904
+ // Parse IdP metadata XML
905
+ return {} as SAMLIdPMetadata;
906
+ }
907
+
908
+ private async deflate(data: string): Promise<string> {
909
+ // Deflate and base64url encode
910
+ return data;
911
+ }
912
+ }
913
+ ```
914
+
915
+ ---
916
+
917
+ ## OIDCProvider
918
+
919
+ ```typescript
920
+ interface OIDCConfig {
921
+ clientId: string;
922
+ clientSecret: string;
923
+ issuer: string;
924
+ authorizationEndpoint?: string;
925
+ tokenEndpoint?: string;
926
+ userInfoEndpoint?: string;
927
+ jwksUri?: string;
928
+ endSessionEndpoint?: string;
929
+ scopes: string[];
930
+ responseType: 'code' | 'id_token' | 'code id_token';
931
+ responseMode?: 'query' | 'fragment' | 'form_post';
932
+ usePKCE: boolean;
933
+ redirectUri: string;
934
+ postLogoutRedirectUri?: string;
935
+ attributeMapping: OIDCAttributeMapping;
936
+ clockTolerance: number;
937
+ }
938
+
939
+ interface OIDCAttributeMapping {
940
+ email: string;
941
+ name?: string;
942
+ firstName?: string;
943
+ lastName?: string;
944
+ groups?: string;
945
+ roles?: string;
946
+ customClaims?: Record<string, string>;
947
+ }
948
+
949
+ interface OIDCTokens {
950
+ accessToken: string;
951
+ refreshToken?: string;
952
+ idToken: string;
953
+ tokenType: string;
954
+ expiresIn: number;
955
+ scope: string;
956
+ }
957
+
958
+ interface OIDCIdTokenClaims {
959
+ iss: string;
960
+ sub: string;
961
+ aud: string | string[];
962
+ exp: number;
963
+ iat: number;
964
+ nonce?: string;
965
+ auth_time?: number;
966
+ acr?: string;
967
+ amr?: string[];
968
+ [key: string]: unknown;
969
+ }
970
+
971
+ interface PKCEChallenge {
972
+ verifier: string;
973
+ challenge: string;
974
+ method: 'S256';
975
+ }
976
+
977
+ class OIDCProvider implements IdentityProvider {
978
+ id: string;
979
+ name: string;
980
+ protocol: 'oidc' = 'oidc';
981
+
982
+ private config: OIDCConfig;
983
+ private discoveryDoc: OIDCDiscoveryDocument;
984
+ private jwks: JWKS;
985
+ roleMapping?: Record<string, string>;
986
+
987
+ constructor(id: string, name: string, config: OIDCConfig) {
988
+ this.id = id;
989
+ this.name = name;
990
+ this.config = config;
991
+ this.discoverEndpoints();
992
+ }
993
+
994
+ // Discover OIDC endpoints from issuer
995
+ private async discoverEndpoints(): Promise<void> {
996
+ const discoveryUrl = `${this.config.issuer}/.well-known/openid-configuration`;
997
+ const response = await fetch(discoveryUrl);
998
+ this.discoveryDoc = await response.json();
999
+
1000
+ // Load JWKS for token validation
1001
+ if (this.discoveryDoc.jwks_uri) {
1002
+ const jwksResponse = await fetch(this.discoveryDoc.jwks_uri);
1003
+ this.jwks = await jwksResponse.json();
1004
+ }
1005
+ }
1006
+
1007
+ // Get authorization URL
1008
+ async getAuthorizationUrl(options: AuthOptions): Promise<string> {
1009
+ const endpoint = this.config.authorizationEndpoint ||
1010
+ this.discoveryDoc.authorization_endpoint;
1011
+
1012
+ const params: Record<string, string> = {
1013
+ client_id: this.config.clientId,
1014
+ redirect_uri: options.redirectUri || this.config.redirectUri,
1015
+ response_type: this.config.responseType,
1016
+ scope: options.scope?.join(' ') || this.config.scopes.join(' '),
1017
+ state: options.state || '',
1018
+ nonce: options.nonce || ''
1019
+ };
1020
+
1021
+ if (this.config.responseMode) {
1022
+ params.response_mode = this.config.responseMode;
1023
+ }
1024
+
1025
+ if (options.prompt) {
1026
+ params.prompt = options.prompt;
1027
+ }
1028
+
1029
+ // Add PKCE if enabled
1030
+ if (this.config.usePKCE) {
1031
+ const pkce = this.generatePKCE();
1032
+ params.code_challenge = pkce.challenge;
1033
+ params.code_challenge_method = pkce.method;
1034
+ // Store verifier for token exchange
1035
+ await this.storePKCEVerifier(options.state!, pkce.verifier);
1036
+ }
1037
+
1038
+ const queryString = new URLSearchParams(params).toString();
1039
+ return `${endpoint}?${queryString}`;
1040
+ }
1041
+
1042
+ // Process OIDC callback
1043
+ async processCallback(
1044
+ callbackData: CallbackData,
1045
+ expectedNonce?: string
1046
+ ): Promise<ProviderAuthResult> {
1047
+ // Check for error response
1048
+ if (callbackData.error) {
1049
+ throw new Error(`OIDC error: ${callbackData.error} - ${callbackData.error_description}`);
1050
+ }
1051
+
1052
+ // Exchange authorization code for tokens
1053
+ const tokens = await this.exchangeCode(callbackData.code, callbackData.state);
1054
+
1055
+ // Validate ID token
1056
+ const claims = await this.validateIdToken(tokens.idToken, expectedNonce);
1057
+
1058
+ // Get user info if needed
1059
+ let userInfo: Record<string, unknown> = {};
1060
+ if (this.config.userInfoEndpoint || this.discoveryDoc.userinfo_endpoint) {
1061
+ userInfo = await this.getUserInfo(tokens.accessToken);
1062
+ }
1063
+
1064
+ // Merge claims with userinfo
1065
+ const allClaims = { ...claims, ...userInfo };
1066
+
1067
+ // Map attributes
1068
+ const attributes = this.mapAttributes(allClaims);
1069
+
1070
+ return {
1071
+ protocol: 'oidc',
1072
+ userId: claims.sub,
1073
+ accessToken: tokens.accessToken,
1074
+ refreshToken: tokens.refreshToken,
1075
+ idToken: tokens.idToken,
1076
+ expiresAt: new Date(Date.now() + tokens.expiresIn * 1000).toISOString(),
1077
+ attributes,
1078
+ mfaVerified: this.checkMFAClaims(claims)
1079
+ };
1080
+ }
1081
+
1082
+ // Exchange authorization code for tokens
1083
+ private async exchangeCode(code: string, state?: string): Promise<OIDCTokens> {
1084
+ const endpoint = this.config.tokenEndpoint || this.discoveryDoc.token_endpoint;
1085
+
1086
+ const params: Record<string, string> = {
1087
+ grant_type: 'authorization_code',
1088
+ client_id: this.config.clientId,
1089
+ client_secret: this.config.clientSecret,
1090
+ code,
1091
+ redirect_uri: this.config.redirectUri
1092
+ };
1093
+
1094
+ // Add PKCE verifier if used
1095
+ if (this.config.usePKCE && state) {
1096
+ const verifier = await this.getPKCEVerifier(state);
1097
+ if (verifier) {
1098
+ params.code_verifier = verifier;
1099
+ }
1100
+ }
1101
+
1102
+ const response = await fetch(endpoint, {
1103
+ method: 'POST',
1104
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1105
+ body: new URLSearchParams(params).toString()
1106
+ });
1107
+
1108
+ if (!response.ok) {
1109
+ const error = await response.json();
1110
+ throw new Error(`Token exchange failed: ${error.error}`);
1111
+ }
1112
+
1113
+ const data = await response.json();
1114
+ return {
1115
+ accessToken: data.access_token,
1116
+ refreshToken: data.refresh_token,
1117
+ idToken: data.id_token,
1118
+ tokenType: data.token_type,
1119
+ expiresIn: data.expires_in,
1120
+ scope: data.scope
1121
+ };
1122
+ }
1123
+
1124
+ // Refresh tokens
1125
+ async refreshTokens(refreshToken: string): Promise<{
1126
+ accessToken: string;
1127
+ refreshToken?: string;
1128
+ idToken?: string;
1129
+ expiresAt: string;
1130
+ }> {
1131
+ const endpoint = this.config.tokenEndpoint || this.discoveryDoc.token_endpoint;
1132
+
1133
+ const params = {
1134
+ grant_type: 'refresh_token',
1135
+ client_id: this.config.clientId,
1136
+ client_secret: this.config.clientSecret,
1137
+ refresh_token: refreshToken
1138
+ };
1139
+
1140
+ const response = await fetch(endpoint, {
1141
+ method: 'POST',
1142
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1143
+ body: new URLSearchParams(params).toString()
1144
+ });
1145
+
1146
+ if (!response.ok) {
1147
+ throw new Error('Token refresh failed');
1148
+ }
1149
+
1150
+ const data = await response.json();
1151
+ return {
1152
+ accessToken: data.access_token,
1153
+ refreshToken: data.refresh_token,
1154
+ idToken: data.id_token,
1155
+ expiresAt: new Date(Date.now() + data.expires_in * 1000).toISOString()
1156
+ };
1157
+ }
1158
+
1159
+ // Revoke token
1160
+ async revokeToken(token: string): Promise<void> {
1161
+ if (!this.discoveryDoc.revocation_endpoint) return;
1162
+
1163
+ await fetch(this.discoveryDoc.revocation_endpoint, {
1164
+ method: 'POST',
1165
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1166
+ body: new URLSearchParams({
1167
+ client_id: this.config.clientId,
1168
+ client_secret: this.config.clientSecret,
1169
+ token
1170
+ }).toString()
1171
+ });
1172
+ }
1173
+
1174
+ // Get logout URL
1175
+ async getLogoutUrl(session: SSOSession): Promise<string | null> {
1176
+ const endpoint = this.config.endSessionEndpoint ||
1177
+ this.discoveryDoc.end_session_endpoint;
1178
+ if (!endpoint) return null;
1179
+
1180
+ const params: Record<string, string> = {
1181
+ client_id: this.config.clientId
1182
+ };
1183
+
1184
+ if (session.idToken) {
1185
+ params.id_token_hint = session.idToken;
1186
+ }
1187
+
1188
+ if (this.config.postLogoutRedirectUri) {
1189
+ params.post_logout_redirect_uri = this.config.postLogoutRedirectUri;
1190
+ }
1191
+
1192
+ return `${endpoint}?${new URLSearchParams(params).toString()}`;
1193
+ }
1194
+
1195
+ // Validate ID token
1196
+ private async validateIdToken(
1197
+ idToken: string,
1198
+ expectedNonce?: string
1199
+ ): Promise<OIDCIdTokenClaims> {
1200
+ // Decode token
1201
+ const [headerB64, payloadB64, signature] = idToken.split('.');
1202
+ const header = JSON.parse(Buffer.from(headerB64, 'base64url').toString());
1203
+ const claims = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
1204
+
1205
+ // Find signing key
1206
+ const key = this.jwks.keys.find(k => k.kid === header.kid);
1207
+ if (!key) {
1208
+ throw new Error('Signing key not found');
1209
+ }
1210
+
1211
+ // Verify signature
1212
+ const valid = await this.verifySignature(idToken, key, header.alg);
1213
+ if (!valid) {
1214
+ throw new Error('Invalid ID token signature');
1215
+ }
1216
+
1217
+ // Validate claims
1218
+ const now = Math.floor(Date.now() / 1000);
1219
+
1220
+ if (claims.iss !== this.config.issuer) {
1221
+ throw new Error('Invalid issuer');
1222
+ }
1223
+
1224
+ const aud = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
1225
+ if (!aud.includes(this.config.clientId)) {
1226
+ throw new Error('Invalid audience');
1227
+ }
1228
+
1229
+ if (claims.exp < now - this.config.clockTolerance) {
1230
+ throw new Error('Token expired');
1231
+ }
1232
+
1233
+ if (claims.iat > now + this.config.clockTolerance) {
1234
+ throw new Error('Token issued in the future');
1235
+ }
1236
+
1237
+ if (expectedNonce && claims.nonce !== expectedNonce) {
1238
+ throw new Error('Invalid nonce');
1239
+ }
1240
+
1241
+ return claims;
1242
+ }
1243
+
1244
+ // Get user info from userinfo endpoint
1245
+ private async getUserInfo(accessToken: string): Promise<Record<string, unknown>> {
1246
+ const endpoint = this.config.userInfoEndpoint ||
1247
+ this.discoveryDoc.userinfo_endpoint;
1248
+ if (!endpoint) return {};
1249
+
1250
+ const response = await fetch(endpoint, {
1251
+ headers: { Authorization: `Bearer ${accessToken}` }
1252
+ });
1253
+
1254
+ if (!response.ok) {
1255
+ return {};
1256
+ }
1257
+
1258
+ return response.json();
1259
+ }
1260
+
1261
+ // Map OIDC claims to UserAttributes
1262
+ private mapAttributes(claims: Record<string, unknown>): UserAttributes {
1263
+ const mapping = this.config.attributeMapping;
1264
+
1265
+ const getValue = (key: string): string | undefined => {
1266
+ const value = claims[key];
1267
+ return typeof value === 'string' ? value : undefined;
1268
+ };
1269
+
1270
+ const getArrayValue = (key: string): string[] => {
1271
+ const value = claims[key];
1272
+ return Array.isArray(value) ? value : value ? [String(value)] : [];
1273
+ };
1274
+
1275
+ const attributes: UserAttributes = {
1276
+ email: getValue(mapping.email) || '',
1277
+ name: mapping.name ? getValue(mapping.name) : undefined,
1278
+ firstName: mapping.firstName ? getValue(mapping.firstName) : undefined,
1279
+ lastName: mapping.lastName ? getValue(mapping.lastName) : undefined,
1280
+ groups: mapping.groups ? getArrayValue(mapping.groups) : [],
1281
+ roles: mapping.roles ? getArrayValue(mapping.roles) : []
1282
+ };
1283
+
1284
+ // Map custom claims
1285
+ if (mapping.customClaims) {
1286
+ attributes.customAttributes = {};
1287
+ for (const [target, source] of Object.entries(mapping.customClaims)) {
1288
+ attributes.customAttributes[target] = getValue(source);
1289
+ }
1290
+ }
1291
+
1292
+ return attributes;
1293
+ }
1294
+
1295
+ private checkMFAClaims(claims: OIDCIdTokenClaims): boolean {
1296
+ // Check AMR (Authentication Methods References)
1297
+ if (claims.amr && Array.isArray(claims.amr)) {
1298
+ const mfaMethods = ['mfa', 'otp', 'sms', 'hwk', 'swk'];
1299
+ return claims.amr.some(m => mfaMethods.includes(m));
1300
+ }
1301
+
1302
+ // Check ACR (Authentication Context Class Reference)
1303
+ if (claims.acr) {
1304
+ const mfaAcrs = ['urn:mace:incommon:iap:silver', 'http://schemas.openid.net/pape/policies/2007/06/multi-factor'];
1305
+ return mfaAcrs.includes(claims.acr);
1306
+ }
1307
+
1308
+ return false;
1309
+ }
1310
+
1311
+ // Generate PKCE challenge
1312
+ private generatePKCE(): PKCEChallenge {
1313
+ const verifier = crypto.randomBytes(32).toString('base64url');
1314
+ const challenge = crypto
1315
+ .createHash('sha256')
1316
+ .update(verifier)
1317
+ .digest('base64url');
1318
+
1319
+ return { verifier, challenge, method: 'S256' };
1320
+ }
1321
+
1322
+ private async storePKCEVerifier(state: string, verifier: string): Promise<void> {
1323
+ // Store in cache with TTL
1324
+ }
1325
+
1326
+ private async getPKCEVerifier(state: string): Promise<string | null> {
1327
+ // Retrieve from cache
1328
+ return null;
1329
+ }
1330
+
1331
+ private async verifySignature(
1332
+ token: string,
1333
+ key: JWK,
1334
+ algorithm: string
1335
+ ): Promise<boolean> {
1336
+ // Verify JWT signature using jose
1337
+ return true;
1338
+ }
1339
+ }
1340
+ ```
1341
+
1342
+ ---
1343
+
1344
+ ## Identity Provider Configurations
1345
+
1346
+ ### Okta
1347
+
1348
+ ```typescript
1349
+ const oktaProvider = new OIDCProvider('okta', 'Okta', {
1350
+ clientId: process.env.OKTA_CLIENT_ID!,
1351
+ clientSecret: process.env.OKTA_CLIENT_SECRET!,
1352
+ issuer: `https://${process.env.OKTA_DOMAIN}/oauth2/default`,
1353
+ scopes: ['openid', 'profile', 'email', 'groups'],
1354
+ responseType: 'code',
1355
+ usePKCE: true,
1356
+ redirectUri: 'https://app.example.com/api/auth/callback/okta',
1357
+ attributeMapping: {
1358
+ email: 'email',
1359
+ name: 'name',
1360
+ firstName: 'given_name',
1361
+ lastName: 'family_name',
1362
+ groups: 'groups'
1363
+ },
1364
+ clockTolerance: 120
1365
+ });
1366
+
1367
+ oktaProvider.roleMapping = {
1368
+ 'Everyone': 'user',
1369
+ 'Engineering': 'developer',
1370
+ 'Admins': 'admin',
1371
+ 'Security': 'security-admin'
1372
+ };
1373
+ ```
1374
+
1375
+ ### Azure AD / Entra ID
1376
+
1377
+ ```typescript
1378
+ const azureAdProvider = new OIDCProvider('azure-ad', 'Azure AD', {
1379
+ clientId: process.env.AZURE_CLIENT_ID!,
1380
+ clientSecret: process.env.AZURE_CLIENT_SECRET!,
1381
+ issuer: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0`,
1382
+ scopes: ['openid', 'profile', 'email', 'User.Read', 'GroupMember.Read.All'],
1383
+ responseType: 'code',
1384
+ usePKCE: true,
1385
+ redirectUri: 'https://app.example.com/api/auth/callback/azure',
1386
+ attributeMapping: {
1387
+ email: 'email',
1388
+ name: 'name',
1389
+ firstName: 'given_name',
1390
+ lastName: 'family_name',
1391
+ groups: 'groups',
1392
+ customClaims: {
1393
+ department: 'department',
1394
+ jobTitle: 'jobTitle'
1395
+ }
1396
+ },
1397
+ clockTolerance: 120
1398
+ });
1399
+
1400
+ azureAdProvider.roleMapping = {
1401
+ 'Global Administrators': 'admin',
1402
+ 'Application Administrators': 'app-admin',
1403
+ 'Developers': 'developer',
1404
+ 'Users': 'user'
1405
+ };
1406
+ ```
1407
+
1408
+ ### Google Workspace
1409
+
1410
+ ```typescript
1411
+ const googleProvider = new OIDCProvider('google', 'Google Workspace', {
1412
+ clientId: process.env.GOOGLE_CLIENT_ID!,
1413
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
1414
+ issuer: 'https://accounts.google.com',
1415
+ scopes: [
1416
+ 'openid',
1417
+ 'profile',
1418
+ 'email',
1419
+ 'https://www.googleapis.com/auth/admin.directory.group.readonly'
1420
+ ],
1421
+ responseType: 'code',
1422
+ usePKCE: true,
1423
+ redirectUri: 'https://app.example.com/api/auth/callback/google',
1424
+ attributeMapping: {
1425
+ email: 'email',
1426
+ name: 'name',
1427
+ firstName: 'given_name',
1428
+ lastName: 'family_name',
1429
+ customClaims: {
1430
+ picture: 'picture',
1431
+ hd: 'hd' // Hosted domain
1432
+ }
1433
+ },
1434
+ clockTolerance: 120
1435
+ });
1436
+ ```
1437
+
1438
+ ### Auth0
1439
+
1440
+ ```typescript
1441
+ const auth0Provider = new OIDCProvider('auth0', 'Auth0', {
1442
+ clientId: process.env.AUTH0_CLIENT_ID!,
1443
+ clientSecret: process.env.AUTH0_CLIENT_SECRET!,
1444
+ issuer: `https://${process.env.AUTH0_DOMAIN}/`,
1445
+ scopes: ['openid', 'profile', 'email'],
1446
+ responseType: 'code',
1447
+ usePKCE: true,
1448
+ redirectUri: 'https://app.example.com/api/auth/callback/auth0',
1449
+ attributeMapping: {
1450
+ email: 'email',
1451
+ name: 'name',
1452
+ firstName: 'given_name',
1453
+ lastName: 'family_name',
1454
+ roles: 'https://app.example.com/roles',
1455
+ customClaims: {
1456
+ permissions: 'https://app.example.com/permissions'
1457
+ }
1458
+ },
1459
+ clockTolerance: 120
1460
+ });
1461
+ ```
1462
+
1463
+ ### Keycloak
1464
+
1465
+ ```typescript
1466
+ const keycloakProvider = new OIDCProvider('keycloak', 'Keycloak', {
1467
+ clientId: process.env.KEYCLOAK_CLIENT_ID!,
1468
+ clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
1469
+ issuer: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`,
1470
+ scopes: ['openid', 'profile', 'email', 'roles'],
1471
+ responseType: 'code',
1472
+ usePKCE: true,
1473
+ redirectUri: 'https://app.example.com/api/auth/callback/keycloak',
1474
+ attributeMapping: {
1475
+ email: 'email',
1476
+ name: 'name',
1477
+ firstName: 'given_name',
1478
+ lastName: 'family_name',
1479
+ groups: 'groups',
1480
+ roles: 'realm_access.roles'
1481
+ },
1482
+ clockTolerance: 120
1483
+ });
1484
+
1485
+ keycloakProvider.roleMapping = {
1486
+ 'realm-admin': 'admin',
1487
+ 'realm-user': 'user',
1488
+ 'app-developer': 'developer'
1489
+ };
1490
+ ```
1491
+
1492
+ ### Generic SAML
1493
+
1494
+ ```typescript
1495
+ const genericSamlProvider = new SAMLProvider('generic-saml', 'Corporate IdP', {
1496
+ entityId: 'https://app.example.com/saml/metadata',
1497
+ assertionConsumerServiceUrl: 'https://app.example.com/api/auth/callback/saml',
1498
+ singleLogoutServiceUrl: 'https://app.example.com/api/auth/logout/saml',
1499
+ certificate: process.env.SAML_SP_CERT!,
1500
+ privateKey: process.env.SAML_SP_KEY!,
1501
+ signAuthnRequests: true,
1502
+ wantAssertionsSigned: true,
1503
+ wantResponseSigned: true,
1504
+ signatureAlgorithm: 'sha256',
1505
+ identityProviderMetadataUrl: 'https://idp.example.com/saml/metadata',
1506
+ attributeMapping: {
1507
+ email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
1508
+ name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
1509
+ firstName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
1510
+ lastName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
1511
+ groups: 'http://schemas.xmlsoap.org/claims/Group'
1512
+ },
1513
+ allowedClockSkewSeconds: 120
1514
+ });
1515
+ ```
1516
+
1517
+ ---
1518
+
1519
+ ## SCIM 2.0 User Sync
1520
+
1521
+ ```typescript
1522
+ interface SCIMConfig {
1523
+ enabled: boolean;
1524
+ endpoint: string;
1525
+ bearerToken: string;
1526
+ syncInterval: number;
1527
+ provisionOnSync: boolean;
1528
+ deprovisionOnSync: boolean;
1529
+ }
1530
+
1531
+ interface SCIMUser {
1532
+ id: string;
1533
+ externalId: string;
1534
+ userName: string;
1535
+ name: {
1536
+ formatted: string;
1537
+ givenName: string;
1538
+ familyName: string;
1539
+ };
1540
+ emails: Array<{
1541
+ value: string;
1542
+ type: string;
1543
+ primary: boolean;
1544
+ }>;
1545
+ groups: Array<{
1546
+ value: string;
1547
+ display: string;
1548
+ }>;
1549
+ active: boolean;
1550
+ meta: {
1551
+ created: string;
1552
+ lastModified: string;
1553
+ };
1554
+ }
1555
+
1556
+ class SCIMClient {
1557
+ private config: SCIMConfig;
1558
+ private headers: Record<string, string>;
1559
+
1560
+ constructor(config: SCIMConfig) {
1561
+ this.config = config;
1562
+ this.headers = {
1563
+ 'Authorization': `Bearer ${config.bearerToken}`,
1564
+ 'Content-Type': 'application/scim+json'
1565
+ };
1566
+ }
1567
+
1568
+ // List users
1569
+ async listUsers(filter?: string, startIndex: number = 1, count: number = 100): Promise<{
1570
+ totalResults: number;
1571
+ Resources: SCIMUser[];
1572
+ }> {
1573
+ const params = new URLSearchParams({
1574
+ startIndex: startIndex.toString(),
1575
+ count: count.toString()
1576
+ });
1577
+
1578
+ if (filter) {
1579
+ params.append('filter', filter);
1580
+ }
1581
+
1582
+ const response = await fetch(`${this.config.endpoint}/Users?${params}`, {
1583
+ headers: this.headers
1584
+ });
1585
+
1586
+ return response.json();
1587
+ }
1588
+
1589
+ // Get user by ID
1590
+ async getUser(id: string): Promise<SCIMUser> {
1591
+ const response = await fetch(`${this.config.endpoint}/Users/${id}`, {
1592
+ headers: this.headers
1593
+ });
1594
+
1595
+ return response.json();
1596
+ }
1597
+
1598
+ // Create user
1599
+ async createUser(user: Partial<SCIMUser>): Promise<SCIMUser> {
1600
+ const response = await fetch(`${this.config.endpoint}/Users`, {
1601
+ method: 'POST',
1602
+ headers: this.headers,
1603
+ body: JSON.stringify({
1604
+ schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
1605
+ ...user
1606
+ })
1607
+ });
1608
+
1609
+ return response.json();
1610
+ }
1611
+
1612
+ // Update user
1613
+ async updateUser(id: string, user: Partial<SCIMUser>): Promise<SCIMUser> {
1614
+ const response = await fetch(`${this.config.endpoint}/Users/${id}`, {
1615
+ method: 'PUT',
1616
+ headers: this.headers,
1617
+ body: JSON.stringify({
1618
+ schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
1619
+ ...user
1620
+ })
1621
+ });
1622
+
1623
+ return response.json();
1624
+ }
1625
+
1626
+ // Delete user
1627
+ async deleteUser(id: string): Promise<void> {
1628
+ await fetch(`${this.config.endpoint}/Users/${id}`, {
1629
+ method: 'DELETE',
1630
+ headers: this.headers
1631
+ });
1632
+ }
1633
+
1634
+ // Sync users from IdP
1635
+ async syncUsers(): Promise<SyncResult> {
1636
+ const result: SyncResult = {
1637
+ created: 0,
1638
+ updated: 0,
1639
+ deactivated: 0,
1640
+ errors: []
1641
+ };
1642
+
1643
+ let startIndex = 1;
1644
+ const pageSize = 100;
1645
+ let hasMore = true;
1646
+
1647
+ while (hasMore) {
1648
+ const response = await this.listUsers(undefined, startIndex, pageSize);
1649
+
1650
+ for (const scimUser of response.Resources) {
1651
+ try {
1652
+ await this.syncUser(scimUser, result);
1653
+ } catch (error) {
1654
+ result.errors.push({
1655
+ userId: scimUser.id,
1656
+ error: error instanceof Error ? error.message : 'Unknown error'
1657
+ });
1658
+ }
1659
+ }
1660
+
1661
+ startIndex += pageSize;
1662
+ hasMore = startIndex < response.totalResults;
1663
+ }
1664
+
1665
+ AuditLogger.log({
1666
+ action: 'scim.sync.completed',
1667
+ details: result
1668
+ });
1669
+
1670
+ return result;
1671
+ }
1672
+
1673
+ private async syncUser(scimUser: SCIMUser, result: SyncResult): Promise<void> {
1674
+ // Check if user exists in ELSABRO
1675
+ const existingUser = await this.findLocalUser(scimUser.userName);
1676
+
1677
+ if (existingUser) {
1678
+ if (scimUser.active) {
1679
+ await this.updateLocalUser(existingUser.id, scimUser);
1680
+ result.updated++;
1681
+ } else if (this.config.deprovisionOnSync) {
1682
+ await this.deactivateLocalUser(existingUser.id);
1683
+ result.deactivated++;
1684
+ }
1685
+ } else if (scimUser.active && this.config.provisionOnSync) {
1686
+ await this.createLocalUser(scimUser);
1687
+ result.created++;
1688
+ }
1689
+ }
1690
+
1691
+ private async findLocalUser(email: string): Promise<{ id: string } | null> {
1692
+ // Find user in local database
1693
+ return null;
1694
+ }
1695
+
1696
+ private async createLocalUser(scimUser: SCIMUser): Promise<void> {
1697
+ // Create user in local database
1698
+ EventBus.publish('scim.user.created', {
1699
+ externalId: scimUser.externalId,
1700
+ email: scimUser.emails.find(e => e.primary)?.value
1701
+ });
1702
+ }
1703
+
1704
+ private async updateLocalUser(id: string, scimUser: SCIMUser): Promise<void> {
1705
+ // Update user in local database
1706
+ EventBus.publish('scim.user.updated', { id });
1707
+ }
1708
+
1709
+ private async deactivateLocalUser(id: string): Promise<void> {
1710
+ // Deactivate user in local database
1711
+ EventBus.publish('scim.user.deactivated', { id });
1712
+ }
1713
+ }
1714
+
1715
+ interface SyncResult {
1716
+ created: number;
1717
+ updated: number;
1718
+ deactivated: number;
1719
+ errors: Array<{ userId: string; error: string }>;
1720
+ }
1721
+ ```
1722
+
1723
+ ---
1724
+
1725
+ ## Token Service
1726
+
1727
+ ```typescript
1728
+ interface TokenServiceConfig {
1729
+ algorithm: string;
1730
+ publicKey: string;
1731
+ privateKey: string;
1732
+ issuer: string;
1733
+ audience: string;
1734
+ accessTokenTTL: number;
1735
+ refreshTokenTTL: number;
1736
+ }
1737
+
1738
+ class TokenService {
1739
+ private config: TokenServiceConfig;
1740
+
1741
+ constructor(config: TokenServiceConfig) {
1742
+ this.config = config;
1743
+ }
1744
+
1745
+ // Generate access token
1746
+ async generateAccessToken(payload: TokenPayload): Promise<string> {
1747
+ const now = Math.floor(Date.now() / 1000);
1748
+
1749
+ const claims = {
1750
+ iss: this.config.issuer,
1751
+ aud: this.config.audience,
1752
+ sub: payload.userId,
1753
+ iat: now,
1754
+ exp: now + this.config.accessTokenTTL,
1755
+ jti: crypto.randomBytes(16).toString('hex'),
1756
+ ...payload.claims
1757
+ };
1758
+
1759
+ return this.sign(claims);
1760
+ }
1761
+
1762
+ // Generate refresh token
1763
+ async generateRefreshToken(payload: TokenPayload): Promise<string> {
1764
+ const now = Math.floor(Date.now() / 1000);
1765
+
1766
+ const claims = {
1767
+ iss: this.config.issuer,
1768
+ aud: this.config.audience,
1769
+ sub: payload.userId,
1770
+ iat: now,
1771
+ exp: now + this.config.refreshTokenTTL,
1772
+ jti: crypto.randomBytes(16).toString('hex'),
1773
+ type: 'refresh'
1774
+ };
1775
+
1776
+ return this.sign(claims);
1777
+ }
1778
+
1779
+ // Verify token
1780
+ async verifyToken(token: string): Promise<TokenClaims> {
1781
+ const [headerB64, payloadB64, signature] = token.split('.');
1782
+ const header = JSON.parse(Buffer.from(headerB64, 'base64url').toString());
1783
+ const claims = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
1784
+
1785
+ // Verify signature
1786
+ const valid = await this.verify(token);
1787
+ if (!valid) {
1788
+ throw new Error('Invalid token signature');
1789
+ }
1790
+
1791
+ // Verify claims
1792
+ const now = Math.floor(Date.now() / 1000);
1793
+
1794
+ if (claims.exp < now) {
1795
+ throw new Error('Token expired');
1796
+ }
1797
+
1798
+ if (claims.iss !== this.config.issuer) {
1799
+ throw new Error('Invalid issuer');
1800
+ }
1801
+
1802
+ if (claims.aud !== this.config.audience) {
1803
+ throw new Error('Invalid audience');
1804
+ }
1805
+
1806
+ return claims;
1807
+ }
1808
+
1809
+ // Encrypt sensitive data
1810
+ async encrypt(data: string): Promise<string> {
1811
+ const iv = crypto.randomBytes(16);
1812
+ const key = crypto.scryptSync(this.config.privateKey, 'salt', 32);
1813
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
1814
+
1815
+ let encrypted = cipher.update(data, 'utf8', 'base64');
1816
+ encrypted += cipher.final('base64');
1817
+
1818
+ const authTag = cipher.getAuthTag();
1819
+
1820
+ return JSON.stringify({
1821
+ iv: iv.toString('base64'),
1822
+ data: encrypted,
1823
+ tag: authTag.toString('base64')
1824
+ });
1825
+ }
1826
+
1827
+ // Decrypt sensitive data
1828
+ async decrypt(encryptedData: string): Promise<string> {
1829
+ const { iv, data, tag } = JSON.parse(encryptedData);
1830
+ const key = crypto.scryptSync(this.config.privateKey, 'salt', 32);
1831
+
1832
+ const decipher = crypto.createDecipheriv(
1833
+ 'aes-256-gcm',
1834
+ key,
1835
+ Buffer.from(iv, 'base64')
1836
+ );
1837
+ decipher.setAuthTag(Buffer.from(tag, 'base64'));
1838
+
1839
+ let decrypted = decipher.update(data, 'base64', 'utf8');
1840
+ decrypted += decipher.final('utf8');
1841
+
1842
+ return decrypted;
1843
+ }
1844
+
1845
+ private async sign(claims: Record<string, unknown>): Promise<string> {
1846
+ // Sign JWT using jose library
1847
+ return '';
1848
+ }
1849
+
1850
+ private async verify(token: string): Promise<boolean> {
1851
+ // Verify JWT signature
1852
+ return true;
1853
+ }
1854
+ }
1855
+
1856
+ interface TokenPayload {
1857
+ userId: string;
1858
+ claims?: Record<string, unknown>;
1859
+ }
1860
+
1861
+ interface TokenClaims {
1862
+ iss: string;
1863
+ aud: string;
1864
+ sub: string;
1865
+ iat: number;
1866
+ exp: number;
1867
+ jti: string;
1868
+ [key: string]: unknown;
1869
+ }
1870
+ ```
1871
+
1872
+ ---
1873
+
1874
+ ## Comandos CLI
1875
+
1876
+ ```bash
1877
+ # Configurar SSO
1878
+ /elsabro:sso configure # Configuracion interactiva
1879
+ /elsabro:sso configure --provider okta # Configurar provider especifico
1880
+ /elsabro:sso configure --import metadata.xml # Importar metadata SAML
1881
+
1882
+ # Estado y testing
1883
+ /elsabro:sso status # Ver estado de todos los providers
1884
+ /elsabro:sso status --provider azure-ad # Estado de provider especifico
1885
+ /elsabro:sso test # Test de conectividad
1886
+ /elsabro:sso test --provider google # Test provider especifico
1887
+ /elsabro:sso test --flow # Test completo de auth flow
1888
+
1889
+ # Gestion de sesiones
1890
+ /elsabro:sso sessions # Listar sesiones activas
1891
+ /elsabro:sso sessions --user user@example.com # Sesiones de usuario
1892
+ /elsabro:sso sessions terminate --id session_id # Terminar sesion
1893
+ /elsabro:sso sessions terminate --user user@example.com # Terminar todas
1894
+
1895
+ # Sincronizacion SCIM
1896
+ /elsabro:sso scim sync # Sincronizar usuarios
1897
+ /elsabro:sso scim status # Estado de sincronizacion
1898
+ /elsabro:sso scim users # Listar usuarios sincronizados
1899
+
1900
+ # Metadata y certificados
1901
+ /elsabro:sso metadata # Generar SP metadata
1902
+ /elsabro:sso metadata --format xml # Formato XML
1903
+ /elsabro:sso certs rotate # Rotar certificados
1904
+ /elsabro:sso certs status # Estado de certificados
1905
+
1906
+ # Audit
1907
+ /elsabro:sso audit # Ver eventos de auth
1908
+ /elsabro:sso audit --from 2024-01-01 # Eventos desde fecha
1909
+ /elsabro:sso audit --provider okta # Eventos por provider
1910
+ /elsabro:sso audit --failures # Solo fallos de auth
1911
+ ```
1912
+
1913
+ ---
1914
+
1915
+ ## Security Features
1916
+
1917
+ ### IP Allowlisting
1918
+
1919
+ ```typescript
1920
+ // Configurar allowlist por provider
1921
+ ssoManager.config.ipAllowlist = [
1922
+ '10.0.0.0/8', // Private network
1923
+ '192.168.0.0/16', // Private network
1924
+ '203.0.113.50', // Specific IP
1925
+ ];
1926
+ ```
1927
+
1928
+ ### Session Policies
1929
+
1930
+ ```typescript
1931
+ interface SessionPolicy {
1932
+ maxConcurrentSessions: number;
1933
+ absoluteTimeout: number; // Max session lifetime
1934
+ inactivityTimeout: number; // Idle timeout
1935
+ requireReauthFor: string[]; // Actions requiring reauth
1936
+ bindToIP: boolean; // Bind session to IP
1937
+ bindToUserAgent: boolean; // Bind session to UA
1938
+ }
1939
+ ```
1940
+
1941
+ ### MFA Enforcement
1942
+
1943
+ ```typescript
1944
+ interface MFAPolicy {
1945
+ required: boolean;
1946
+ allowedMethods: ('totp' | 'sms' | 'email' | 'push' | 'webauthn')[];
1947
+ rememberDevice: boolean;
1948
+ rememberDeviceDays: number;
1949
+ requireForSensitiveActions: boolean;
1950
+ sensitiveActions: string[];
1951
+ }
1952
+ ```
1953
+
1954
+ ### Audit Events
1955
+
1956
+ | Event | Description |
1957
+ |-------|-------------|
1958
+ | `sso.auth.initiated` | Auth flow started |
1959
+ | `sso.auth.completed` | Auth successful |
1960
+ | `sso.auth.failed` | Auth failed |
1961
+ | `sso.auth.ip_blocked` | IP not in allowlist |
1962
+ | `sso.auth.domain_blocked` | Email domain not allowed |
1963
+ | `sso.session.created` | New session created |
1964
+ | `sso.session.refreshed` | Tokens refreshed |
1965
+ | `sso.session.terminated` | Session ended |
1966
+ | `sso.logout` | User logged out |
1967
+ | `sso.mfa.required` | MFA challenge sent |
1968
+ | `sso.mfa.verified` | MFA verified |
1969
+ | `sso.provider.registered` | Provider added |
1970
+ | `sso.scim.sync.completed` | User sync finished |
1971
+
1972
+ ---
1973
+
1974
+ ## Configuracion
1975
+
1976
+ ```json
1977
+ {
1978
+ "sso": {
1979
+ "enabled": true,
1980
+ "defaultProvider": "okta",
1981
+ "sessionTimeout": 28800,
1982
+ "singleLogout": true,
1983
+ "mfaRequired": false,
1984
+ "allowedDomains": ["example.com", "corp.example.com"],
1985
+ "ipAllowlist": ["10.0.0.0/8"]
1986
+ }
1987
+ }
1988
+ ```
1989
+
1990
+ ---
1991
+
1992
+ ## Changelog
1993
+
1994
+ - **v3.6.0**: Enterprise SSO System
1995
+ - SSOManager con multi-provider support
1996
+ - SAMLProvider con SAML 2.0 completo
1997
+ - OIDCProvider con PKCE support
1998
+ - Configuraciones para Okta, Azure AD, Google, Auth0, Keycloak
1999
+ - SCIM 2.0 para user sync
2000
+ - Token encryption y signing
2001
+ - CLI commands completos