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