@devilsdev/rag-pipeline-utils 2.1.12 → 2.2.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/.ragrc.schema.json +218 -0
- package/README.md +178 -550
- package/bin/cli.js +8 -14
- package/contracts/reranker-contract.json +69 -0
- package/dist/index.d.ts +237 -0
- package/dist/index.mjs +22 -7
- package/package.json +18 -13
- package/src/ai/adaptive-retrieval.js +191 -0
- package/src/ai/federated-learning.js +214 -0
- package/src/ai/federation/federated-learning-coordinator.js +345 -0
- package/src/ai/federation.js +76 -0
- package/src/ai/index-fixed.js +509 -0
- package/src/ai/index.js +33 -0
- package/src/ai/model-trainer.js +217 -0
- package/src/ai/model-training.js +205 -0
- package/src/ai/module-template.js +37 -0
- package/src/ai/multimodal/multi-modal-processor.js +345 -0
- package/src/ai/multimodal-processing.js +268 -0
- package/src/ai/multimodal.js +71 -0
- package/src/ai/orchestrator.js +70 -0
- package/src/ai/retrieval/adaptive-retrieval-engine.js +398 -0
- package/src/ai/retrieval-engine.js +69 -0
- package/src/ai/training/model-training-orchestrator.js +228 -0
- package/src/ci/workflow-analyzer.js +71 -0
- package/src/cli/commands/ai-ml.js +967 -0
- package/src/cli/commands/docs.js +228 -0
- package/src/cli/commands/dx.js +538 -0
- package/src/cli/commands/plugin-hub.js +875 -0
- package/src/cli/doctor-command.js +639 -0
- package/src/cli/enhanced-cli-commands.js +945 -0
- package/src/cli/interactive-wizard.js +997 -0
- package/src/cli/plugin-marketplace-commands.js +869 -0
- package/src/cli.js +17 -0
- package/src/config/enhanced-ragrc-schema.js +632 -0
- package/src/config/load-config.js +54 -0
- package/src/config/load-plugin-config.js +69 -0
- package/src/config/normalize-config.js +105 -0
- package/src/config/schema-validator.js +315 -0
- package/src/config/validate-plugin-schema.js +35 -0
- package/src/config/validate-schema.js +113 -0
- package/src/contracts/plugin-contracts.js +33 -0
- package/src/core/create-pipeline.js +99 -0
- package/src/core/dependency-injection.js +242 -0
- package/src/core/observability/event-logger.js +241 -0
- package/src/core/observability/instrumented-pipeline.js +609 -0
- package/src/core/observability/metrics.js +497 -0
- package/src/core/observability/tracing.js +781 -0
- package/src/core/performance/benchmark.js +516 -0
- package/src/core/performance/parallel-processor.js +240 -0
- package/src/core/performance/streaming-safeguards.js +620 -0
- package/src/core/pipeline-factory.js +60 -0
- package/src/core/plugin-contracts.js +89 -0
- package/src/core/plugin-marketplace/plugin-metadata.js +485 -0
- package/src/core/plugin-marketplace/plugin-publisher.js +574 -0
- package/src/core/plugin-marketplace/plugin-registry-format.js +419 -0
- package/src/core/plugin-marketplace/version-resolver.js +61 -0
- package/src/core/plugin-registry.js +294 -0
- package/src/dag/dag-engine.js +954 -0
- package/src/dag/dag-node.js +76 -0
- package/src/dag/enhanced-dag-engine.js +822 -0
- package/src/dx/index.js +40 -0
- package/src/dx/integration-templates.js +665 -0
- package/src/dx/performance-profiler.js +968 -0
- package/src/dx/realtime-debugger.js +755 -0
- package/src/dx/visual-pipeline-builder.js +578 -0
- package/src/ecosystem/plugin-analytics-dashboard.js +778 -0
- package/src/ecosystem/plugin-certification.js +580 -0
- package/src/ecosystem/plugin-hub.js +821 -0
- package/src/enterprise/audit-logging.js +967 -0
- package/src/enterprise/data-governance.js +408 -0
- package/src/enterprise/multi-tenancy.js +496 -0
- package/src/enterprise/sso-integration.js +1030 -0
- package/src/evaluate/evaluator.js +52 -0
- package/src/evaluate/scoring.js +91 -0
- package/src/index.js +67 -0
- package/src/ingest.js +59 -0
- package/src/loader/csv-loader.js +38 -0
- package/src/loader/directory-loader.js +48 -0
- package/src/loader/html-loader.js +62 -0
- package/src/loader/markdown-loader.js +67 -0
- package/src/mocks/openai-embedder.js +119 -0
- package/src/mocks/openai-llm.js +77 -0
- package/src/mocks/pdf-loader.js +21 -0
- package/src/mocks/pinecone-retriever.js +106 -0
- package/src/observability/audit-logger.js +758 -0
- package/src/observability/dashboard-config.json +675 -0
- package/src/observability/event-logger.js +310 -0
- package/src/observability/metrics.js +691 -0
- package/src/observability/security-monitor.js +830 -0
- package/src/observability/slo-monitor.js +319 -0
- package/src/observability/telemetry.js +686 -0
- package/src/pipeline/pipeline.js +18 -0
- package/src/plugins/sample-embedder.js +53 -0
- package/src/plugins/sample-llm.js +30 -0
- package/src/plugins/sample-loader.js +24 -0
- package/src/plugins/sample-retriever.js +84 -0
- package/src/query.js +117 -0
- package/src/reranker/llm-reranker.js +111 -0
- package/src/scripts/script-utilities.js +36 -0
- package/src/security/plugin-sandbox.js +586 -0
- package/src/security/plugin-signature-verifier.js +414 -0
- package/src/security/validator.js +36 -0
- package/src/utils/ci/diagnostic-reporter.js +57 -0
- package/src/utils/logger.js +211 -0
- package/src/utils/performance-monitor.js +561 -0
- package/src/utils/plugin-scaffolder.js +447 -0
- package/src/utils/rate-limiter.js +244 -0
- package/src/utils/retry-policy.js +443 -0
- package/src/utils/retry.js +107 -0
- package/src/utils/safe-expression-evaluator.js +579 -0
- package/src/utils/secure-logging.js +338 -0
- package/src/utils/structured-logger.js +388 -0
- package/src/utils/validate-plugin-contract.js +42 -0
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise SSO Integration
|
|
3
|
+
* SAML 2.0, OAuth2, Active Directory, and identity provider support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const { EventEmitter } = require('events');
|
|
10
|
+
const jwt = require('jsonwebtoken');
|
|
11
|
+
const RateLimiter = require('../utils/rate-limiter');
|
|
12
|
+
|
|
13
|
+
class SSOManager extends EventEmitter {
|
|
14
|
+
constructor(_options = {}) {
|
|
15
|
+
super();
|
|
16
|
+
|
|
17
|
+
this._config = {
|
|
18
|
+
providers: {
|
|
19
|
+
saml: {
|
|
20
|
+
enabled: false,
|
|
21
|
+
entityId: _options.entityId || 'rag-pipeline-utils',
|
|
22
|
+
ssoUrl: _options.ssoUrl,
|
|
23
|
+
sloUrl: _options.sloUrl,
|
|
24
|
+
certificate: _options.certificate,
|
|
25
|
+
privateKey: _options.privateKey,
|
|
26
|
+
},
|
|
27
|
+
oauth2: {
|
|
28
|
+
enabled: false,
|
|
29
|
+
clientId: _options.clientId,
|
|
30
|
+
clientSecret: _options.clientSecret,
|
|
31
|
+
authorizationUrl: _options.authorizationUrl,
|
|
32
|
+
tokenUrl: _options.tokenUrl,
|
|
33
|
+
userInfoUrl: _options.userInfoUrl,
|
|
34
|
+
scopes: ['openid', 'profile', 'email'],
|
|
35
|
+
},
|
|
36
|
+
activeDirectory: {
|
|
37
|
+
enabled: false,
|
|
38
|
+
domain: _options.adDomain,
|
|
39
|
+
url: _options.adUrl,
|
|
40
|
+
baseDN: _options.baseDN,
|
|
41
|
+
bindDN: _options.bindDN,
|
|
42
|
+
bindCredentials: _options.bindCredentials,
|
|
43
|
+
},
|
|
44
|
+
oidc: {
|
|
45
|
+
enabled: false,
|
|
46
|
+
issuer: _options.oidcIssuer,
|
|
47
|
+
clientId: _options.oidcClientId,
|
|
48
|
+
clientSecret: _options.oidcClientSecret,
|
|
49
|
+
redirectUri: _options.redirectUri,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
session: {
|
|
53
|
+
timeout: _options.sessionTimeout || 8 * 60 * 60 * 1000, // 8 hours
|
|
54
|
+
renewalThreshold: _options.renewalThreshold || 30 * 60 * 1000, // 30 minutes
|
|
55
|
+
maxConcurrentSessions: _options.maxConcurrentSessions || 5,
|
|
56
|
+
},
|
|
57
|
+
security: {
|
|
58
|
+
jwtSecret: _options.jwtSecret || crypto.randomBytes(64).toString('hex'),
|
|
59
|
+
encryptionKey: _options.encryptionKey || crypto.randomBytes(32),
|
|
60
|
+
tokenExpiry: _options.tokenExpiry || 3600, // 1 hour
|
|
61
|
+
refreshTokenExpiry: _options.refreshTokenExpiry || 7 * 24 * 3600, // 7 days
|
|
62
|
+
},
|
|
63
|
+
rateLimit: {
|
|
64
|
+
enabled: _options.rateLimitEnabled !== false, // Enabled by default
|
|
65
|
+
maxLoginAttempts: _options.maxLoginAttempts || 5,
|
|
66
|
+
loginWindowMs: _options.loginWindowMs || 15 * 60 * 1000, // 15 minutes
|
|
67
|
+
blockDurationMs: _options.blockDurationMs || 60 * 60 * 1000, // 1 hour
|
|
68
|
+
},
|
|
69
|
+
..._options,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.sessions = new Map();
|
|
73
|
+
this.providers = new Map();
|
|
74
|
+
this.userCache = new Map();
|
|
75
|
+
|
|
76
|
+
// Initialize rate limiter for login attempts
|
|
77
|
+
if (this._config.rateLimit.enabled) {
|
|
78
|
+
this.rateLimiter = new RateLimiter({
|
|
79
|
+
maxAttempts: this._config.rateLimit.maxLoginAttempts,
|
|
80
|
+
windowMs: this._config.rateLimit.loginWindowMs,
|
|
81
|
+
blockDurationMs: this._config.rateLimit.blockDurationMs,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this._initializeProviders();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initialize SSO providers
|
|
90
|
+
*/
|
|
91
|
+
async _initializeProviders() {
|
|
92
|
+
if (this._config.providers.saml.enabled) {
|
|
93
|
+
this.providers.set('saml', new SAMLProvider(this._config.providers.saml));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this._config.providers.oauth2.enabled) {
|
|
97
|
+
this.providers.set(
|
|
98
|
+
'oauth2',
|
|
99
|
+
new OAuth2Provider(this._config.providers.oauth2),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (this._config.providers.activeDirectory.enabled) {
|
|
104
|
+
this.providers.set(
|
|
105
|
+
'ad',
|
|
106
|
+
new ActiveDirectoryProvider(this._config.providers.activeDirectory),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this._config.providers.oidc.enabled) {
|
|
111
|
+
this.providers.set('oidc', new OIDCProvider(this._config.providers.oidc));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Initiate SSO login
|
|
117
|
+
*/
|
|
118
|
+
async initiateLogin(tenantId, provider, _redirectUrl) {
|
|
119
|
+
const ssoProvider = this.providers.get(provider);
|
|
120
|
+
if (!ssoProvider) {
|
|
121
|
+
throw new Error(`SSO provider ${provider} not configured`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Rate limiting check
|
|
125
|
+
if (this.rateLimiter) {
|
|
126
|
+
const identifier = `${tenantId}:${provider}`;
|
|
127
|
+
const rateLimitResult = this.rateLimiter.allowRequest(identifier);
|
|
128
|
+
|
|
129
|
+
if (!rateLimitResult.allowed) {
|
|
130
|
+
this.emit('login_rate_limited', {
|
|
131
|
+
tenantId,
|
|
132
|
+
provider,
|
|
133
|
+
blockedUntil: rateLimitResult.blockedUntil,
|
|
134
|
+
retryAfter: rateLimitResult.retryAfter,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const error = new Error(
|
|
138
|
+
'Too many login attempts. Please try again later.',
|
|
139
|
+
);
|
|
140
|
+
error.code = 'RATE_LIMIT_EXCEEDED';
|
|
141
|
+
error.retryAfter = rateLimitResult.retryAfter;
|
|
142
|
+
error.blockedUntil = rateLimitResult.blockedUntil;
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const state = crypto.randomUUID();
|
|
148
|
+
const loginRequest = {
|
|
149
|
+
tenantId,
|
|
150
|
+
provider,
|
|
151
|
+
state,
|
|
152
|
+
_redirectUrl,
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Store login request for callback validation
|
|
158
|
+
this.sessions.set(state, loginRequest);
|
|
159
|
+
|
|
160
|
+
const authUrl = await ssoProvider.getAuthorizationUrl(state, _redirectUrl);
|
|
161
|
+
|
|
162
|
+
this.emit('login_initiated', { tenantId, provider, state });
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
authUrl,
|
|
166
|
+
state,
|
|
167
|
+
expiresAt: loginRequest.expiresAt,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Handle SSO callback
|
|
173
|
+
*/
|
|
174
|
+
async handleCallback(provider, _callbackData) {
|
|
175
|
+
const ssoProvider = this.providers.get(provider);
|
|
176
|
+
if (!ssoProvider) {
|
|
177
|
+
throw new Error(`SSO provider ${provider} not configured`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const loginRequest = this.sessions.get(_callbackData.state);
|
|
181
|
+
if (!loginRequest) {
|
|
182
|
+
throw new Error('Invalid or expired login request');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (loginRequest.expiresAt < Date.now()) {
|
|
186
|
+
this.sessions.delete(_callbackData.state);
|
|
187
|
+
throw new Error('Login request expired');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Exchange authorization code for tokens
|
|
191
|
+
const tokenData = await ssoProvider.exchangeCodeForTokens(_callbackData);
|
|
192
|
+
|
|
193
|
+
// Get user information
|
|
194
|
+
const userInfo = await ssoProvider.getUserInfo(tokenData._accessToken);
|
|
195
|
+
|
|
196
|
+
// Create internal user session
|
|
197
|
+
const session = await this._createUserSession(
|
|
198
|
+
loginRequest.tenantId,
|
|
199
|
+
userInfo,
|
|
200
|
+
provider,
|
|
201
|
+
tokenData,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Clean up login request
|
|
205
|
+
this.sessions.delete(_callbackData.state);
|
|
206
|
+
|
|
207
|
+
this.emit('login_completed', {
|
|
208
|
+
tenantId: loginRequest.tenantId,
|
|
209
|
+
provider,
|
|
210
|
+
userId: userInfo.id,
|
|
211
|
+
sessionId: session.id,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
session,
|
|
216
|
+
user: userInfo,
|
|
217
|
+
redirectUrl: loginRequest._redirectUrl,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create user session with JWT tokens
|
|
223
|
+
*/
|
|
224
|
+
async _createUserSession(tenantId, userInfo, provider, tokenData) {
|
|
225
|
+
const sessionId = crypto.randomUUID();
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
|
|
228
|
+
// Check concurrent session limits
|
|
229
|
+
const userSessions = Array.from(this.sessions.values()).filter(
|
|
230
|
+
(s) => s.userId === userInfo.id && s.tenantId === tenantId,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (userSessions.length >= this._config.session.maxConcurrentSessions) {
|
|
234
|
+
// Remove oldest session
|
|
235
|
+
const oldestSession = userSessions.sort(
|
|
236
|
+
(a, b) => a.createdAt - b.createdAt,
|
|
237
|
+
)[0];
|
|
238
|
+
this.sessions.delete(oldestSession.id);
|
|
239
|
+
this.emit('session_evicted', {
|
|
240
|
+
sessionId: oldestSession.id,
|
|
241
|
+
reason: 'concurrent_limit',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const session = {
|
|
246
|
+
id: sessionId,
|
|
247
|
+
tenantId,
|
|
248
|
+
userId: userInfo.id,
|
|
249
|
+
provider,
|
|
250
|
+
user: {
|
|
251
|
+
id: userInfo.id,
|
|
252
|
+
email: userInfo.email,
|
|
253
|
+
name: userInfo.name,
|
|
254
|
+
roles: userInfo.roles || [],
|
|
255
|
+
groups: userInfo.groups || [],
|
|
256
|
+
attributes: userInfo.attributes || {},
|
|
257
|
+
},
|
|
258
|
+
tokens: {
|
|
259
|
+
_accessToken: this._generateJWT({
|
|
260
|
+
sub: userInfo.id,
|
|
261
|
+
tenant: tenantId,
|
|
262
|
+
provider,
|
|
263
|
+
roles: userInfo.roles || [],
|
|
264
|
+
exp: Math.floor(now / 1000) + this._config.security.tokenExpiry,
|
|
265
|
+
}),
|
|
266
|
+
refreshToken: this._generateRefreshToken(),
|
|
267
|
+
externalTokens: {
|
|
268
|
+
accessToken: tokenData._accessToken,
|
|
269
|
+
refreshToken: tokenData.refreshToken,
|
|
270
|
+
expiresAt: tokenData.expiresAt,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
metadata: {
|
|
274
|
+
createdAt: now,
|
|
275
|
+
lastActivity: now,
|
|
276
|
+
expiresAt: now + this._config.session.timeout,
|
|
277
|
+
ipAddress: tokenData.ipAddress,
|
|
278
|
+
userAgent: tokenData.userAgent,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
this.sessions.set(sessionId, session);
|
|
283
|
+
this.userCache.set(`${tenantId}:${userInfo.id}`, userInfo);
|
|
284
|
+
|
|
285
|
+
return session;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Validate and refresh session
|
|
290
|
+
*/
|
|
291
|
+
async validateSession(sessionId, tenantId) {
|
|
292
|
+
const session = this.sessions.get(sessionId);
|
|
293
|
+
if (!session) {
|
|
294
|
+
throw new Error('Session not found');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (session.tenantId !== tenantId) {
|
|
298
|
+
throw new Error('Session tenant mismatch');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
|
|
303
|
+
// Check if session expired
|
|
304
|
+
if (session.metadata.expiresAt < now) {
|
|
305
|
+
this.sessions.delete(sessionId);
|
|
306
|
+
this.emit('session_expired', { sessionId, tenantId });
|
|
307
|
+
throw new Error('Session expired');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check if session needs renewal
|
|
311
|
+
const renewalTime =
|
|
312
|
+
session.metadata.expiresAt - this._config.session.renewalThreshold;
|
|
313
|
+
if (now > renewalTime) {
|
|
314
|
+
await this._renewSession(session);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Update last activity
|
|
318
|
+
session.metadata.lastActivity = now;
|
|
319
|
+
|
|
320
|
+
return session;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Renew session tokens
|
|
325
|
+
*/
|
|
326
|
+
async _renewSession(session) {
|
|
327
|
+
const _provider = this.providers.get(session.provider);
|
|
328
|
+
if (!_provider) {
|
|
329
|
+
throw new Error(`Provider ${session.provider} not available`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
// Refresh external tokens if needed
|
|
334
|
+
if (session.tokens.externalTokens.refreshToken) {
|
|
335
|
+
const newTokens = await _provider.refreshTokens(
|
|
336
|
+
session.tokens.externalTokens.refreshToken,
|
|
337
|
+
);
|
|
338
|
+
session.tokens.externalTokens = newTokens;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Generate new internal tokens
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
session.tokens.accessToken = this._generateJWT({
|
|
344
|
+
sub: session.userId,
|
|
345
|
+
tenant: session.tenantId,
|
|
346
|
+
provider: session.provider,
|
|
347
|
+
roles: session.user.roles,
|
|
348
|
+
exp: Math.floor(now / 1000) + this._config.security.tokenExpiry,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
session.metadata.expiresAt = now + this._config.session.timeout;
|
|
352
|
+
|
|
353
|
+
this.emit('session_renewed', {
|
|
354
|
+
sessionId: session.id,
|
|
355
|
+
tenantId: session.tenantId,
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
this.emit('session_renewal_failed', {
|
|
359
|
+
sessionId: session.id,
|
|
360
|
+
tenantId: session.tenantId,
|
|
361
|
+
error: error.message,
|
|
362
|
+
});
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Logout user and invalidate session
|
|
369
|
+
*/
|
|
370
|
+
async logout(sessionId, tenantId, _options = {}) {
|
|
371
|
+
const session = this.sessions.get(sessionId);
|
|
372
|
+
if (!session || session.tenantId !== tenantId) {
|
|
373
|
+
return { success: false, reason: 'session_not_found' };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Perform SSO logout if supported
|
|
377
|
+
if (_options.ssoLogout) {
|
|
378
|
+
const provider = this.providers.get(session.provider);
|
|
379
|
+
if (provider && provider.logout) {
|
|
380
|
+
try {
|
|
381
|
+
await provider.logout(session.tokens.externalTokens._accessToken);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
this.emit('sso_logout_failed', {
|
|
384
|
+
sessionId,
|
|
385
|
+
tenantId,
|
|
386
|
+
provider: session.provider,
|
|
387
|
+
error: error.message,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Remove session
|
|
394
|
+
this.sessions.delete(sessionId);
|
|
395
|
+
|
|
396
|
+
this.emit('logout_completed', {
|
|
397
|
+
sessionId,
|
|
398
|
+
tenantId,
|
|
399
|
+
userId: session.userId,
|
|
400
|
+
provider: session.provider,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return { success: true };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get user information from session
|
|
408
|
+
*/
|
|
409
|
+
async getUser(sessionId, tenantId) {
|
|
410
|
+
const session = await this.validateSession(sessionId, tenantId);
|
|
411
|
+
return session.user;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* List active sessions for a tenant
|
|
416
|
+
*/
|
|
417
|
+
async getActiveSessions(tenantId, _options = {}) {
|
|
418
|
+
const sessions = Array.from(this.sessions.values())
|
|
419
|
+
.filter((s) => s.tenantId === tenantId)
|
|
420
|
+
.map((s) => ({
|
|
421
|
+
id: s.id,
|
|
422
|
+
userId: s.userId,
|
|
423
|
+
provider: s.provider,
|
|
424
|
+
user: _options.includeUserInfo
|
|
425
|
+
? s.user
|
|
426
|
+
: { id: s.user.id, email: s.user.email },
|
|
427
|
+
metadata: s.metadata,
|
|
428
|
+
}));
|
|
429
|
+
|
|
430
|
+
return sessions;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Revoke all sessions for a user
|
|
435
|
+
*/
|
|
436
|
+
async revokeUserSessions(tenantId, userId, excludeSessionId = null) {
|
|
437
|
+
const userSessions = Array.from(this.sessions.entries()).filter(
|
|
438
|
+
([sessionId, session]) =>
|
|
439
|
+
session.tenantId === tenantId &&
|
|
440
|
+
session.userId === userId &&
|
|
441
|
+
sessionId !== excludeSessionId,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
for (const [sessionId] of userSessions) {
|
|
445
|
+
this.sessions.delete(sessionId);
|
|
446
|
+
this.emit('session_revoked', {
|
|
447
|
+
sessionId,
|
|
448
|
+
tenantId,
|
|
449
|
+
userId,
|
|
450
|
+
reason: 'admin_revoke',
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return { revokedSessions: userSessions.length };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Generate JWT token using secure jsonwebtoken library
|
|
459
|
+
* @param {object} payload - JWT payload
|
|
460
|
+
* @returns {string} Signed JWT token
|
|
461
|
+
*/
|
|
462
|
+
_generateJWT(payload) {
|
|
463
|
+
// Security fix: Use proper JWT library with signature verification
|
|
464
|
+
try {
|
|
465
|
+
return jwt.sign(payload, this._config.security.jwtSecret, {
|
|
466
|
+
algorithm: 'HS256',
|
|
467
|
+
issuer: 'rag-pipeline-utils',
|
|
468
|
+
audience: 'rag-pipeline-api',
|
|
469
|
+
expiresIn: this._config.security.jwtExpiresIn || '1h', // Default 1 hour expiration
|
|
470
|
+
jwtid: crypto.randomUUID(),
|
|
471
|
+
notBefore: 0,
|
|
472
|
+
});
|
|
473
|
+
} catch (error) {
|
|
474
|
+
throw new Error(`JWT generation failed: ${error.message}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Verify and decode JWT token
|
|
480
|
+
* @param {string} token - JWT token to verify
|
|
481
|
+
* @returns {object} Decoded payload
|
|
482
|
+
*/
|
|
483
|
+
_verifyJWT(token) {
|
|
484
|
+
try {
|
|
485
|
+
return jwt.verify(token, this._config.security.jwtSecret, {
|
|
486
|
+
algorithms: ['HS256'],
|
|
487
|
+
issuer: 'rag-pipeline-utils',
|
|
488
|
+
audience: 'rag-pipeline-api',
|
|
489
|
+
clockTolerance: 30, // 30 seconds tolerance for clock skew
|
|
490
|
+
});
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error.name === 'TokenExpiredError') {
|
|
493
|
+
throw new Error('JWT token has expired');
|
|
494
|
+
} else if (error.name === 'JsonWebTokenError') {
|
|
495
|
+
throw new Error('Invalid JWT token');
|
|
496
|
+
} else if (error.name === 'NotBeforeError') {
|
|
497
|
+
throw new Error('JWT token not yet valid');
|
|
498
|
+
}
|
|
499
|
+
throw new Error(`JWT verification failed: ${error.message}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Generate refresh token
|
|
505
|
+
*/
|
|
506
|
+
_generateRefreshToken() {
|
|
507
|
+
return crypto.randomBytes(32).toString('hex');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Cleanup expired sessions
|
|
512
|
+
*/
|
|
513
|
+
async cleanupExpiredSessions() {
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
const expiredSessions = [];
|
|
516
|
+
|
|
517
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
518
|
+
if (session.metadata.expiresAt < now) {
|
|
519
|
+
expiredSessions.push(sessionId);
|
|
520
|
+
this.sessions.delete(sessionId);
|
|
521
|
+
this.emit('session_expired', { sessionId, tenantId: session.tenantId });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return { cleanedSessions: expiredSessions.length };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Reset rate limit for a specific tenant/provider
|
|
530
|
+
* @param {string} tenantId - Tenant identifier
|
|
531
|
+
* @param {string} provider - Provider name
|
|
532
|
+
*/
|
|
533
|
+
resetRateLimit(tenantId, provider) {
|
|
534
|
+
if (this.rateLimiter) {
|
|
535
|
+
const identifier = `${tenantId}:${provider}`;
|
|
536
|
+
this.rateLimiter.reset(identifier);
|
|
537
|
+
this.emit('rate_limit_reset', { tenantId, provider });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Get rate limit status for a tenant/provider
|
|
543
|
+
* @param {string} tenantId - Tenant identifier
|
|
544
|
+
* @param {string} provider - Provider name
|
|
545
|
+
* @returns {Object} Rate limit status
|
|
546
|
+
*/
|
|
547
|
+
getRateLimitStatus(tenantId, provider) {
|
|
548
|
+
if (!this.rateLimiter) {
|
|
549
|
+
return { enabled: false };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const identifier = `${tenantId}:${provider}`;
|
|
553
|
+
return {
|
|
554
|
+
enabled: true,
|
|
555
|
+
...this.rateLimiter.getStatus(identifier),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Get rate limiter statistics
|
|
561
|
+
* @returns {Object} Statistics about rate limiting
|
|
562
|
+
*/
|
|
563
|
+
getRateLimiterStats() {
|
|
564
|
+
if (!this.rateLimiter) {
|
|
565
|
+
return { enabled: false };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
enabled: true,
|
|
570
|
+
...this.rateLimiter.getStats(),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Destroy SSO manager and cleanup resources
|
|
576
|
+
*/
|
|
577
|
+
destroy() {
|
|
578
|
+
if (this.rateLimiter) {
|
|
579
|
+
this.rateLimiter.destroy();
|
|
580
|
+
this.rateLimiter = null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
this.sessions.clear();
|
|
584
|
+
this.providers.clear();
|
|
585
|
+
this.userCache.clear();
|
|
586
|
+
this.removeAllListeners();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// SSO Provider implementations
|
|
591
|
+
class SAMLProvider {
|
|
592
|
+
constructor(_config) {
|
|
593
|
+
this._config = _config;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async getAuthorizationUrl(state, _redirectUrl) {
|
|
597
|
+
// SAML SSO URL construction
|
|
598
|
+
const params = new URLSearchParams({
|
|
599
|
+
SAMLRequest: this._buildSAMLRequest(state),
|
|
600
|
+
RelayState: state,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
return `${this._config.ssoUrl}?${params.toString()}`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async exchangeCodeForTokens(_callbackData) {
|
|
607
|
+
// SAML assertion processing
|
|
608
|
+
const assertion = this._validateSAMLResponse(_callbackData.SAMLResponse);
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
_accessToken: assertion.sessionIndex,
|
|
612
|
+
refreshToken: null,
|
|
613
|
+
expiresAt: assertion.notOnOrAfter,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async getUserInfo(_accessToken) {
|
|
618
|
+
// Extract user info from SAML assertion
|
|
619
|
+
return {
|
|
620
|
+
id: _accessToken.nameID,
|
|
621
|
+
email: _accessToken.attributes.email,
|
|
622
|
+
name: _accessToken.attributes.displayName,
|
|
623
|
+
roles: _accessToken.attributes.roles || [],
|
|
624
|
+
groups: _accessToken.attributes.groups || [],
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
_buildSAMLRequest(state) {
|
|
629
|
+
if (process.env.NODE_ENV === 'production') {
|
|
630
|
+
throw new Error(
|
|
631
|
+
'Production SAML implementation required - please configure SAML library',
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Development only - remove in production
|
|
636
|
+
return Buffer.from(
|
|
637
|
+
`<samlp:AuthnRequest ID="${state}" Version="2.0" IssueInstant="${new Date().toISOString()}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
638
|
+
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${this._config.entityId}</saml:Issuer>
|
|
639
|
+
<samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress" AllowCreate="true"/>
|
|
640
|
+
</samlp:AuthnRequest>`,
|
|
641
|
+
).toString('base64');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
_validateSAMLResponse(response) {
|
|
645
|
+
if (process.env.NODE_ENV === 'production') {
|
|
646
|
+
throw new Error(
|
|
647
|
+
'Production SAML validation required - please configure SAML library',
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!response) {
|
|
652
|
+
throw new Error('SAML response is required');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Development only - implement proper SAML validation in production
|
|
656
|
+
try {
|
|
657
|
+
const decodedResponse = Buffer.from(response, 'base64').toString('utf8');
|
|
658
|
+
|
|
659
|
+
if (!decodedResponse.includes('samlp:Response')) {
|
|
660
|
+
throw new Error('Invalid SAML response format');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
sessionIndex: crypto.randomUUID(),
|
|
665
|
+
nameID: 'dev-user@example.com',
|
|
666
|
+
attributes: {
|
|
667
|
+
email: 'dev-user@example.com',
|
|
668
|
+
displayName: 'Development User',
|
|
669
|
+
roles: ['user', 'dev'],
|
|
670
|
+
},
|
|
671
|
+
notOnOrAfter: Date.now() + 8 * 60 * 60 * 1000,
|
|
672
|
+
};
|
|
673
|
+
} catch (error) {
|
|
674
|
+
throw new Error(`SAML response validation failed: ${error.message}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
class OAuth2Provider {
|
|
680
|
+
constructor(_config) {
|
|
681
|
+
this._config = _config;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async getAuthorizationUrl(state, _redirectUrl) {
|
|
685
|
+
const params = new URLSearchParams({
|
|
686
|
+
response_type: 'code',
|
|
687
|
+
client_id: this._config.clientId,
|
|
688
|
+
redirect_uri: _redirectUrl,
|
|
689
|
+
scope: this._config.scopes.join(' '),
|
|
690
|
+
state,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
return `${this._config.authorizationUrl}?${params.toString()}`;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async exchangeCodeForTokens(callbackData) {
|
|
697
|
+
if (process.env.NODE_ENV === 'production') {
|
|
698
|
+
// Production implementation using real OAuth2 flow
|
|
699
|
+
const tokenRequest = {
|
|
700
|
+
method: 'POST',
|
|
701
|
+
headers: {
|
|
702
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
703
|
+
Authorization: `Basic ${Buffer.from(`${this._config.clientId}:${this._config.clientSecret}`).toString('base64')}`,
|
|
704
|
+
},
|
|
705
|
+
body: new URLSearchParams({
|
|
706
|
+
grant_type: 'authorization_code',
|
|
707
|
+
code: callbackData.code,
|
|
708
|
+
redirect_uri: callbackData.redirect_uri,
|
|
709
|
+
}),
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
try {
|
|
713
|
+
const response = await fetch(this._config.tokenUrl, tokenRequest);
|
|
714
|
+
|
|
715
|
+
if (!response.ok) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`Token exchange failed: ${response.status} ${response.statusText}`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const tokenData = await response.json();
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
_accessToken: tokenData.access_token,
|
|
725
|
+
refreshToken: tokenData.refresh_token,
|
|
726
|
+
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
727
|
+
tokenType: tokenData.token_type || 'Bearer',
|
|
728
|
+
};
|
|
729
|
+
} catch (error) {
|
|
730
|
+
throw new Error(`OAuth2 token exchange failed: ${error.message}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Development only
|
|
735
|
+
if (!callbackData.code) {
|
|
736
|
+
throw new Error('Authorization code is required');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
_accessToken: crypto.randomBytes(32).toString('hex'),
|
|
741
|
+
refreshToken: crypto.randomBytes(32).toString('hex'),
|
|
742
|
+
expiresAt: Date.now() + 3600 * 1000,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async getUserInfo(accessToken) {
|
|
747
|
+
if (process.env.NODE_ENV === 'production') {
|
|
748
|
+
// Production implementation
|
|
749
|
+
try {
|
|
750
|
+
const userResponse = await fetch(this._config.userInfoUrl, {
|
|
751
|
+
headers: {
|
|
752
|
+
Authorization: `Bearer ${accessToken}`,
|
|
753
|
+
Accept: 'application/json',
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
if (!userResponse.ok) {
|
|
758
|
+
throw new Error(`User info request failed: ${userResponse.status}`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const userData = await userResponse.json();
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
id: userData.id || userData.sub,
|
|
765
|
+
email: userData.email,
|
|
766
|
+
name: userData.name || userData.display_name,
|
|
767
|
+
roles: userData.roles || [],
|
|
768
|
+
attributes: userData,
|
|
769
|
+
};
|
|
770
|
+
} catch (error) {
|
|
771
|
+
throw new Error(`Failed to get user info: ${error.message}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Development only
|
|
776
|
+
return {
|
|
777
|
+
id: 'dev-' + crypto.randomUUID(),
|
|
778
|
+
email: 'dev-oauth-user@example.com',
|
|
779
|
+
name: 'Development OAuth User',
|
|
780
|
+
roles: ['user', 'dev'],
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async refreshTokens(refreshToken) {
|
|
785
|
+
if (process.env.NODE_ENV === 'production') {
|
|
786
|
+
// Production token refresh
|
|
787
|
+
const refreshRequest = {
|
|
788
|
+
method: 'POST',
|
|
789
|
+
headers: {
|
|
790
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
791
|
+
Authorization: `Basic ${Buffer.from(`${this._config.clientId}:${this._config.clientSecret}`).toString('base64')}`,
|
|
792
|
+
},
|
|
793
|
+
body: new URLSearchParams({
|
|
794
|
+
grant_type: 'refresh_token',
|
|
795
|
+
refresh_token: refreshToken,
|
|
796
|
+
}),
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
const response = await fetch(this._config.tokenUrl, refreshRequest);
|
|
801
|
+
|
|
802
|
+
if (!response.ok) {
|
|
803
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const tokenData = await response.json();
|
|
807
|
+
|
|
808
|
+
return {
|
|
809
|
+
_accessToken: tokenData.access_token,
|
|
810
|
+
refreshToken: tokenData.refresh_token || refreshToken,
|
|
811
|
+
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
812
|
+
};
|
|
813
|
+
} catch (error) {
|
|
814
|
+
throw new Error(`Token refresh failed: ${error.message}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Development only
|
|
819
|
+
return {
|
|
820
|
+
_accessToken: crypto.randomBytes(32).toString('hex'),
|
|
821
|
+
refreshToken: refreshToken,
|
|
822
|
+
expiresAt: Date.now() + 3600 * 1000,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
class ActiveDirectoryProvider {
|
|
828
|
+
constructor(_config) {
|
|
829
|
+
this._config = _config;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async getAuthorizationUrl(state, _redirectUrl) {
|
|
833
|
+
// AD FS OAuth2 flow
|
|
834
|
+
const params = new URLSearchParams({
|
|
835
|
+
response_type: 'code',
|
|
836
|
+
client_id: 'rag-pipeline-utils',
|
|
837
|
+
resource: this._config.url,
|
|
838
|
+
redirect_uri: _redirectUrl,
|
|
839
|
+
state,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
return `${this._config.url}/oauth2/authorize?${params.toString()}`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async exchangeCodeForTokens(callbackData) {
|
|
846
|
+
if (process.env.NODE_ENV === 'production') {
|
|
847
|
+
throw new Error(
|
|
848
|
+
'Production Active Directory integration required - please configure AD FS or Azure AD',
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Development only
|
|
853
|
+
if (!callbackData.code) {
|
|
854
|
+
throw new Error('Authorization code is required');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
_accessToken: crypto.randomBytes(32).toString('hex'),
|
|
859
|
+
refreshToken: crypto.randomBytes(32).toString('hex'),
|
|
860
|
+
expiresAt: Date.now() + 3600 * 1000,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async getUserInfo(accessToken) {
|
|
865
|
+
if (process.env.NODE_ENV === 'production') {
|
|
866
|
+
throw new Error(
|
|
867
|
+
'Production Active Directory user lookup required - please configure AD integration',
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Development only
|
|
872
|
+
return {
|
|
873
|
+
id: 'dev-' + crypto.randomUUID(),
|
|
874
|
+
email: 'dev-user@corp.com',
|
|
875
|
+
name: 'Development AD User',
|
|
876
|
+
roles: ['user', 'dev'],
|
|
877
|
+
groups: ['Domain Users', 'Developers'],
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
class OIDCProvider {
|
|
883
|
+
constructor(_config) {
|
|
884
|
+
this._config = _config;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async getAuthorizationUrl(state, _redirectUrl) {
|
|
888
|
+
const params = new URLSearchParams({
|
|
889
|
+
response_type: 'code',
|
|
890
|
+
client_id: this._config.clientId,
|
|
891
|
+
redirect_uri: _redirectUrl,
|
|
892
|
+
scope: 'openid profile email',
|
|
893
|
+
state,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
return `${this._config.issuer}/auth?${params.toString()}`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async exchangeCodeForTokens(callbackData) {
|
|
900
|
+
if (process.env.NODE_ENV === 'production') {
|
|
901
|
+
// Production OIDC implementation
|
|
902
|
+
const tokenRequest = {
|
|
903
|
+
method: 'POST',
|
|
904
|
+
headers: {
|
|
905
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
906
|
+
Authorization: `Basic ${Buffer.from(`${this._config.clientId}:${this._config.clientSecret}`).toString('base64')}`,
|
|
907
|
+
},
|
|
908
|
+
body: new URLSearchParams({
|
|
909
|
+
grant_type: 'authorization_code',
|
|
910
|
+
code: callbackData.code,
|
|
911
|
+
redirect_uri: callbackData.redirect_uri,
|
|
912
|
+
}),
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
try {
|
|
916
|
+
const response = await fetch(
|
|
917
|
+
`${this._config.issuer}/token`,
|
|
918
|
+
tokenRequest,
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
if (!response.ok) {
|
|
922
|
+
throw new Error(`OIDC token exchange failed: ${response.status}`);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const tokenData = await response.json();
|
|
926
|
+
|
|
927
|
+
return {
|
|
928
|
+
_accessToken: tokenData.access_token,
|
|
929
|
+
refreshToken: tokenData.refresh_token,
|
|
930
|
+
idToken: tokenData.id_token,
|
|
931
|
+
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
932
|
+
};
|
|
933
|
+
} catch (error) {
|
|
934
|
+
throw new Error(`OIDC token exchange failed: ${error.message}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Development only
|
|
939
|
+
if (!callbackData.code) {
|
|
940
|
+
throw new Error('Authorization code is required');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
_accessToken: crypto.randomBytes(32).toString('hex'),
|
|
945
|
+
refreshToken: crypto.randomBytes(32).toString('hex'),
|
|
946
|
+
idToken: this._generateMockIdToken(),
|
|
947
|
+
expiresAt: Date.now() + 3600 * 1000,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async getUserInfo(accessToken) {
|
|
952
|
+
if (process.env.NODE_ENV === 'production') {
|
|
953
|
+
// Production OIDC userinfo
|
|
954
|
+
try {
|
|
955
|
+
const userResponse = await fetch(`${this._config.issuer}/userinfo`, {
|
|
956
|
+
headers: {
|
|
957
|
+
Authorization: `Bearer ${accessToken}`,
|
|
958
|
+
Accept: 'application/json',
|
|
959
|
+
},
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
if (!userResponse.ok) {
|
|
963
|
+
throw new Error(
|
|
964
|
+
`OIDC userinfo request failed: ${userResponse.status}`,
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const userData = await userResponse.json();
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
id: userData.sub,
|
|
972
|
+
email: userData.email,
|
|
973
|
+
name: userData.name || userData.preferred_username,
|
|
974
|
+
roles: userData.roles || [],
|
|
975
|
+
groups: userData.groups || [],
|
|
976
|
+
attributes: userData,
|
|
977
|
+
};
|
|
978
|
+
} catch (error) {
|
|
979
|
+
throw new Error(`OIDC getUserInfo failed: ${error.message}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Development only
|
|
984
|
+
return {
|
|
985
|
+
id: 'dev-' + crypto.randomUUID(),
|
|
986
|
+
email: 'dev-user@oidc.com',
|
|
987
|
+
name: 'Development OIDC User',
|
|
988
|
+
roles: ['user', 'dev'],
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
_generateMockIdToken() {
|
|
993
|
+
if (process.env.NODE_ENV === 'production') {
|
|
994
|
+
throw new Error('Mock ID token generation not allowed in production');
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Development only - basic JWT structure
|
|
998
|
+
const header = Buffer.from(
|
|
999
|
+
JSON.stringify({ alg: 'HS256', typ: 'JWT' }),
|
|
1000
|
+
).toString('base64url');
|
|
1001
|
+
const payload = Buffer.from(
|
|
1002
|
+
JSON.stringify({
|
|
1003
|
+
sub: 'dev-user-' + crypto.randomUUID(),
|
|
1004
|
+
iss: this._config.issuer || 'dev-issuer',
|
|
1005
|
+
aud: this._config.clientId,
|
|
1006
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
1007
|
+
iat: Math.floor(Date.now() / 1000),
|
|
1008
|
+
email: 'dev-user@oidc.com',
|
|
1009
|
+
name: 'Development OIDC User',
|
|
1010
|
+
}),
|
|
1011
|
+
).toString('base64url');
|
|
1012
|
+
const signature = crypto
|
|
1013
|
+
.createHmac('sha256', 'dev-secret')
|
|
1014
|
+
.update(`${header}.${payload}`)
|
|
1015
|
+
.digest('base64url');
|
|
1016
|
+
|
|
1017
|
+
return `${header}.${payload}.${signature}`;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
module.exports = {
|
|
1022
|
+
SSOManager,
|
|
1023
|
+
SAMLProvider,
|
|
1024
|
+
OAuth2Provider,
|
|
1025
|
+
ActiveDirectoryProvider,
|
|
1026
|
+
OIDCProvider,
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// Ensure module.exports is properly defined
|
|
1030
|
+
module.exports.default = module.exports;
|