@devilsdev/rag-pipeline-utils 2.4.4 → 2.5.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.
- package/.ragrc.schema.json +83 -192
- package/CHANGELOG.md +345 -0
- package/README.md +418 -117
- package/SECURITY.md +5 -5
- package/dist/experimental.cjs +2 -0
- package/dist/experimental.d.ts +1 -0
- package/dist/experimental.mjs +5 -0
- package/dist/index.cjs +4992 -1242
- package/dist/index.d.ts +80 -2
- package/dist/index.mjs +1 -1
- package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
- package/dist/types/agentic/iterative-retriever.d.ts +85 -0
- package/dist/types/agentic/query-planner.d.ts +145 -0
- package/dist/types/agentic/self-critique.d.ts +45 -0
- package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
- package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
- package/dist/types/ai/multimodal-processing.d.ts +32 -0
- package/dist/types/ai/multimodal.d.ts +39 -0
- package/dist/types/ai/retrieval-engine.d.ts +37 -0
- package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
- package/dist/types/chunking/chunking-engine.d.ts +70 -0
- package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
- package/dist/types/chunking/strategies/recursive.d.ts +20 -0
- package/dist/types/chunking/strategies/semantic.d.ts +19 -0
- package/dist/types/chunking/strategies/sentence.d.ts +13 -0
- package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
- package/dist/types/citation/citation-tracker.d.ts +64 -0
- package/dist/types/citation/hallucination-detector.d.ts +40 -0
- package/dist/types/citation/source-mapper.d.ts +37 -0
- package/dist/types/connectors/anthropic-connector.d.ts +92 -0
- package/dist/types/connectors/base-connector.d.ts +87 -0
- package/dist/types/connectors/cohere-connector.d.ts +89 -0
- package/dist/types/connectors/connector-utils.d.ts +110 -0
- package/dist/types/connectors/local-embedder.d.ts +86 -0
- package/dist/types/connectors/memory-retriever.d.ts +91 -0
- package/dist/types/connectors/ollama-connector.d.ts +110 -0
- package/dist/types/connectors/openai-connector.d.ts +114 -0
- package/dist/types/connectors/rag-prompt.d.ts +40 -0
- package/dist/types/cost/cost-calculator.d.ts +152 -0
- package/dist/types/cost/cost-tracker.d.ts +126 -0
- package/dist/types/cost/token-budget.d.ts +110 -0
- package/dist/types/dag/core/error-context.d.ts +239 -0
- package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
- package/dist/types/dag/core/topology-validation.d.ts +226 -0
- package/dist/types/dag/dag-engine.d.ts +156 -0
- package/dist/types/dag/dag-node.d.ts +37 -0
- package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
- package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
- package/dist/types/debug/execution-tracer.d.ts +57 -0
- package/dist/types/debug/trace-inspector.d.ts +31 -0
- package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
- package/dist/types/enterprise/data-governance.d.ts +203 -0
- package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
- package/dist/types/enterprise/sso-integration.d.ts +420 -0
- package/dist/types/evaluate/context-metrics.d.ts +55 -0
- package/dist/types/evaluate/faithfulness.d.ts +41 -0
- package/dist/types/evaluate/groundedness.d.ts +60 -0
- package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
- package/dist/types/evaluate/relevance.d.ts +21 -0
- package/dist/types/evaluate/scoring.d.ts +123 -0
- package/dist/types/experimental.d.ts +15 -0
- package/dist/types/graph/entity-extractor.d.ts +84 -0
- package/dist/types/graph/graph-index.d.ts +49 -0
- package/dist/types/graph/graph-retriever.d.ts +81 -0
- package/dist/types/graph/knowledge-graph.d.ts +129 -0
- package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
- package/dist/types/guardrails/injection-detector.d.ts +47 -0
- package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
- package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
- package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
- package/dist/types/mcp/mcp-server.d.ts +85 -0
- package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
- package/dist/types/reranker/cascade-reranker.d.ts +40 -0
- package/dist/types/reranker/embedding-reranker.d.ts +77 -0
- package/dist/types/reranker/scoring-reranker.d.ts +41 -0
- package/dist/types/retrieval/bm25-search.d.ts +43 -0
- package/dist/types/retrieval/file-vector-store.d.ts +87 -0
- package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
- package/dist/types/retrieval/rank-fusion.d.ts +10 -0
- package/dist/types/security/jwt-config.d.ts +80 -0
- package/dist/types/security/jwt-validator.d.ts +195 -0
- package/dist/types/security/plugin-sandbox.d.ts +158 -0
- package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
- package/dist/types/security/validator.d.ts +5 -0
- package/dist/types/streaming/embedding-stream.d.ts +65 -0
- package/dist/types/streaming/sse-adapter.d.ts +59 -0
- package/dist/types/streaming/stream-router.d.ts +88 -0
- package/dist/types/streaming/streaming-embedder.d.ts +99 -0
- package/dist/types/streaming/websocket-adapter.d.ts +71 -0
- package/dist/types/templates/scaffold.d.ts +55 -0
- package/dist/types/templates/template-registry.d.ts +63 -0
- package/dist/types/utils/embed-query.d.ts +20 -0
- package/dist/types/utils/logger.d.ts +50 -0
- package/dist/types/utils/rate-limiter.d.ts +66 -0
- package/dist/types/utils/retry.d.ts +27 -0
- package/dist/types/utils/run-abort.d.ts +34 -0
- package/dist/types/utils/secure-logging.d.ts +51 -0
- package/dist/types/utils/structured-logger.d.ts +126 -0
- package/package.json +53 -29
- package/src/agentic/agentic-pipeline.js +75 -7
- package/src/agentic/iterative-retriever.js +47 -10
- package/src/agentic/query-planner.js +329 -78
- package/src/citation/citation-tracker.js +6 -4
- package/src/citation/hallucination-detector.js +2 -2
- package/src/citation/source-mapper.js +15 -28
- package/src/cli/commands/ai.js +101 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
- package/src/cli/commands/plugin-hub/local-commands.js +143 -0
- package/src/cli/commands/plugin-hub.js +96 -36
- package/src/cli/diagnostics/config-checker.js +76 -63
- package/src/cli/diagnostics/environment-checker.js +62 -31
- package/src/cli/diagnostics/plugin-checker.js +36 -89
- package/src/cli/doctor-command.js +39 -5
- package/src/cli/enhanced-cli-commands.js +72 -217
- package/src/cli/handlers/ai-handler.js +364 -0
- package/src/cli/handlers/diagnostics-handler.js +78 -31
- package/src/cli/handlers/ingest-handler.js +28 -22
- package/src/cli/handlers/init-handler.js +148 -25
- package/src/cli/handlers/query-handler.js +41 -26
- package/src/cli/handlers/run-handler.js +16 -167
- package/src/cli/handlers/serve-handler.js +60 -0
- package/src/cli/services/bench-stats.js +54 -0
- package/src/cli/services/benchmark.js +161 -0
- package/src/cli/services/feedback.js +155 -0
- package/src/cli/services/rag-service.js +322 -0
- package/src/cli/shared/cli-utils.js +24 -7
- package/src/cli/shared/config-context.js +21 -0
- package/src/cli/wizard/config-builder.js +81 -0
- package/src/cli/wizard/init-wizard.js +617 -0
- package/src/config/component-factory.js +125 -0
- package/src/config/load-config.js +29 -10
- package/src/config/load-plugin-config.js +46 -2
- package/src/connectors/anthropic-connector.js +93 -55
- package/src/connectors/base-connector.js +74 -19
- package/src/connectors/cohere-connector.js +88 -32
- package/src/connectors/connector-utils.js +302 -0
- package/src/connectors/local-embedder.js +43 -5
- package/src/connectors/memory-retriever.js +108 -15
- package/src/connectors/ollama-connector.js +83 -49
- package/src/connectors/openai-connector.js +108 -44
- package/src/connectors/rag-prompt.js +88 -0
- package/src/core/create-pipeline.js +209 -60
- package/src/core/plugin-registry.js +87 -11
- package/src/cost/cost-calculator.js +205 -32
- package/src/dev/dev-server.js +166 -11
- package/src/dev/hot-reload.js +40 -1
- package/src/ecosystem/plugin-checks.js +181 -0
- package/src/ecosystem/plugin-hub.js +50 -125
- package/src/ecosystem/plugin-installer.js +298 -0
- package/src/ecosystem/plugin-probe.js +127 -0
- package/src/ecosystem/plugin-scanner.js +185 -0
- package/src/ecosystem/tar-extract.js +114 -0
- package/src/enterprise/audit-logging.js +158 -29
- package/src/enterprise/sso-integration.js +715 -228
- package/src/evaluate/context-metrics.js +28 -16
- package/src/evaluate/evaluator.js +3 -3
- package/src/evaluate/faithfulness.js +18 -14
- package/src/evaluate/groundedness.js +43 -12
- package/src/evaluate/pipeline-evaluator.js +35 -8
- package/src/evaluate/scoring.js +251 -3
- package/src/experimental.js +71 -0
- package/src/guardrails/guardrails-pipeline.js +69 -7
- package/src/guardrails/injection-detector.js +283 -0
- package/src/guardrails/post-generation-guard.js +19 -0
- package/src/guardrails/pre-retrieval-guard.js +36 -36
- package/src/guardrails/retrieval-guard.js +58 -8
- package/src/index.js +36 -1
- package/src/loader/document-reader.js +91 -0
- package/src/reranker/cascade-reranker.js +3 -0
- package/src/reranker/embedding-reranker.js +136 -21
- package/src/reranker/llm-reranker.js +79 -12
- package/src/reranker/scoring-reranker.js +3 -0
- package/src/retrieval/bm25-search.js +12 -6
- package/src/retrieval/file-vector-store.js +212 -0
- package/src/retrieval/hybrid-retriever.js +22 -19
- package/src/security/jwt-config.js +13 -2
- package/src/security/jwt-validator.js +134 -30
- package/src/security/plugin-sandbox.js +18 -1
- package/src/security/plugin-signature-verifier.js +245 -70
- package/src/utils/deprecate.js +69 -0
- package/src/utils/embed-query.js +33 -0
- package/src/utils/input-sanitizer.js +19 -4
- package/src/utils/logger.js +34 -4
- package/src/utils/run-abort.js +102 -0
- package/src/ai/federated-learning.js +0 -211
- package/src/ai/federation.js +0 -71
- package/src/ai/index.js +0 -22
- package/src/ai/model-trainer.js +0 -217
- package/src/ai/model-training.js +0 -202
- package/src/ai/multimodal/multi-modal-processor.js +0 -345
- package/src/ai/orchestrator.js +0 -65
- package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
- package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
- package/src/cli/commands/ai-ml/federated-commands.js +0 -207
- package/src/cli/commands/ai-ml/index.js +0 -17
- package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
- package/src/cli/commands/ai-ml/shared.js +0 -51
- package/src/cli/commands/ai-ml/training-commands.js +0 -228
- package/src/cli/commands/ai-ml.js +0 -386
- package/src/cli/commands/docs.js +0 -228
- package/src/cli/commands/dx.js +0 -538
- package/src/cli/commands/wizard.js +0 -627
- package/src/cli/input-sanitization-middleware.js +0 -356
- package/src/cli/interactive-wizard.js +0 -373
- package/src/cli/templates/development.template.json +0 -64
- package/src/cli/templates/minimal.template.json +0 -33
- package/src/cli/templates/production.template.json +0 -104
- package/src/cli/templates/testing.template.json +0 -45
- package/src/cli/wizard/configuration.js +0 -148
- package/src/cli/wizard/observability.js +0 -79
- package/src/cli/wizard/performance.js +0 -76
- package/src/cli/wizard/plugin-selection.js +0 -297
- package/src/cli/wizard/preview-save.js +0 -172
- package/src/cli/wizard/project-setup.js +0 -66
- package/src/core/pipeline-factory.js +0 -59
- package/src/dag/enhanced-dag-engine.js +0 -894
- package/src/dx/index.js +0 -40
- package/src/dx/integration-templates.js +0 -662
- package/src/dx/performance-profiler.js +0 -968
- package/src/dx/realtime-debugger.js +0 -755
- package/src/dx/visual-pipeline-builder.js +0 -578
- package/src/ingest.js +0 -59
- package/src/pipeline/pipeline.js +0 -18
- package/src/query.js +0 -117
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enterprise SSO Integration
|
|
2
|
+
* Enterprise SSO Integration (EXPERIMENTAL - see src/experimental.js)
|
|
3
3
|
* SAML 2.0, OAuth2, Active Directory, and identity provider support
|
|
4
|
+
*
|
|
5
|
+
* Security behaviour (2.5.0):
|
|
6
|
+
* - OAuth2 and OIDC always run the real authorization-code flow with PKCE
|
|
7
|
+
* (S256). redirect_uri comes from provider configuration (`redirectUri`)
|
|
8
|
+
* or the server-side login request, never from callback data.
|
|
9
|
+
* - OIDC id_tokens are validated (JWKS signature, iss, aud, exp, nonce).
|
|
10
|
+
* - SAML refuses to authenticate (no vetted XML-signature verification).
|
|
11
|
+
* - Active Directory is only implemented as a development stub.
|
|
12
|
+
* - The development bypass (any code accepted, fixed dev identity) is OFF
|
|
13
|
+
* by default and only active with `devBypass: true` AND
|
|
14
|
+
* NODE_ENV !== 'production'; it throws in production and logs a loud
|
|
15
|
+
* warning otherwise.
|
|
4
16
|
*/
|
|
5
17
|
|
|
6
|
-
const fs = require('fs').promises;
|
|
7
|
-
const path = require('path');
|
|
8
18
|
const crypto = require('crypto');
|
|
9
19
|
const { EventEmitter } = require('events');
|
|
20
|
+
const jwt = require('jsonwebtoken');
|
|
10
21
|
const RateLimiter = require('../utils/rate-limiter');
|
|
22
|
+
const { logger } = require('../utils/logger');
|
|
11
23
|
const { JWTValidator } = require('../security/jwt-validator');
|
|
12
24
|
const { createJWTConfig } = require('../security/jwt-config');
|
|
13
25
|
|
|
@@ -17,6 +29,20 @@ class SSOManager extends EventEmitter {
|
|
|
17
29
|
|
|
18
30
|
this._config = this._buildConfig(_options);
|
|
19
31
|
|
|
32
|
+
if (this._config.devBypass === true) {
|
|
33
|
+
if (process.env.NODE_ENV === 'production') {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'SSOManager: devBypass must not be enabled when NODE_ENV=production',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
logger.warn(
|
|
39
|
+
'[SSO] WARNING: SSOManager created with devBypass: true. SSO ' +
|
|
40
|
+
'callbacks will accept any authorization code and issue sessions ' +
|
|
41
|
+
'for a fixed development identity. Never use this outside local ' +
|
|
42
|
+
'development.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
20
46
|
this.sessions = new Map();
|
|
21
47
|
this.providers = new Map();
|
|
22
48
|
this.userCache = new Map();
|
|
@@ -51,6 +77,7 @@ class SSOManager extends EventEmitter {
|
|
|
51
77
|
authorizationUrl: _options.authorizationUrl,
|
|
52
78
|
tokenUrl: _options.tokenUrl,
|
|
53
79
|
userInfoUrl: _options.userInfoUrl,
|
|
80
|
+
redirectUri: _options.redirectUri,
|
|
54
81
|
scopes: ['openid', 'profile', 'email'],
|
|
55
82
|
},
|
|
56
83
|
activeDirectory: {
|
|
@@ -60,6 +87,7 @@ class SSOManager extends EventEmitter {
|
|
|
60
87
|
baseDN: _options.baseDN,
|
|
61
88
|
bindDN: _options.bindDN,
|
|
62
89
|
bindCredentials: _options.bindCredentials,
|
|
90
|
+
redirectUri: _options.redirectUri,
|
|
63
91
|
},
|
|
64
92
|
oidc: {
|
|
65
93
|
enabled: false,
|
|
@@ -87,6 +115,8 @@ class SSOManager extends EventEmitter {
|
|
|
87
115
|
blockDurationMs: _options.blockDurationMs || 60 * 60 * 1000, // 1 hour
|
|
88
116
|
},
|
|
89
117
|
..._options,
|
|
118
|
+
// Development bypass: off unless explicitly requested
|
|
119
|
+
devBypass: _options.devBypass === true,
|
|
90
120
|
};
|
|
91
121
|
}
|
|
92
122
|
|
|
@@ -150,27 +180,36 @@ class SSOManager extends EventEmitter {
|
|
|
150
180
|
/**
|
|
151
181
|
* Initialize SSO providers
|
|
152
182
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
_initializeProviders() {
|
|
184
|
+
// Synchronous so configuration errors (e.g. devBypass in production)
|
|
185
|
+
// surface from the constructor instead of as unhandled rejections.
|
|
186
|
+
const providers = this._config.providers || {};
|
|
187
|
+
const withBypass = (providerConfig) => ({
|
|
188
|
+
...providerConfig,
|
|
189
|
+
devBypass:
|
|
190
|
+
this._config.devBypass === true || providerConfig.devBypass === true,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (providers.saml && providers.saml.enabled) {
|
|
194
|
+
this.providers.set('saml', new SAMLProvider(withBypass(providers.saml)));
|
|
156
195
|
}
|
|
157
196
|
|
|
158
|
-
if (
|
|
197
|
+
if (providers.oauth2 && providers.oauth2.enabled) {
|
|
159
198
|
this.providers.set(
|
|
160
199
|
'oauth2',
|
|
161
|
-
new OAuth2Provider(
|
|
200
|
+
new OAuth2Provider(withBypass(providers.oauth2)),
|
|
162
201
|
);
|
|
163
202
|
}
|
|
164
203
|
|
|
165
|
-
if (
|
|
204
|
+
if (providers.activeDirectory && providers.activeDirectory.enabled) {
|
|
166
205
|
this.providers.set(
|
|
167
206
|
'ad',
|
|
168
|
-
new ActiveDirectoryProvider(
|
|
207
|
+
new ActiveDirectoryProvider(withBypass(providers.activeDirectory)),
|
|
169
208
|
);
|
|
170
209
|
}
|
|
171
210
|
|
|
172
|
-
if (
|
|
173
|
-
this.providers.set('oidc', new OIDCProvider(
|
|
211
|
+
if (providers.oidc && providers.oidc.enabled) {
|
|
212
|
+
this.providers.set('oidc', new OIDCProvider(withBypass(providers.oidc)));
|
|
174
213
|
}
|
|
175
214
|
}
|
|
176
215
|
|
|
@@ -207,11 +246,19 @@ class SSOManager extends EventEmitter {
|
|
|
207
246
|
}
|
|
208
247
|
|
|
209
248
|
const state = crypto.randomUUID();
|
|
249
|
+
const { codeVerifier, codeChallenge } = createPkcePair();
|
|
210
250
|
const loginRequest = {
|
|
211
251
|
tenantId,
|
|
212
252
|
provider,
|
|
213
253
|
state,
|
|
214
254
|
_redirectUrl,
|
|
255
|
+
// Server-side only: PKCE verifier, OIDC nonce and the redirect_uri
|
|
256
|
+
// sent in the authorization request (configured value preferred).
|
|
257
|
+
codeVerifier,
|
|
258
|
+
nonce: crypto.randomBytes(16).toString('base64url'),
|
|
259
|
+
redirectUri:
|
|
260
|
+
(ssoProvider._config && ssoProvider._config.redirectUri) ||
|
|
261
|
+
_redirectUrl,
|
|
215
262
|
timestamp: Date.now(),
|
|
216
263
|
expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes
|
|
217
264
|
};
|
|
@@ -219,7 +266,11 @@ class SSOManager extends EventEmitter {
|
|
|
219
266
|
// Store login request for callback validation
|
|
220
267
|
this.sessions.set(state, loginRequest);
|
|
221
268
|
|
|
222
|
-
const authUrl = await ssoProvider.getAuthorizationUrl(state, _redirectUrl
|
|
269
|
+
const authUrl = await ssoProvider.getAuthorizationUrl(state, _redirectUrl, {
|
|
270
|
+
codeChallenge,
|
|
271
|
+
nonce: loginRequest.nonce,
|
|
272
|
+
redirectUri: loginRequest.redirectUri,
|
|
273
|
+
});
|
|
223
274
|
|
|
224
275
|
this.emit('login_initiated', { tenantId, provider, state });
|
|
225
276
|
|
|
@@ -239,21 +290,54 @@ class SSOManager extends EventEmitter {
|
|
|
239
290
|
throw new Error(`SSO provider ${provider} not configured`);
|
|
240
291
|
}
|
|
241
292
|
|
|
293
|
+
if (
|
|
294
|
+
!_callbackData ||
|
|
295
|
+
typeof _callbackData !== 'object' ||
|
|
296
|
+
typeof _callbackData.state !== 'string'
|
|
297
|
+
) {
|
|
298
|
+
throw new Error('Invalid or expired login request');
|
|
299
|
+
}
|
|
300
|
+
|
|
242
301
|
const loginRequest = this.sessions.get(_callbackData.state);
|
|
243
|
-
|
|
302
|
+
// Only login requests (not sessions) are keyed by state; they carry it.
|
|
303
|
+
if (!loginRequest || loginRequest.state !== _callbackData.state) {
|
|
244
304
|
throw new Error('Invalid or expired login request');
|
|
245
305
|
}
|
|
246
306
|
|
|
307
|
+
// A state is single-use: consume it before doing anything else
|
|
308
|
+
this.sessions.delete(_callbackData.state);
|
|
309
|
+
|
|
310
|
+
if (loginRequest.provider !== provider) {
|
|
311
|
+
this.emit('login_provider_mismatch', {
|
|
312
|
+
tenantId: loginRequest.tenantId,
|
|
313
|
+
expected: loginRequest.provider,
|
|
314
|
+
received: provider,
|
|
315
|
+
});
|
|
316
|
+
throw new Error('Login request provider mismatch');
|
|
317
|
+
}
|
|
318
|
+
|
|
247
319
|
if (loginRequest.expiresAt < Date.now()) {
|
|
248
|
-
this.sessions.delete(_callbackData.state);
|
|
249
320
|
throw new Error('Login request expired');
|
|
250
321
|
}
|
|
251
322
|
|
|
323
|
+
const loginContext = {
|
|
324
|
+
state: loginRequest.state,
|
|
325
|
+
codeVerifier: loginRequest.codeVerifier,
|
|
326
|
+
nonce: loginRequest.nonce,
|
|
327
|
+
redirectUri: loginRequest.redirectUri,
|
|
328
|
+
};
|
|
329
|
+
|
|
252
330
|
// Exchange authorization code for tokens
|
|
253
|
-
const tokenData = await ssoProvider.exchangeCodeForTokens(
|
|
331
|
+
const tokenData = await ssoProvider.exchangeCodeForTokens(
|
|
332
|
+
_callbackData,
|
|
333
|
+
loginContext,
|
|
334
|
+
);
|
|
254
335
|
|
|
255
336
|
// Get user information
|
|
256
|
-
const userInfo = await ssoProvider.getUserInfo(
|
|
337
|
+
const userInfo = await ssoProvider.getUserInfo(
|
|
338
|
+
tokenData._accessToken,
|
|
339
|
+
tokenData,
|
|
340
|
+
);
|
|
257
341
|
|
|
258
342
|
// Create internal user session
|
|
259
343
|
const session = await this._createUserSession(
|
|
@@ -263,9 +347,6 @@ class SSOManager extends EventEmitter {
|
|
|
263
347
|
tokenData,
|
|
264
348
|
);
|
|
265
349
|
|
|
266
|
-
// Clean up login request
|
|
267
|
-
this.sessions.delete(_callbackData.state);
|
|
268
|
-
|
|
269
350
|
this.emit('login_completed', {
|
|
270
351
|
tenantId: loginRequest.tenantId,
|
|
271
352
|
provider,
|
|
@@ -652,10 +733,149 @@ class SSOManager extends EventEmitter {
|
|
|
652
733
|
}
|
|
653
734
|
}
|
|
654
735
|
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
// Shared helpers
|
|
738
|
+
// ---------------------------------------------------------------------------
|
|
739
|
+
|
|
740
|
+
const SAML_UNSUPPORTED_MESSAGE =
|
|
741
|
+
'SAML authentication is not supported: SAML assertions cannot be verified ' +
|
|
742
|
+
'without a vetted XML-signature library, so SAMLProvider refuses to ' +
|
|
743
|
+
'authenticate. Use the OIDC provider instead (or devBypass: true outside ' +
|
|
744
|
+
'production for local testing only).';
|
|
745
|
+
|
|
746
|
+
const DEFAULT_ID_TOKEN_ALGORITHMS = [
|
|
747
|
+
'RS256',
|
|
748
|
+
'RS384',
|
|
749
|
+
'RS512',
|
|
750
|
+
'PS256',
|
|
751
|
+
'PS384',
|
|
752
|
+
'PS512',
|
|
753
|
+
'ES256',
|
|
754
|
+
'ES384',
|
|
755
|
+
'ES512',
|
|
756
|
+
];
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* True when the development bypass is active for a provider: it must be
|
|
760
|
+
* requested explicitly (devBypass: true) AND NODE_ENV must not be
|
|
761
|
+
* "production". Evaluated at call time.
|
|
762
|
+
* @param {Object} config - Provider configuration
|
|
763
|
+
* @returns {boolean}
|
|
764
|
+
*/
|
|
765
|
+
function isDevBypassActive(config) {
|
|
766
|
+
return (
|
|
767
|
+
Boolean(config) &&
|
|
768
|
+
config.devBypass === true &&
|
|
769
|
+
process.env.NODE_ENV !== 'production'
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Refuse devBypass in production and warn loudly when it is enabled.
|
|
775
|
+
* @param {string} providerName - Provider name for the message
|
|
776
|
+
* @param {Object} config - Provider configuration
|
|
777
|
+
*/
|
|
778
|
+
function checkDevBypass(providerName, config) {
|
|
779
|
+
if (!config || config.devBypass !== true) return;
|
|
780
|
+
if (process.env.NODE_ENV === 'production') {
|
|
781
|
+
throw new Error(
|
|
782
|
+
`${providerName}: devBypass must not be enabled when NODE_ENV=production`,
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
logger.warn(
|
|
786
|
+
`[SSO] WARNING: devBypass is ENABLED for ${providerName}. Any ` +
|
|
787
|
+
'authorization code with a valid state is accepted and a session is ' +
|
|
788
|
+
'issued for a fixed development identity. Never enable this outside ' +
|
|
789
|
+
'local development.',
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Create a PKCE (RFC 7636, S256) verifier/challenge pair.
|
|
795
|
+
* @returns {{codeVerifier: string, codeChallenge: string}}
|
|
796
|
+
*/
|
|
797
|
+
function createPkcePair() {
|
|
798
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
799
|
+
const codeChallenge = crypto
|
|
800
|
+
.createHash('sha256')
|
|
801
|
+
.update(codeVerifier)
|
|
802
|
+
.digest('base64url');
|
|
803
|
+
return { codeVerifier, codeChallenge };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Build a token-endpoint request (authorization_code grant with PKCE).
|
|
808
|
+
* Confidential clients authenticate with HTTP Basic; public clients send
|
|
809
|
+
* client_id in the body.
|
|
810
|
+
* @private
|
|
811
|
+
*/
|
|
812
|
+
function buildCodeExchangeRequest(config, code, redirectUri, codeVerifier) {
|
|
813
|
+
const body = new URLSearchParams({
|
|
814
|
+
grant_type: 'authorization_code',
|
|
815
|
+
code,
|
|
816
|
+
redirect_uri: redirectUri,
|
|
817
|
+
code_verifier: codeVerifier,
|
|
818
|
+
});
|
|
819
|
+
const headers = {
|
|
820
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
821
|
+
Accept: 'application/json',
|
|
822
|
+
};
|
|
823
|
+
if (config.clientSecret) {
|
|
824
|
+
headers.Authorization = `Basic ${Buffer.from(
|
|
825
|
+
`${config.clientId}:${config.clientSecret}`,
|
|
826
|
+
).toString('base64')}`;
|
|
827
|
+
} else {
|
|
828
|
+
body.set('client_id', config.clientId);
|
|
829
|
+
}
|
|
830
|
+
return { method: 'POST', headers, body };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Validate the common inputs of a real (non-bypass) code exchange.
|
|
835
|
+
* redirect_uri comes from provider configuration (or the server-side login
|
|
836
|
+
* request), never from callback data.
|
|
837
|
+
* @private
|
|
838
|
+
*/
|
|
839
|
+
function requireExchangeInputs(providerName, config, callbackData, ctx) {
|
|
840
|
+
if (!callbackData.code || typeof callbackData.code !== 'string') {
|
|
841
|
+
throw new Error('Authorization code is required');
|
|
842
|
+
}
|
|
843
|
+
const redirectUri = config.redirectUri || (ctx && ctx.redirectUri);
|
|
844
|
+
if (!redirectUri) {
|
|
845
|
+
throw new Error(
|
|
846
|
+
`${providerName}: redirectUri must be configured for the provider`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (!ctx || !ctx.codeVerifier) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`${providerName}: PKCE code_verifier missing; use SSOManager.initiateLogin()/handleCallback()`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return redirectUri;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function assertCallbackData(callbackData) {
|
|
858
|
+
if (!callbackData || typeof callbackData !== 'object') {
|
|
859
|
+
throw new Error('Callback data is required');
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
655
863
|
// SSO Provider implementations
|
|
656
864
|
class SAMLProvider {
|
|
657
|
-
|
|
865
|
+
/**
|
|
866
|
+
* SAML 2.0 provider.
|
|
867
|
+
*
|
|
868
|
+
* Real SAML authentication is NOT supported: verifying SAML assertions
|
|
869
|
+
* requires XML canonicalization and XML-signature verification, which
|
|
870
|
+
* must come from a vetted library rather than hand-written code. Every
|
|
871
|
+
* authentication method throws unless the explicit development bypass is
|
|
872
|
+
* active (devBypass: true and NODE_ENV !== 'production').
|
|
873
|
+
*
|
|
874
|
+
* @param {Object} _config - Provider configuration
|
|
875
|
+
*/
|
|
876
|
+
constructor(_config = {}) {
|
|
658
877
|
this._config = _config;
|
|
878
|
+
checkDevBypass('SAMLProvider', this._config);
|
|
659
879
|
}
|
|
660
880
|
|
|
661
881
|
async getAuthorizationUrl(state, _redirectUrl) {
|
|
@@ -669,35 +889,39 @@ class SAMLProvider {
|
|
|
669
889
|
}
|
|
670
890
|
|
|
671
891
|
async exchangeCodeForTokens(_callbackData) {
|
|
892
|
+
assertCallbackData(_callbackData);
|
|
672
893
|
// SAML assertion processing
|
|
673
894
|
const assertion = this._validateSAMLResponse(_callbackData.SAMLResponse);
|
|
674
895
|
|
|
675
896
|
return {
|
|
676
897
|
_accessToken: assertion.sessionIndex,
|
|
898
|
+
assertion,
|
|
677
899
|
refreshToken: null,
|
|
678
900
|
expiresAt: assertion.notOnOrAfter,
|
|
679
901
|
};
|
|
680
902
|
}
|
|
681
903
|
|
|
682
|
-
async getUserInfo(_accessToken) {
|
|
683
|
-
|
|
904
|
+
async getUserInfo(_accessToken, tokenData = {}) {
|
|
905
|
+
if (!isDevBypassActive(this._config)) {
|
|
906
|
+
throw new Error(SAML_UNSUPPORTED_MESSAGE);
|
|
907
|
+
}
|
|
908
|
+
// Extract user info from the (development-only) SAML assertion
|
|
909
|
+
const assertion = tokenData.assertion || _accessToken;
|
|
684
910
|
return {
|
|
685
|
-
id:
|
|
686
|
-
email:
|
|
687
|
-
name:
|
|
688
|
-
roles:
|
|
689
|
-
groups:
|
|
911
|
+
id: assertion.nameID,
|
|
912
|
+
email: assertion.attributes.email,
|
|
913
|
+
name: assertion.attributes.displayName,
|
|
914
|
+
roles: assertion.attributes.roles || [],
|
|
915
|
+
groups: assertion.attributes.groups || [],
|
|
690
916
|
};
|
|
691
917
|
}
|
|
692
918
|
|
|
693
919
|
_buildSAMLRequest(state) {
|
|
694
|
-
if (
|
|
695
|
-
throw new Error(
|
|
696
|
-
'Production SAML implementation required - please configure SAML library',
|
|
697
|
-
);
|
|
920
|
+
if (!isDevBypassActive(this._config)) {
|
|
921
|
+
throw new Error(SAML_UNSUPPORTED_MESSAGE);
|
|
698
922
|
}
|
|
699
923
|
|
|
700
|
-
// Development only
|
|
924
|
+
// Development bypass only
|
|
701
925
|
return Buffer.from(
|
|
702
926
|
`<samlp:AuthnRequest ID="${state}" Version="2.0" IssueInstant="${new Date().toISOString()}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
703
927
|
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${this._config.entityId}</saml:Issuer>
|
|
@@ -707,17 +931,15 @@ class SAMLProvider {
|
|
|
707
931
|
}
|
|
708
932
|
|
|
709
933
|
_validateSAMLResponse(response) {
|
|
710
|
-
if (
|
|
711
|
-
throw new Error(
|
|
712
|
-
'Production SAML validation required - please configure SAML library',
|
|
713
|
-
);
|
|
934
|
+
if (!isDevBypassActive(this._config)) {
|
|
935
|
+
throw new Error(SAML_UNSUPPORTED_MESSAGE);
|
|
714
936
|
}
|
|
715
937
|
|
|
716
938
|
if (!response) {
|
|
717
939
|
throw new Error('SAML response is required');
|
|
718
940
|
}
|
|
719
941
|
|
|
720
|
-
// Development only
|
|
942
|
+
// Development bypass only: the response is NOT verified
|
|
721
943
|
try {
|
|
722
944
|
const decodedResponse = Buffer.from(response, 'base64').toString('utf8');
|
|
723
945
|
|
|
@@ -742,179 +964,217 @@ class SAMLProvider {
|
|
|
742
964
|
}
|
|
743
965
|
|
|
744
966
|
class OAuth2Provider {
|
|
745
|
-
|
|
746
|
-
|
|
967
|
+
/**
|
|
968
|
+
* OAuth 2.0 authorization-code provider with PKCE (S256).
|
|
969
|
+
*
|
|
970
|
+
* @param {Object} _config - clientId, clientSecret (optional for public
|
|
971
|
+
* clients), authorizationUrl, tokenUrl, userInfoUrl, redirectUri,
|
|
972
|
+
* scopes, devBypass
|
|
973
|
+
*/
|
|
974
|
+
constructor(_config = {}) {
|
|
975
|
+
this._config = { scopes: ['openid', 'profile', 'email'], ..._config };
|
|
976
|
+
checkDevBypass('OAuth2Provider', this._config);
|
|
747
977
|
}
|
|
748
978
|
|
|
749
|
-
|
|
979
|
+
/**
|
|
980
|
+
* @param {string} state - Opaque state bound to the login request
|
|
981
|
+
* @param {string} [_redirectUrl] - Fallback redirect_uri when none is configured
|
|
982
|
+
* @param {Object} [loginContext] - { codeChallenge, redirectUri } from SSOManager
|
|
983
|
+
*/
|
|
984
|
+
async getAuthorizationUrl(state, _redirectUrl, loginContext = {}) {
|
|
750
985
|
const params = new URLSearchParams({
|
|
751
986
|
response_type: 'code',
|
|
752
987
|
client_id: this._config.clientId,
|
|
753
|
-
redirect_uri:
|
|
988
|
+
redirect_uri:
|
|
989
|
+
this._config.redirectUri || loginContext.redirectUri || _redirectUrl,
|
|
754
990
|
scope: this._config.scopes.join(' '),
|
|
755
991
|
state,
|
|
756
992
|
});
|
|
993
|
+
if (loginContext.codeChallenge) {
|
|
994
|
+
params.set('code_challenge', loginContext.codeChallenge);
|
|
995
|
+
params.set('code_challenge_method', 'S256');
|
|
996
|
+
}
|
|
757
997
|
|
|
758
998
|
return `${this._config.authorizationUrl}?${params.toString()}`;
|
|
759
999
|
}
|
|
760
1000
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
grant_type: 'authorization_code',
|
|
772
|
-
code: callbackData.code,
|
|
773
|
-
redirect_uri: callbackData.redirect_uri,
|
|
774
|
-
}),
|
|
775
|
-
};
|
|
1001
|
+
/**
|
|
1002
|
+
* Exchange the authorization code. redirect_uri is taken from provider
|
|
1003
|
+
* configuration (or the server-side login request), never from
|
|
1004
|
+
* callbackData, and the PKCE code_verifier from the login request.
|
|
1005
|
+
*
|
|
1006
|
+
* @param {Object} callbackData - { code, state }
|
|
1007
|
+
* @param {Object} [loginContext] - { codeVerifier, redirectUri }
|
|
1008
|
+
*/
|
|
1009
|
+
async exchangeCodeForTokens(callbackData, loginContext = {}) {
|
|
1010
|
+
assertCallbackData(callbackData);
|
|
776
1011
|
|
|
777
|
-
|
|
778
|
-
|
|
1012
|
+
if (isDevBypassActive(this._config)) {
|
|
1013
|
+
if (!callbackData.code) {
|
|
1014
|
+
throw new Error('Authorization code is required');
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
_accessToken: crypto.randomBytes(32).toString('hex'),
|
|
1018
|
+
refreshToken: crypto.randomBytes(32).toString('hex'),
|
|
1019
|
+
expiresAt: Date.now() + 3600 * 1000,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
779
1022
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1023
|
+
const redirectUri = requireExchangeInputs(
|
|
1024
|
+
'OAuth2Provider',
|
|
1025
|
+
this._config,
|
|
1026
|
+
callbackData,
|
|
1027
|
+
loginContext,
|
|
1028
|
+
);
|
|
1029
|
+
const tokenRequest = buildCodeExchangeRequest(
|
|
1030
|
+
this._config,
|
|
1031
|
+
callbackData.code,
|
|
1032
|
+
redirectUri,
|
|
1033
|
+
loginContext.codeVerifier,
|
|
1034
|
+
);
|
|
785
1035
|
|
|
786
|
-
|
|
1036
|
+
try {
|
|
1037
|
+
const response = await fetch(this._config.tokenUrl, tokenRequest);
|
|
787
1038
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
tokenType: tokenData.token_type || 'Bearer',
|
|
793
|
-
};
|
|
794
|
-
} catch (error) {
|
|
795
|
-
throw new Error(`OAuth2 token exchange failed: ${error.message}`);
|
|
1039
|
+
if (!response.ok) {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
`Token exchange failed: ${response.status} ${response.statusText}`,
|
|
1042
|
+
);
|
|
796
1043
|
}
|
|
797
|
-
}
|
|
798
1044
|
|
|
799
|
-
|
|
800
|
-
if (!callbackData.code) {
|
|
801
|
-
throw new Error('Authorization code is required');
|
|
802
|
-
}
|
|
1045
|
+
const tokenData = await response.json();
|
|
803
1046
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1047
|
+
return {
|
|
1048
|
+
_accessToken: tokenData.access_token,
|
|
1049
|
+
refreshToken: tokenData.refresh_token,
|
|
1050
|
+
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
1051
|
+
tokenType: tokenData.token_type || 'Bearer',
|
|
1052
|
+
};
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
throw new Error(`OAuth2 token exchange failed: ${error.message}`);
|
|
1055
|
+
}
|
|
809
1056
|
}
|
|
810
1057
|
|
|
811
1058
|
async getUserInfo(accessToken) {
|
|
812
|
-
if (
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
});
|
|
1059
|
+
if (isDevBypassActive(this._config)) {
|
|
1060
|
+
return {
|
|
1061
|
+
id: 'dev-' + crypto.randomUUID(),
|
|
1062
|
+
email: 'dev-oauth-user@example.com',
|
|
1063
|
+
name: 'Development OAuth User',
|
|
1064
|
+
roles: ['user', 'dev'],
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
821
1067
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1068
|
+
try {
|
|
1069
|
+
const userResponse = await fetch(this._config.userInfoUrl, {
|
|
1070
|
+
headers: {
|
|
1071
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1072
|
+
Accept: 'application/json',
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
825
1075
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return {
|
|
829
|
-
id: userData.id || userData.sub,
|
|
830
|
-
email: userData.email,
|
|
831
|
-
name: userData.name || userData.display_name,
|
|
832
|
-
roles: userData.roles || [],
|
|
833
|
-
attributes: userData,
|
|
834
|
-
};
|
|
835
|
-
} catch (error) {
|
|
836
|
-
throw new Error(`Failed to get user info: ${error.message}`);
|
|
1076
|
+
if (!userResponse.ok) {
|
|
1077
|
+
throw new Error(`User info request failed: ${userResponse.status}`);
|
|
837
1078
|
}
|
|
838
|
-
}
|
|
839
1079
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1080
|
+
const userData = await userResponse.json();
|
|
1081
|
+
|
|
1082
|
+
return {
|
|
1083
|
+
id: userData.id || userData.sub,
|
|
1084
|
+
email: userData.email,
|
|
1085
|
+
name: userData.name || userData.display_name,
|
|
1086
|
+
roles: userData.roles || [],
|
|
1087
|
+
attributes: userData,
|
|
1088
|
+
};
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
throw new Error(`Failed to get user info: ${error.message}`);
|
|
1091
|
+
}
|
|
847
1092
|
}
|
|
848
1093
|
|
|
849
1094
|
async refreshTokens(refreshToken) {
|
|
850
|
-
if (
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
856
|
-
Authorization: `Basic ${Buffer.from(`${this._config.clientId}:${this._config.clientSecret}`).toString('base64')}`,
|
|
857
|
-
},
|
|
858
|
-
body: new URLSearchParams({
|
|
859
|
-
grant_type: 'refresh_token',
|
|
860
|
-
refresh_token: refreshToken,
|
|
861
|
-
}),
|
|
1095
|
+
if (isDevBypassActive(this._config)) {
|
|
1096
|
+
return {
|
|
1097
|
+
_accessToken: crypto.randomBytes(32).toString('hex'),
|
|
1098
|
+
refreshToken: refreshToken,
|
|
1099
|
+
expiresAt: Date.now() + 3600 * 1000,
|
|
862
1100
|
};
|
|
1101
|
+
}
|
|
863
1102
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1103
|
+
const body = new URLSearchParams({
|
|
1104
|
+
grant_type: 'refresh_token',
|
|
1105
|
+
refresh_token: refreshToken,
|
|
1106
|
+
});
|
|
1107
|
+
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
|
1108
|
+
if (this._config.clientSecret) {
|
|
1109
|
+
headers.Authorization = `Basic ${Buffer.from(
|
|
1110
|
+
`${this._config.clientId}:${this._config.clientSecret}`,
|
|
1111
|
+
).toString('base64')}`;
|
|
1112
|
+
} else {
|
|
1113
|
+
body.set('client_id', this._config.clientId);
|
|
1114
|
+
}
|
|
870
1115
|
|
|
871
|
-
|
|
1116
|
+
try {
|
|
1117
|
+
const response = await fetch(this._config.tokenUrl, {
|
|
1118
|
+
method: 'POST',
|
|
1119
|
+
headers,
|
|
1120
|
+
body,
|
|
1121
|
+
});
|
|
872
1122
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
refreshToken: tokenData.refresh_token || refreshToken,
|
|
876
|
-
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
877
|
-
};
|
|
878
|
-
} catch (error) {
|
|
879
|
-
throw new Error(`Token refresh failed: ${error.message}`);
|
|
1123
|
+
if (!response.ok) {
|
|
1124
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
880
1125
|
}
|
|
881
|
-
}
|
|
882
1126
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1127
|
+
const tokenData = await response.json();
|
|
1128
|
+
|
|
1129
|
+
return {
|
|
1130
|
+
_accessToken: tokenData.access_token,
|
|
1131
|
+
refreshToken: tokenData.refresh_token || refreshToken,
|
|
1132
|
+
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
1133
|
+
};
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
throw new Error(`Token refresh failed: ${error.message}`);
|
|
1136
|
+
}
|
|
889
1137
|
}
|
|
890
1138
|
}
|
|
891
1139
|
|
|
892
1140
|
class ActiveDirectoryProvider {
|
|
893
|
-
|
|
1141
|
+
/**
|
|
1142
|
+
* Active Directory provider. Only the development bypass is implemented;
|
|
1143
|
+
* for AD FS / Entra ID use the OIDC provider, which both support.
|
|
1144
|
+
* @param {Object} _config - Provider configuration
|
|
1145
|
+
*/
|
|
1146
|
+
constructor(_config = {}) {
|
|
894
1147
|
this._config = _config;
|
|
1148
|
+
checkDevBypass('ActiveDirectoryProvider', this._config);
|
|
895
1149
|
}
|
|
896
1150
|
|
|
897
|
-
async getAuthorizationUrl(state, _redirectUrl) {
|
|
1151
|
+
async getAuthorizationUrl(state, _redirectUrl, loginContext = {}) {
|
|
898
1152
|
// AD FS OAuth2 flow
|
|
899
1153
|
const params = new URLSearchParams({
|
|
900
1154
|
response_type: 'code',
|
|
901
|
-
client_id: 'rag-pipeline-utils',
|
|
1155
|
+
client_id: this._config.clientId || 'rag-pipeline-utils',
|
|
902
1156
|
resource: this._config.url,
|
|
903
|
-
redirect_uri:
|
|
1157
|
+
redirect_uri:
|
|
1158
|
+
this._config.redirectUri || loginContext.redirectUri || _redirectUrl,
|
|
904
1159
|
state,
|
|
905
1160
|
});
|
|
1161
|
+
if (loginContext.codeChallenge) {
|
|
1162
|
+
params.set('code_challenge', loginContext.codeChallenge);
|
|
1163
|
+
params.set('code_challenge_method', 'S256');
|
|
1164
|
+
}
|
|
906
1165
|
|
|
907
1166
|
return `${this._config.url}/oauth2/authorize?${params.toString()}`;
|
|
908
1167
|
}
|
|
909
1168
|
|
|
910
1169
|
async exchangeCodeForTokens(callbackData) {
|
|
911
|
-
|
|
1170
|
+
assertCallbackData(callbackData);
|
|
1171
|
+
if (!isDevBypassActive(this._config)) {
|
|
912
1172
|
throw new Error(
|
|
913
|
-
'Production Active Directory integration required - please configure AD FS or Azure AD',
|
|
1173
|
+
'Production Active Directory integration required - please configure AD FS or Azure AD (use the OIDC provider)',
|
|
914
1174
|
);
|
|
915
1175
|
}
|
|
916
1176
|
|
|
917
|
-
// Development only
|
|
1177
|
+
// Development bypass only
|
|
918
1178
|
if (!callbackData.code) {
|
|
919
1179
|
throw new Error('Authorization code is required');
|
|
920
1180
|
}
|
|
@@ -926,14 +1186,14 @@ class ActiveDirectoryProvider {
|
|
|
926
1186
|
};
|
|
927
1187
|
}
|
|
928
1188
|
|
|
929
|
-
async getUserInfo(
|
|
930
|
-
if (
|
|
1189
|
+
async getUserInfo(_accessToken) {
|
|
1190
|
+
if (!isDevBypassActive(this._config)) {
|
|
931
1191
|
throw new Error(
|
|
932
|
-
'Production Active Directory user lookup required - please configure AD integration',
|
|
1192
|
+
'Production Active Directory user lookup required - please configure AD integration (use the OIDC provider)',
|
|
933
1193
|
);
|
|
934
1194
|
}
|
|
935
1195
|
|
|
936
|
-
// Development only
|
|
1196
|
+
// Development bypass only
|
|
937
1197
|
return {
|
|
938
1198
|
id: 'dev-' + crypto.randomUUID(),
|
|
939
1199
|
email: 'dev-user@corp.com',
|
|
@@ -945,79 +1205,306 @@ class ActiveDirectoryProvider {
|
|
|
945
1205
|
}
|
|
946
1206
|
|
|
947
1207
|
class OIDCProvider {
|
|
948
|
-
|
|
1208
|
+
/**
|
|
1209
|
+
* OpenID Connect provider (authorization code + PKCE, validated id_token).
|
|
1210
|
+
*
|
|
1211
|
+
* Endpoints and keys come from discovery
|
|
1212
|
+
* (`<issuer>/.well-known/openid-configuration`) unless configured:
|
|
1213
|
+
* authorizationUrl, tokenUrl, userInfoUrl, jwksUri, or `jwks` (a JWK Set
|
|
1214
|
+
* `{ keys: [...] }` or an array of JWKs) to avoid fetching keys.
|
|
1215
|
+
*
|
|
1216
|
+
* The id_token must be signed by a key from the provider JWKS (selected
|
|
1217
|
+
* by `kid`) with an asymmetric algorithm (`idTokenAlgorithms`, default
|
|
1218
|
+
* RS/PS/ES 256-512), and its iss, aud (clientId), exp and nonce are
|
|
1219
|
+
* checked.
|
|
1220
|
+
*
|
|
1221
|
+
* @param {Object} _config - issuer, clientId, clientSecret, redirectUri,
|
|
1222
|
+
* scopes, jwks, jwksUri, authorizationUrl, tokenUrl, userInfoUrl,
|
|
1223
|
+
* idTokenAlgorithms, clockTolerance (seconds, default 60), devBypass
|
|
1224
|
+
*/
|
|
1225
|
+
constructor(_config = {}) {
|
|
949
1226
|
this._config = _config;
|
|
1227
|
+
this._metadata = null;
|
|
1228
|
+
this._jwks = null;
|
|
1229
|
+
checkDevBypass('OIDCProvider', this._config);
|
|
950
1230
|
}
|
|
951
1231
|
|
|
952
|
-
|
|
1232
|
+
/**
|
|
1233
|
+
* Resolve provider metadata (configured values override discovery).
|
|
1234
|
+
* @returns {Promise<Object>}
|
|
1235
|
+
*/
|
|
1236
|
+
async getMetadata() {
|
|
1237
|
+
if (this._metadata) return this._metadata;
|
|
1238
|
+
|
|
1239
|
+
const configured = {
|
|
1240
|
+
issuer: this._config.issuer,
|
|
1241
|
+
authorization_endpoint: this._config.authorizationUrl,
|
|
1242
|
+
token_endpoint: this._config.tokenUrl,
|
|
1243
|
+
userinfo_endpoint: this._config.userInfoUrl,
|
|
1244
|
+
jwks_uri: this._config.jwksUri,
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
const needsDiscovery =
|
|
1248
|
+
!configured.authorization_endpoint ||
|
|
1249
|
+
!configured.token_endpoint ||
|
|
1250
|
+
(!configured.jwks_uri && !this._config.jwks);
|
|
1251
|
+
|
|
1252
|
+
let discovered = {};
|
|
1253
|
+
if (needsDiscovery) {
|
|
1254
|
+
if (!this._config.issuer) {
|
|
1255
|
+
throw new Error('OIDC issuer must be configured');
|
|
1256
|
+
}
|
|
1257
|
+
const url = `${this._config.issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`;
|
|
1258
|
+
const response = await fetch(url, {
|
|
1259
|
+
headers: { Accept: 'application/json' },
|
|
1260
|
+
});
|
|
1261
|
+
if (!response.ok) {
|
|
1262
|
+
throw new Error(`OIDC discovery failed: ${response.status}`);
|
|
1263
|
+
}
|
|
1264
|
+
discovered = await response.json();
|
|
1265
|
+
if (discovered.issuer !== this._config.issuer) {
|
|
1266
|
+
throw new Error(
|
|
1267
|
+
`OIDC discovery issuer mismatch: expected ${this._config.issuer}, got ${discovered.issuer}`,
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const metadata = { ...discovered };
|
|
1273
|
+
for (const [key, value] of Object.entries(configured)) {
|
|
1274
|
+
if (value) metadata[key] = value;
|
|
1275
|
+
}
|
|
1276
|
+
this._metadata = metadata;
|
|
1277
|
+
return metadata;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async getAuthorizationUrl(state, _redirectUrl, loginContext = {}) {
|
|
1281
|
+
const metadata = await this.getMetadata();
|
|
953
1282
|
const params = new URLSearchParams({
|
|
954
1283
|
response_type: 'code',
|
|
955
1284
|
client_id: this._config.clientId,
|
|
956
|
-
redirect_uri:
|
|
957
|
-
|
|
1285
|
+
redirect_uri:
|
|
1286
|
+
this._config.redirectUri || loginContext.redirectUri || _redirectUrl,
|
|
1287
|
+
scope: (this._config.scopes || ['openid', 'profile', 'email']).join(' '),
|
|
958
1288
|
state,
|
|
959
1289
|
});
|
|
1290
|
+
if (loginContext.nonce) {
|
|
1291
|
+
params.set('nonce', loginContext.nonce);
|
|
1292
|
+
}
|
|
1293
|
+
if (loginContext.codeChallenge) {
|
|
1294
|
+
params.set('code_challenge', loginContext.codeChallenge);
|
|
1295
|
+
params.set('code_challenge_method', 'S256');
|
|
1296
|
+
}
|
|
960
1297
|
|
|
961
|
-
return `${
|
|
1298
|
+
return `${metadata.authorization_endpoint}?${params.toString()}`;
|
|
962
1299
|
}
|
|
963
1300
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
Authorization: `Basic ${Buffer.from(`${this._config.clientId}:${this._config.clientSecret}`).toString('base64')}`,
|
|
972
|
-
},
|
|
973
|
-
body: new URLSearchParams({
|
|
974
|
-
grant_type: 'authorization_code',
|
|
975
|
-
code: callbackData.code,
|
|
976
|
-
redirect_uri: callbackData.redirect_uri,
|
|
977
|
-
}),
|
|
978
|
-
};
|
|
1301
|
+
/**
|
|
1302
|
+
* Exchange the code and validate the returned id_token.
|
|
1303
|
+
* @param {Object} callbackData - { code, state }
|
|
1304
|
+
* @param {Object} [loginContext] - { codeVerifier, nonce, redirectUri }
|
|
1305
|
+
*/
|
|
1306
|
+
async exchangeCodeForTokens(callbackData, loginContext = {}) {
|
|
1307
|
+
assertCallbackData(callbackData);
|
|
979
1308
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1309
|
+
if (isDevBypassActive(this._config)) {
|
|
1310
|
+
if (!callbackData.code) {
|
|
1311
|
+
throw new Error('Authorization code is required');
|
|
1312
|
+
}
|
|
1313
|
+
return {
|
|
1314
|
+
_accessToken: crypto.randomBytes(32).toString('hex'),
|
|
1315
|
+
refreshToken: crypto.randomBytes(32).toString('hex'),
|
|
1316
|
+
idToken: this._generateMockIdToken(),
|
|
1317
|
+
expiresAt: Date.now() + 3600 * 1000,
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
985
1320
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1321
|
+
const redirectUri = requireExchangeInputs(
|
|
1322
|
+
'OIDCProvider',
|
|
1323
|
+
this._config,
|
|
1324
|
+
callbackData,
|
|
1325
|
+
loginContext,
|
|
1326
|
+
);
|
|
1327
|
+
if (!loginContext.nonce) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
'OIDCProvider: nonce missing; use SSOManager.initiateLogin()/handleCallback()',
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
989
1332
|
|
|
990
|
-
|
|
1333
|
+
let tokenData;
|
|
1334
|
+
try {
|
|
1335
|
+
const metadata = await this.getMetadata();
|
|
1336
|
+
const response = await fetch(
|
|
1337
|
+
metadata.token_endpoint,
|
|
1338
|
+
buildCodeExchangeRequest(
|
|
1339
|
+
this._config,
|
|
1340
|
+
callbackData.code,
|
|
1341
|
+
redirectUri,
|
|
1342
|
+
loginContext.codeVerifier,
|
|
1343
|
+
),
|
|
1344
|
+
);
|
|
991
1345
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
refreshToken: tokenData.refresh_token,
|
|
995
|
-
idToken: tokenData.id_token,
|
|
996
|
-
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
997
|
-
};
|
|
998
|
-
} catch (error) {
|
|
999
|
-
throw new Error(`OIDC token exchange failed: ${error.message}`);
|
|
1346
|
+
if (!response.ok) {
|
|
1347
|
+
throw new Error(`OIDC token exchange failed: ${response.status}`);
|
|
1000
1348
|
}
|
|
1349
|
+
|
|
1350
|
+
tokenData = await response.json();
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
throw new Error(`OIDC token exchange failed: ${error.message}`);
|
|
1001
1353
|
}
|
|
1002
1354
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
throw new Error('Authorization code is required');
|
|
1355
|
+
if (!tokenData.id_token) {
|
|
1356
|
+
throw new Error('OIDC token response did not include an id_token');
|
|
1006
1357
|
}
|
|
1358
|
+
const claims = await this.validateIdToken(tokenData.id_token, {
|
|
1359
|
+
nonce: loginContext.nonce,
|
|
1360
|
+
});
|
|
1007
1361
|
|
|
1008
1362
|
return {
|
|
1009
|
-
_accessToken:
|
|
1010
|
-
refreshToken:
|
|
1011
|
-
idToken:
|
|
1012
|
-
|
|
1363
|
+
_accessToken: tokenData.access_token,
|
|
1364
|
+
refreshToken: tokenData.refresh_token,
|
|
1365
|
+
idToken: tokenData.id_token,
|
|
1366
|
+
claims,
|
|
1367
|
+
expiresAt: Date.now() + tokenData.expires_in * 1000,
|
|
1013
1368
|
};
|
|
1014
1369
|
}
|
|
1015
1370
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1371
|
+
/**
|
|
1372
|
+
* Validate an id_token: signature (provider JWKS), iss, aud, exp, nonce.
|
|
1373
|
+
* @param {string} idToken - Compact JWS
|
|
1374
|
+
* @param {Object} options - { nonce } expected nonce (required)
|
|
1375
|
+
* @returns {Promise<Object>} Verified claims
|
|
1376
|
+
*/
|
|
1377
|
+
async validateIdToken(idToken, { nonce } = {}) {
|
|
1378
|
+
try {
|
|
1379
|
+
if (!nonce) {
|
|
1380
|
+
throw new Error('expected nonce is required');
|
|
1381
|
+
}
|
|
1382
|
+
// jsonwebtoken silently skips iss/aud checks for falsy values
|
|
1383
|
+
if (!this._config.issuer) {
|
|
1384
|
+
throw new Error('issuer must be configured');
|
|
1385
|
+
}
|
|
1386
|
+
if (!this._config.clientId) {
|
|
1387
|
+
throw new Error('clientId must be configured');
|
|
1388
|
+
}
|
|
1389
|
+
const decoded = jwt.decode(idToken, { complete: true });
|
|
1390
|
+
if (!decoded || !decoded.header || !decoded.payload) {
|
|
1391
|
+
throw new Error('malformed id_token');
|
|
1392
|
+
}
|
|
1393
|
+
const { header, payload } = decoded;
|
|
1394
|
+
const allowed =
|
|
1395
|
+
this._config.idTokenAlgorithms || DEFAULT_ID_TOKEN_ALGORITHMS;
|
|
1396
|
+
if (!allowed.includes(header.alg)) {
|
|
1397
|
+
throw new Error(`algorithm ${header.alg} is not allowed`);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const jwk = await this._selectJwk(header);
|
|
1401
|
+
if (jwk.alg && jwk.alg !== header.alg) {
|
|
1402
|
+
throw new Error('JWK algorithm does not match the token');
|
|
1403
|
+
}
|
|
1404
|
+
const key = crypto.createPublicKey({ key: jwk, format: 'jwk' });
|
|
1405
|
+
|
|
1406
|
+
if (typeof payload.exp !== 'number') {
|
|
1407
|
+
throw new Error('id_token has no exp claim');
|
|
1408
|
+
}
|
|
1409
|
+
if (!payload.sub) {
|
|
1410
|
+
throw new Error('id_token has no sub claim');
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const claims = jwt.verify(idToken, key, {
|
|
1414
|
+
algorithms: [header.alg],
|
|
1415
|
+
issuer: this._config.issuer,
|
|
1416
|
+
audience: this._config.clientId,
|
|
1417
|
+
nonce,
|
|
1418
|
+
clockTolerance: this._config.clockTolerance ?? 60,
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
if (
|
|
1422
|
+
Array.isArray(claims.aud) &&
|
|
1423
|
+
claims.aud.length > 1 &&
|
|
1424
|
+
claims.azp !== this._config.clientId
|
|
1425
|
+
) {
|
|
1426
|
+
throw new Error('azp does not match client_id');
|
|
1427
|
+
}
|
|
1428
|
+
return claims;
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
throw new Error(`OIDC id_token validation failed: ${error.message}`);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Pick the verification key for a token header from the JWKS.
|
|
1436
|
+
* Refetches the JWKS once when the kid is unknown (key rotation).
|
|
1437
|
+
* @private
|
|
1438
|
+
*/
|
|
1439
|
+
async _selectJwk(header) {
|
|
1440
|
+
const find = (keys) => {
|
|
1441
|
+
const candidates = keys.filter(
|
|
1442
|
+
(k) => (!k.use || k.use === 'sig') && k.kty !== 'oct',
|
|
1443
|
+
);
|
|
1444
|
+
if (header.kid) {
|
|
1445
|
+
return candidates.find((k) => k.kid === header.kid);
|
|
1446
|
+
}
|
|
1447
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
let jwk = find(await this._getJwks(false));
|
|
1451
|
+
if (!jwk && !this._config.jwks) {
|
|
1452
|
+
jwk = find(await this._getJwks(true));
|
|
1453
|
+
}
|
|
1454
|
+
if (!jwk) {
|
|
1455
|
+
throw new Error(
|
|
1456
|
+
header.kid
|
|
1457
|
+
? `no signing key found for kid ${header.kid}`
|
|
1458
|
+
: 'token has no kid and the JWKS does not have exactly one signing key',
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
return jwk;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** @private */
|
|
1465
|
+
async _getJwks(forceRefresh) {
|
|
1466
|
+
if (this._config.jwks) {
|
|
1467
|
+
const configured = this._config.jwks;
|
|
1468
|
+
return Array.isArray(configured) ? configured : configured.keys || [];
|
|
1469
|
+
}
|
|
1470
|
+
if (this._jwks && !forceRefresh) return this._jwks;
|
|
1471
|
+
|
|
1472
|
+
const metadata = await this.getMetadata();
|
|
1473
|
+
if (!metadata.jwks_uri) {
|
|
1474
|
+
throw new Error('provider metadata has no jwks_uri');
|
|
1475
|
+
}
|
|
1476
|
+
const response = await fetch(metadata.jwks_uri, {
|
|
1477
|
+
headers: { Accept: 'application/json' },
|
|
1478
|
+
});
|
|
1479
|
+
if (!response.ok) {
|
|
1480
|
+
throw new Error(`JWKS request failed: ${response.status}`);
|
|
1481
|
+
}
|
|
1482
|
+
const body = await response.json();
|
|
1483
|
+
this._jwks = Array.isArray(body.keys) ? body.keys : [];
|
|
1484
|
+
return this._jwks;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* @param {string} accessToken - Access token for the userinfo endpoint
|
|
1489
|
+
* @param {Object} [tokenData] - Result of exchangeCodeForTokens; when it
|
|
1490
|
+
* carries validated id_token claims, the userinfo `sub` must match.
|
|
1491
|
+
*/
|
|
1492
|
+
async getUserInfo(accessToken, tokenData = {}) {
|
|
1493
|
+
if (isDevBypassActive(this._config)) {
|
|
1494
|
+
return {
|
|
1495
|
+
id: 'dev-' + crypto.randomUUID(),
|
|
1496
|
+
email: 'dev-user@oidc.com',
|
|
1497
|
+
name: 'Development OIDC User',
|
|
1498
|
+
roles: ['user', 'dev'],
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const claims = tokenData.claims;
|
|
1503
|
+
try {
|
|
1504
|
+
const metadata = await this.getMetadata();
|
|
1505
|
+
let userData;
|
|
1506
|
+
if (metadata.userinfo_endpoint) {
|
|
1507
|
+
const userResponse = await fetch(metadata.userinfo_endpoint, {
|
|
1021
1508
|
headers: {
|
|
1022
1509
|
Authorization: `Bearer ${accessToken}`,
|
|
1023
1510
|
Accept: 'application/json',
|
|
@@ -1030,28 +1517,27 @@ class OIDCProvider {
|
|
|
1030
1517
|
);
|
|
1031
1518
|
}
|
|
1032
1519
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
attributes: userData,
|
|
1042
|
-
};
|
|
1043
|
-
} catch (error) {
|
|
1044
|
-
throw new Error(`OIDC getUserInfo failed: ${error.message}`);
|
|
1520
|
+
userData = await userResponse.json();
|
|
1521
|
+
if (claims && userData.sub !== claims.sub) {
|
|
1522
|
+
throw new Error('userinfo sub does not match the id_token sub');
|
|
1523
|
+
}
|
|
1524
|
+
} else if (claims) {
|
|
1525
|
+
userData = claims;
|
|
1526
|
+
} else {
|
|
1527
|
+
throw new Error('no userinfo endpoint and no validated id_token');
|
|
1045
1528
|
}
|
|
1046
|
-
}
|
|
1047
1529
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1530
|
+
return {
|
|
1531
|
+
id: userData.sub,
|
|
1532
|
+
email: userData.email,
|
|
1533
|
+
name: userData.name || userData.preferred_username,
|
|
1534
|
+
roles: userData.roles || [],
|
|
1535
|
+
groups: userData.groups || [],
|
|
1536
|
+
attributes: userData,
|
|
1537
|
+
};
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
throw new Error(`OIDC getUserInfo failed: ${error.message}`);
|
|
1540
|
+
}
|
|
1055
1541
|
}
|
|
1056
1542
|
|
|
1057
1543
|
_generateMockIdToken() {
|
|
@@ -1059,7 +1545,7 @@ class OIDCProvider {
|
|
|
1059
1545
|
throw new Error('Mock ID token generation not allowed in production');
|
|
1060
1546
|
}
|
|
1061
1547
|
|
|
1062
|
-
// Development only - basic JWT structure
|
|
1548
|
+
// Development bypass only - basic JWT structure, NOT a verifiable token
|
|
1063
1549
|
const header = Buffer.from(
|
|
1064
1550
|
JSON.stringify({ alg: 'HS256', typ: 'JWT' }),
|
|
1065
1551
|
).toString('base64url');
|
|
@@ -1089,4 +1575,5 @@ module.exports = {
|
|
|
1089
1575
|
OAuth2Provider,
|
|
1090
1576
|
ActiveDirectoryProvider,
|
|
1091
1577
|
OIDCProvider,
|
|
1578
|
+
createPkcePair,
|
|
1092
1579
|
};
|