@mcp-abap-adt/auth-broker 1.0.8 → 2.1.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/CHANGELOG.md +104 -0
- package/COPYING +674 -0
- package/LICENSE +160 -17
- package/README.md +82 -18
- package/dist/bin/mcp-auth.js +23 -6
- package/dist/bin/mcp-sso.js +15 -202
- package/dist/bin/mcpSsoConfig.js +398 -0
- package/package.json +5 -4
package/dist/bin/mcp-sso.js
CHANGED
|
@@ -60,15 +60,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
60
60
|
};
|
|
61
61
|
})();
|
|
62
62
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
63
|
-
const node_readline_1 = require("node:readline");
|
|
64
63
|
const fs = __importStar(require("fs"));
|
|
65
64
|
const path = __importStar(require("path"));
|
|
66
65
|
// Use require for CommonJS dist files with absolute path
|
|
67
66
|
const distPath = path.resolve(__dirname, '..', 'index.js');
|
|
68
67
|
const { AuthBroker } = require(distPath);
|
|
69
|
-
const logger_1 = require("@mcp-abap-adt/logger");
|
|
70
68
|
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
71
69
|
const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
|
|
70
|
+
const logger_1 = require("@mcp-abap-adt/logger");
|
|
71
|
+
const mcpSsoConfig_1 = require("./mcpSsoConfig");
|
|
72
72
|
function getVersion() {
|
|
73
73
|
try {
|
|
74
74
|
const candidates = [
|
|
@@ -112,7 +112,7 @@ function showHelp() {
|
|
|
112
112
|
console.log(' --service-url <url> Service URL (ABAP: SAP URL, XSUAA: MCP URL)');
|
|
113
113
|
console.log(' --config <path> JSON config file (SSO provider config)');
|
|
114
114
|
console.log(' --browser <browser> Browser: auto|none|system|chrome|edge|firefox');
|
|
115
|
-
console.log(' --redirect-port <port> Redirect port for browser flows (default:
|
|
115
|
+
console.log(' --redirect-port <port> Redirect port for browser flows (default: from auth-providers, currently 61001)');
|
|
116
116
|
console.log(' --redirect-uri <uri> Custom redirect URI (OOB/manual code flows)');
|
|
117
117
|
console.log('');
|
|
118
118
|
console.log('OIDC Options:');
|
|
@@ -158,18 +158,6 @@ function parseScopes(value) {
|
|
|
158
158
|
.filter(Boolean);
|
|
159
159
|
return parts.length > 0 ? parts : undefined;
|
|
160
160
|
}
|
|
161
|
-
function readManualInput(prompt) {
|
|
162
|
-
const rl = (0, node_readline_1.createInterface)({
|
|
163
|
-
input: process.stdin,
|
|
164
|
-
output: process.stdout,
|
|
165
|
-
});
|
|
166
|
-
return new Promise((resolve) => {
|
|
167
|
-
rl.question(prompt, (answer) => {
|
|
168
|
-
rl.close();
|
|
169
|
-
resolve(answer.trim());
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
161
|
function createCliLogger(prefix = 'SSO') {
|
|
174
162
|
const isEnabled = () => {
|
|
175
163
|
if (process.env.DEBUG_SSO === 'false' ||
|
|
@@ -520,190 +508,6 @@ function resolveSamlTokenAlias(metadataXml) {
|
|
|
520
508
|
const match = metadataXml.match(regex);
|
|
521
509
|
return match?.[1];
|
|
522
510
|
}
|
|
523
|
-
function normalizeProviderConfig(raw) {
|
|
524
|
-
if (!raw || typeof raw !== 'object') {
|
|
525
|
-
return null;
|
|
526
|
-
}
|
|
527
|
-
if (raw.provider) {
|
|
528
|
-
return raw.provider;
|
|
529
|
-
}
|
|
530
|
-
if (raw.protocol && raw.flow) {
|
|
531
|
-
const { protocol, flow, config, ...rest } = raw;
|
|
532
|
-
return {
|
|
533
|
-
protocol,
|
|
534
|
-
flow,
|
|
535
|
-
config: config ?? rest,
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
540
|
-
function mergeConfig(target, source) {
|
|
541
|
-
const result = { ...target };
|
|
542
|
-
for (const [key, value] of Object.entries(source)) {
|
|
543
|
-
if (value !== undefined) {
|
|
544
|
-
result[key] = value;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return result;
|
|
548
|
-
}
|
|
549
|
-
function buildOidcConfig(options) {
|
|
550
|
-
const scopeList = options.scopes;
|
|
551
|
-
const tokenEndpoint = options.tokenEndpoint ||
|
|
552
|
-
(options.uaaUrl
|
|
553
|
-
? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
|
|
554
|
-
: undefined);
|
|
555
|
-
const passcode = options.passcode;
|
|
556
|
-
const username = options.username || (passcode ? 'passcode' : undefined);
|
|
557
|
-
const password = options.password || passcode;
|
|
558
|
-
return {
|
|
559
|
-
issuerUrl: options.issuerUrl,
|
|
560
|
-
authorizationEndpoint: options.authorizationEndpoint,
|
|
561
|
-
tokenEndpoint,
|
|
562
|
-
deviceAuthorizationEndpoint: options.deviceAuthorizationEndpoint,
|
|
563
|
-
clientId: options.clientId,
|
|
564
|
-
clientSecret: options.clientSecret,
|
|
565
|
-
scopes: scopeList,
|
|
566
|
-
scope: options.scope,
|
|
567
|
-
authorizationCode: options.code,
|
|
568
|
-
username,
|
|
569
|
-
password,
|
|
570
|
-
subjectToken: options.subjectToken,
|
|
571
|
-
subjectTokenType: options.subjectTokenType ||
|
|
572
|
-
'urn:ietf:params:oauth:token-type:access_token',
|
|
573
|
-
audience: options.audience,
|
|
574
|
-
actorToken: options.actorToken,
|
|
575
|
-
actorTokenType: options.actorTokenType,
|
|
576
|
-
browser: options.browser,
|
|
577
|
-
redirectPort: options.redirectPort,
|
|
578
|
-
redirectUri: options.redirectUri,
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
function buildSamlConfig(options) {
|
|
582
|
-
const assertionFlow = options.assertionFlow || (options.assertion ? 'assertion' : 'browser');
|
|
583
|
-
const tokenUrl = options.tokenEndpoint ||
|
|
584
|
-
(options.uaaUrl
|
|
585
|
-
? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
|
|
586
|
-
: undefined);
|
|
587
|
-
const assertionProvider = options.assertion
|
|
588
|
-
? async () => options.assertion
|
|
589
|
-
: assertionFlow === 'assertion'
|
|
590
|
-
? async () => readManualInput('Paste SAMLResponse: ')
|
|
591
|
-
: undefined;
|
|
592
|
-
const cookieProvider = async (samlResponse) => {
|
|
593
|
-
if (options.cookie) {
|
|
594
|
-
return options.cookie;
|
|
595
|
-
}
|
|
596
|
-
if (assertionFlow === 'assertion') {
|
|
597
|
-
return `SAMLResponse=${samlResponse}`;
|
|
598
|
-
}
|
|
599
|
-
return readManualInput('Paste session cookies: ');
|
|
600
|
-
};
|
|
601
|
-
return {
|
|
602
|
-
idpSsoUrl: options.idpSsoUrl,
|
|
603
|
-
spEntityId: options.spEntityId,
|
|
604
|
-
acsUrl: options.acsUrl,
|
|
605
|
-
relayState: options.relayState,
|
|
606
|
-
assertionFlow,
|
|
607
|
-
assertionProvider,
|
|
608
|
-
tokenUrl,
|
|
609
|
-
uaaUrl: options.uaaUrl,
|
|
610
|
-
clientId: options.clientId,
|
|
611
|
-
clientSecret: options.clientSecret,
|
|
612
|
-
browser: options.browser,
|
|
613
|
-
redirectPort: options.redirectPort,
|
|
614
|
-
cookieProvider,
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
function buildProviderConfig(options, existingAuth, existingConn, fileConfig) {
|
|
618
|
-
let configFromCli = null;
|
|
619
|
-
if (options.protocol && options.flow) {
|
|
620
|
-
if (options.protocol === 'oidc') {
|
|
621
|
-
switch (options.flow) {
|
|
622
|
-
case 'browser':
|
|
623
|
-
configFromCli = {
|
|
624
|
-
protocol: 'oidc',
|
|
625
|
-
flow: 'browser',
|
|
626
|
-
config: buildOidcConfig(options),
|
|
627
|
-
};
|
|
628
|
-
break;
|
|
629
|
-
case 'device':
|
|
630
|
-
configFromCli = {
|
|
631
|
-
protocol: 'oidc',
|
|
632
|
-
flow: 'device',
|
|
633
|
-
config: buildOidcConfig(options),
|
|
634
|
-
};
|
|
635
|
-
break;
|
|
636
|
-
case 'password':
|
|
637
|
-
configFromCli = {
|
|
638
|
-
protocol: 'oidc',
|
|
639
|
-
flow: 'password',
|
|
640
|
-
config: buildOidcConfig(options),
|
|
641
|
-
};
|
|
642
|
-
break;
|
|
643
|
-
case 'token_exchange':
|
|
644
|
-
configFromCli = {
|
|
645
|
-
protocol: 'oidc',
|
|
646
|
-
flow: 'token_exchange',
|
|
647
|
-
config: buildOidcConfig(options),
|
|
648
|
-
};
|
|
649
|
-
break;
|
|
650
|
-
default:
|
|
651
|
-
throw new Error(`Unsupported OIDC flow: ${options.flow}`);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
else if (options.protocol === 'saml2') {
|
|
655
|
-
switch (options.flow) {
|
|
656
|
-
case 'bearer':
|
|
657
|
-
configFromCli = {
|
|
658
|
-
protocol: 'saml2',
|
|
659
|
-
flow: 'bearer',
|
|
660
|
-
config: buildSamlConfig(options),
|
|
661
|
-
};
|
|
662
|
-
break;
|
|
663
|
-
case 'pure':
|
|
664
|
-
configFromCli = {
|
|
665
|
-
protocol: 'saml2',
|
|
666
|
-
flow: 'pure',
|
|
667
|
-
config: buildSamlConfig(options),
|
|
668
|
-
};
|
|
669
|
-
break;
|
|
670
|
-
default:
|
|
671
|
-
throw new Error(`Unsupported SAML flow: ${options.flow}`);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
const base = fileConfig ?? configFromCli;
|
|
676
|
-
if (!base) {
|
|
677
|
-
throw new Error('Provider config is missing. Use --config or --protocol/--flow options.');
|
|
678
|
-
}
|
|
679
|
-
let result = base;
|
|
680
|
-
if (options.protocol) {
|
|
681
|
-
result = { ...result, protocol: options.protocol };
|
|
682
|
-
}
|
|
683
|
-
if (options.flow) {
|
|
684
|
-
result = { ...result, flow: options.flow };
|
|
685
|
-
}
|
|
686
|
-
if (configFromCli) {
|
|
687
|
-
result = {
|
|
688
|
-
...result,
|
|
689
|
-
config: mergeConfig(result.config || {}, configFromCli.config || {}),
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
const accessToken = existingConn?.authorizationToken;
|
|
693
|
-
const refreshToken = existingAuth?.refreshToken;
|
|
694
|
-
if ((result.protocol === 'oidc' ||
|
|
695
|
-
(result.protocol === 'saml2' && result.flow === 'bearer')) &&
|
|
696
|
-
(accessToken || refreshToken)) {
|
|
697
|
-
result = {
|
|
698
|
-
...result,
|
|
699
|
-
config: mergeConfig(result.config || {}, {
|
|
700
|
-
accessToken,
|
|
701
|
-
refreshToken,
|
|
702
|
-
}),
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
return result;
|
|
706
|
-
}
|
|
707
511
|
async function main() {
|
|
708
512
|
const options = parseArgs();
|
|
709
513
|
if (!options) {
|
|
@@ -768,12 +572,21 @@ async function main() {
|
|
|
768
572
|
process.exit(1);
|
|
769
573
|
}
|
|
770
574
|
const raw = JSON.parse(fs.readFileSync(resolvedConfigPath, 'utf8'));
|
|
771
|
-
providerConfigFromFile = normalizeProviderConfig(raw);
|
|
575
|
+
providerConfigFromFile = (0, mcpSsoConfig_1.normalizeProviderConfig)(raw);
|
|
772
576
|
if (!providerConfigFromFile) {
|
|
773
577
|
console.error(`❌ Config file does not contain provider config`);
|
|
774
578
|
process.exit(1);
|
|
775
579
|
}
|
|
776
580
|
}
|
|
581
|
+
// Merge the file into `options` *before* anything downstream reads
|
|
582
|
+
// options.protocol/flow or builds a strategy from them — a run driven by
|
|
583
|
+
// --config alone must reach exactly the same validation and
|
|
584
|
+
// strategy-building code a --protocol/--flow run does, or a field the file
|
|
585
|
+
// carries (including a legacy one 2.0.0 removed) can reach the provider
|
|
586
|
+
// untouched with no error and no warning. CLI flags already parsed above
|
|
587
|
+
// are left alone; the file only fills what they didn't set. A no-op when
|
|
588
|
+
// --config wasn't given.
|
|
589
|
+
(0, mcpSsoConfig_1.applyFileConfig)(options, providerConfigFromFile);
|
|
777
590
|
if (options.serviceKeyPath) {
|
|
778
591
|
const resolvedServiceKeyPath = path.resolve(options.serviceKeyPath);
|
|
779
592
|
const serviceKeyDir = path.dirname(resolvedServiceKeyPath);
|
|
@@ -836,7 +649,7 @@ async function main() {
|
|
|
836
649
|
}
|
|
837
650
|
if (options.protocol === 'oidc' && options.flow === 'password') {
|
|
838
651
|
if (!options.passcode && !options.password) {
|
|
839
|
-
options.passcode = await readManualInput('Paste passcode: ');
|
|
652
|
+
options.passcode = await (0, mcpSsoConfig_1.readManualInput)('Paste passcode: ');
|
|
840
653
|
}
|
|
841
654
|
}
|
|
842
655
|
const tempSessionDir = path.join(path.dirname(resolvedOutputPath), '.tmp');
|
|
@@ -893,7 +706,7 @@ async function main() {
|
|
|
893
706
|
refreshToken: existingAuth?.refreshToken,
|
|
894
707
|
});
|
|
895
708
|
}
|
|
896
|
-
const providerConfig = buildProviderConfig(options, existingAuth, existingConn
|
|
709
|
+
const providerConfig = (0, mcpSsoConfig_1.buildProviderConfig)(options, existingAuth, existingConn);
|
|
897
710
|
const providerConfigWithLogger = providerConfig.config
|
|
898
711
|
? {
|
|
899
712
|
...providerConfig,
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure config-building logic for `mcp-sso`, split out from bin/mcp-sso.ts so
|
|
4
|
+
* it can be imported by tests directly.
|
|
5
|
+
*
|
|
6
|
+
* bin/mcp-sso.ts itself cannot be `import`ed safely: it `require()`s the
|
|
7
|
+
* compiled `dist/index.js` at module load time (assuming it is always run
|
|
8
|
+
* from `dist/bin/`) and calls `main()` at the bottom of the file. Neither is
|
|
9
|
+
* true when a test loads the TypeScript source directly, so anything that
|
|
10
|
+
* needs unit coverage — building an `SsoProviderConfig` from CLI options and
|
|
11
|
+
* an optional `--config` file, and merging the two — lives here instead.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.INTERACTIVE_LOGIN_TIMEOUT_MS = void 0;
|
|
15
|
+
exports.readManualInput = readManualInput;
|
|
16
|
+
exports.normalizeProviderConfig = normalizeProviderConfig;
|
|
17
|
+
exports.applyFileConfig = applyFileConfig;
|
|
18
|
+
exports.buildOidcBrowserConfig = buildOidcBrowserConfig;
|
|
19
|
+
exports.buildOidcDeviceConfig = buildOidcDeviceConfig;
|
|
20
|
+
exports.buildOidcPasswordConfig = buildOidcPasswordConfig;
|
|
21
|
+
exports.buildOidcTokenExchangeConfig = buildOidcTokenExchangeConfig;
|
|
22
|
+
exports.buildSamlBearerConfig = buildSamlBearerConfig;
|
|
23
|
+
exports.buildSamlPureConfig = buildSamlPureConfig;
|
|
24
|
+
exports.buildProviderConfig = buildProviderConfig;
|
|
25
|
+
const node_readline_1 = require("node:readline");
|
|
26
|
+
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
27
|
+
/**
|
|
28
|
+
* A person completes these logins at a browser; the library's own default
|
|
29
|
+
* (30s) is sized for an unattended caller instead.
|
|
30
|
+
*/
|
|
31
|
+
exports.INTERACTIVE_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
32
|
+
function readManualInput(prompt) {
|
|
33
|
+
const rl = (0, node_readline_1.createInterface)({
|
|
34
|
+
input: process.stdin,
|
|
35
|
+
output: process.stdout,
|
|
36
|
+
});
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
rl.question(prompt, (answer) => {
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve(answer.trim());
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function normalizeProviderConfig(raw) {
|
|
45
|
+
if (!raw || typeof raw !== 'object') {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (raw.provider) {
|
|
49
|
+
return raw.provider;
|
|
50
|
+
}
|
|
51
|
+
if (raw.protocol && raw.flow) {
|
|
52
|
+
const { protocol, flow, config, ...rest } = raw;
|
|
53
|
+
return {
|
|
54
|
+
protocol,
|
|
55
|
+
flow,
|
|
56
|
+
config: config ?? rest,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pre-2.0.0 provider config fields that named a JavaScript function
|
|
63
|
+
* (`() => Promise<string>` or similar) rather than a value. JSON cannot
|
|
64
|
+
* carry a function, so a `--config` file can never legitimately supply one —
|
|
65
|
+
* if a key like this shows up, the file is stale in a way no conversion can
|
|
66
|
+
* fix. Refuse rather than silently drop it, and say what to write instead.
|
|
67
|
+
*/
|
|
68
|
+
const UNSUPPORTED_LEGACY_CONFIG_FIELDS = {
|
|
69
|
+
authorizationCodeProvider: "'authorizationCodeProvider' (a function) cannot be expressed in a --config file. Use --code for a value you already hold, or drive the OIDC browser flow interactively without it.",
|
|
70
|
+
assertionProvider: "'assertionProvider' (a function) cannot be expressed in a --config file. Use --assertion for a value you already hold, or --assertion-flow to choose how it is obtained.",
|
|
71
|
+
manualInput: "'manualInput' (a function) cannot be expressed in a --config file. Use --assertion-flow manual to prompt on this CLI's own stdin instead.",
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* `--config` file fields that map 1:1 onto a `McpSsoOptions` field of the
|
|
75
|
+
* same name — every pre-2.0.0 config field the strategies still need, other
|
|
76
|
+
* than `authorizationCode` (see `applyFileConfig`), lands here.
|
|
77
|
+
*/
|
|
78
|
+
const CONFIG_BACKFILL_FIELDS = [
|
|
79
|
+
'browser',
|
|
80
|
+
'redirectPort',
|
|
81
|
+
'redirectUri',
|
|
82
|
+
'issuerUrl',
|
|
83
|
+
'authorizationEndpoint',
|
|
84
|
+
'tokenEndpoint',
|
|
85
|
+
'deviceAuthorizationEndpoint',
|
|
86
|
+
'clientId',
|
|
87
|
+
'clientSecret',
|
|
88
|
+
'scopes',
|
|
89
|
+
'scope',
|
|
90
|
+
'username',
|
|
91
|
+
'password',
|
|
92
|
+
'passcode',
|
|
93
|
+
'subjectToken',
|
|
94
|
+
'subjectTokenType',
|
|
95
|
+
'audience',
|
|
96
|
+
'actorToken',
|
|
97
|
+
'actorTokenType',
|
|
98
|
+
'idpSsoUrl',
|
|
99
|
+
'spEntityId',
|
|
100
|
+
'acsUrl',
|
|
101
|
+
'relayState',
|
|
102
|
+
'assertionFlow',
|
|
103
|
+
'assertion',
|
|
104
|
+
'cookie',
|
|
105
|
+
'uaaUrl',
|
|
106
|
+
];
|
|
107
|
+
/**
|
|
108
|
+
* Backfills `options` in place from a `--config` file's raw `config` object.
|
|
109
|
+
* CLI flags always win — a field already set on `options` is left alone; the
|
|
110
|
+
* file only fills gaps.
|
|
111
|
+
*
|
|
112
|
+
* This is what makes a config-file-only run (no --protocol/--flow on the
|
|
113
|
+
* CLI) reach the same strategy-building code as a CLI-flag run: without it,
|
|
114
|
+
* the file's fields — including a `browser`/`redirectPort` a 2.0.0 provider
|
|
115
|
+
* no longer accepts directly — would reach `SsoProviderFactory.create()`
|
|
116
|
+
* untouched and be silently ignored.
|
|
117
|
+
*
|
|
118
|
+
* Every field a pre-2.0.0 config could carry either lands on its
|
|
119
|
+
* `McpSsoOptions` equivalent here (which the strategy built from `options`
|
|
120
|
+
* then consumes) or the run is refused via `process.exit(1)` naming what to
|
|
121
|
+
* write instead. Nothing may reach the provider without one of those two
|
|
122
|
+
* happening.
|
|
123
|
+
*
|
|
124
|
+
* Also backfills `options.protocol`/`options.flow` from the file when the
|
|
125
|
+
* CLI didn't set them — a `--config`-only run (no `--protocol`/`--flow`
|
|
126
|
+
* flags at all) must still resolve to a real flow, or nothing downstream
|
|
127
|
+
* ever calls a builder at all.
|
|
128
|
+
*
|
|
129
|
+
* A no-op when `fileConfig` is `null` (no `--config` was given), so callers
|
|
130
|
+
* can invoke this unconditionally.
|
|
131
|
+
*/
|
|
132
|
+
function applyFileConfig(options, fileConfig) {
|
|
133
|
+
if (!fileConfig) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
options.protocol = options.protocol ?? fileConfig.protocol;
|
|
137
|
+
options.flow = options.flow ?? fileConfig.flow;
|
|
138
|
+
const fields = (fileConfig.config ?? {});
|
|
139
|
+
for (const [field, guidance] of Object.entries(UNSUPPORTED_LEGACY_CONFIG_FIELDS)) {
|
|
140
|
+
if (fields[field] !== undefined) {
|
|
141
|
+
console.error(`❌ --config: ${guidance}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Pre-2.0.0 field name for the OIDC manual/OOB code paste path; maps onto
|
|
146
|
+
// the same slot --code fills.
|
|
147
|
+
if (options.code === undefined &&
|
|
148
|
+
typeof fields.authorizationCode === 'string') {
|
|
149
|
+
options.code = fields.authorizationCode;
|
|
150
|
+
}
|
|
151
|
+
for (const field of CONFIG_BACKFILL_FIELDS) {
|
|
152
|
+
if (options[field] === undefined && fields[field] !== undefined) {
|
|
153
|
+
options[field] = fields[field];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function requireOption(value, flagName) {
|
|
158
|
+
if (!value) {
|
|
159
|
+
console.error(`❌ ${flagName} is required for this flow.`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function resolveOidcTokenEndpoint(options) {
|
|
165
|
+
return (options.tokenEndpoint ||
|
|
166
|
+
(options.uaaUrl
|
|
167
|
+
? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
|
|
168
|
+
: undefined));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Only the OIDC 'browser' flow opens a browser; routes `--browser`,
|
|
172
|
+
* `--redirect-port` and manual/OOB code paste into the strategy that
|
|
173
|
+
* replaces them.
|
|
174
|
+
*/
|
|
175
|
+
function buildOidcBrowserAuthorization(options) {
|
|
176
|
+
if (options.code) {
|
|
177
|
+
// The consumer already holds the code (manual paste / OOB redirect
|
|
178
|
+
// URI); no callback server is opened at all.
|
|
179
|
+
return (0, auth_providers_1.asOidcResult)((0, auth_providers_1.staticCodeStrategy)({
|
|
180
|
+
redirectUri: options.redirectUri,
|
|
181
|
+
payload: options.code,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
// No fallback: an omitted --redirect-port lets the strategy bind its own
|
|
185
|
+
// default port rather than this CLI pinning a number it doesn't own.
|
|
186
|
+
return (0, auth_providers_1.oidcCallbackStrategy)({
|
|
187
|
+
port: options.redirectPort,
|
|
188
|
+
browser: options.browser,
|
|
189
|
+
timeoutMs: exports.INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function buildOidcCommon(options) {
|
|
193
|
+
return {
|
|
194
|
+
issuerUrl: options.issuerUrl,
|
|
195
|
+
clientId: requireOption(options.clientId, '--client-id'),
|
|
196
|
+
clientSecret: options.clientSecret,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function buildOidcBrowserConfig(options) {
|
|
200
|
+
return {
|
|
201
|
+
...buildOidcCommon(options),
|
|
202
|
+
authorizationEndpoint: options.authorizationEndpoint,
|
|
203
|
+
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
204
|
+
scopes: options.scopes,
|
|
205
|
+
authorization: buildOidcBrowserAuthorization(options),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildOidcDeviceConfig(options) {
|
|
209
|
+
// Device flow never opens a browser from this process; --browser and
|
|
210
|
+
// --redirect-port have nothing to attach to here.
|
|
211
|
+
return {
|
|
212
|
+
...buildOidcCommon(options),
|
|
213
|
+
deviceAuthorizationEndpoint: options.deviceAuthorizationEndpoint,
|
|
214
|
+
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
215
|
+
scopes: options.scopes,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function buildOidcPasswordConfig(options) {
|
|
219
|
+
// Password flow never opens a browser; --browser and --redirect-port are
|
|
220
|
+
// not meaningful here either.
|
|
221
|
+
const passcode = options.passcode;
|
|
222
|
+
const username = options.username || (passcode ? 'passcode' : undefined);
|
|
223
|
+
const password = options.password || passcode;
|
|
224
|
+
return {
|
|
225
|
+
...buildOidcCommon(options),
|
|
226
|
+
username: requireOption(username, '--username (or --passcode)'),
|
|
227
|
+
password: requireOption(password, '--password (or --passcode)'),
|
|
228
|
+
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
229
|
+
scopes: options.scopes,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function buildOidcTokenExchangeConfig(options) {
|
|
233
|
+
// Token exchange never opens a browser; --browser and --redirect-port are
|
|
234
|
+
// not meaningful here either.
|
|
235
|
+
return {
|
|
236
|
+
...buildOidcCommon(options),
|
|
237
|
+
subjectToken: requireOption(options.subjectToken, '--subject-token'),
|
|
238
|
+
subjectTokenType: options.subjectTokenType ||
|
|
239
|
+
'urn:ietf:params:oauth:token-type:access_token',
|
|
240
|
+
scope: options.scope,
|
|
241
|
+
audience: options.audience,
|
|
242
|
+
actorToken: options.actorToken,
|
|
243
|
+
actorTokenType: options.actorTokenType,
|
|
244
|
+
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Both SAML flows (bearer, pure) can open a browser; routes `--browser`,
|
|
249
|
+
* `--redirect-port` and the manual/static assertion options into the
|
|
250
|
+
* strategy that replaces them.
|
|
251
|
+
*/
|
|
252
|
+
function buildSamlAuthorization(options) {
|
|
253
|
+
if (options.assertion) {
|
|
254
|
+
// The consumer already holds the assertion; nothing is opened or asked.
|
|
255
|
+
return (0, auth_providers_1.staticCodeStrategy)({
|
|
256
|
+
redirectUri: options.acsUrl,
|
|
257
|
+
payload: options.assertion,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const assertionFlow = options.assertionFlow || 'browser';
|
|
261
|
+
if (assertionFlow !== 'browser') {
|
|
262
|
+
// 'manual', and an 'assertion' flow given no value, both need a human to
|
|
263
|
+
// lift the SAMLResponse out of the POST body by hand.
|
|
264
|
+
return (0, auth_providers_1.manualSamlResponseStrategy)({
|
|
265
|
+
redirectUri: options.acsUrl,
|
|
266
|
+
read: readManualInput,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// No fallback: an omitted --redirect-port lets the strategy bind its own
|
|
270
|
+
// default port rather than this CLI pinning a number it doesn't own.
|
|
271
|
+
return (0, auth_providers_1.samlCallbackStrategy)({
|
|
272
|
+
port: options.redirectPort,
|
|
273
|
+
browser: options.browser,
|
|
274
|
+
timeoutMs: exports.INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function buildSamlCookieProvider(options) {
|
|
278
|
+
const assertionFlow = options.assertionFlow || (options.assertion ? 'assertion' : 'browser');
|
|
279
|
+
return async (samlResponse) => {
|
|
280
|
+
if (options.cookie) {
|
|
281
|
+
return options.cookie;
|
|
282
|
+
}
|
|
283
|
+
if (assertionFlow === 'assertion') {
|
|
284
|
+
return `SAMLResponse=${samlResponse}`;
|
|
285
|
+
}
|
|
286
|
+
return readManualInput('Paste session cookies: ');
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function buildSamlBearerConfig(options) {
|
|
290
|
+
return {
|
|
291
|
+
idpSsoUrl: requireOption(options.idpSsoUrl, '--idp-sso-url'),
|
|
292
|
+
spEntityId: requireOption(options.spEntityId, '--sp-entity-id'),
|
|
293
|
+
acsUrl: options.acsUrl,
|
|
294
|
+
relayState: options.relayState,
|
|
295
|
+
tokenUrl: options.tokenEndpoint,
|
|
296
|
+
uaaUrl: options.uaaUrl,
|
|
297
|
+
clientId: options.clientId,
|
|
298
|
+
clientSecret: options.clientSecret,
|
|
299
|
+
authorization: buildSamlAuthorization(options),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function buildSamlPureConfig(options) {
|
|
303
|
+
return {
|
|
304
|
+
idpSsoUrl: requireOption(options.idpSsoUrl, '--idp-sso-url'),
|
|
305
|
+
spEntityId: requireOption(options.spEntityId, '--sp-entity-id'),
|
|
306
|
+
acsUrl: options.acsUrl,
|
|
307
|
+
relayState: options.relayState,
|
|
308
|
+
authorization: buildSamlAuthorization(options),
|
|
309
|
+
cookieProvider: buildSamlCookieProvider(options),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Builds the effective `SsoProviderConfig` from `options` alone.
|
|
314
|
+
*
|
|
315
|
+
* `options` must already be the *merged* view — `--config` file fields
|
|
316
|
+
* backfilled beneath whatever the CLI flags set, via `applyFileConfig` — so
|
|
317
|
+
* this never needs a separate file/CLI merge step, and required-field
|
|
318
|
+
* validation (`requireOption`, inside each builder above) sees the same
|
|
319
|
+
* merged data regardless of whether a value came from the file or a flag.
|
|
320
|
+
*/
|
|
321
|
+
function buildProviderConfig(options, existingAuth, existingConn) {
|
|
322
|
+
if (!options.protocol || !options.flow) {
|
|
323
|
+
throw new Error('Provider config is missing. Use --config or --protocol/--flow options.');
|
|
324
|
+
}
|
|
325
|
+
let result;
|
|
326
|
+
if (options.protocol === 'oidc') {
|
|
327
|
+
switch (options.flow) {
|
|
328
|
+
case 'browser':
|
|
329
|
+
result = {
|
|
330
|
+
protocol: 'oidc',
|
|
331
|
+
flow: 'browser',
|
|
332
|
+
config: buildOidcBrowserConfig(options),
|
|
333
|
+
};
|
|
334
|
+
break;
|
|
335
|
+
case 'device':
|
|
336
|
+
result = {
|
|
337
|
+
protocol: 'oidc',
|
|
338
|
+
flow: 'device',
|
|
339
|
+
config: buildOidcDeviceConfig(options),
|
|
340
|
+
};
|
|
341
|
+
break;
|
|
342
|
+
case 'password':
|
|
343
|
+
result = {
|
|
344
|
+
protocol: 'oidc',
|
|
345
|
+
flow: 'password',
|
|
346
|
+
config: buildOidcPasswordConfig(options),
|
|
347
|
+
};
|
|
348
|
+
break;
|
|
349
|
+
case 'token_exchange':
|
|
350
|
+
result = {
|
|
351
|
+
protocol: 'oidc',
|
|
352
|
+
flow: 'token_exchange',
|
|
353
|
+
config: buildOidcTokenExchangeConfig(options),
|
|
354
|
+
};
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
throw new Error(`Unsupported OIDC flow: ${options.flow}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (options.protocol === 'saml2') {
|
|
361
|
+
switch (options.flow) {
|
|
362
|
+
case 'bearer':
|
|
363
|
+
result = {
|
|
364
|
+
protocol: 'saml2',
|
|
365
|
+
flow: 'bearer',
|
|
366
|
+
config: buildSamlBearerConfig(options),
|
|
367
|
+
};
|
|
368
|
+
break;
|
|
369
|
+
case 'pure':
|
|
370
|
+
result = {
|
|
371
|
+
protocol: 'saml2',
|
|
372
|
+
flow: 'pure',
|
|
373
|
+
config: buildSamlPureConfig(options),
|
|
374
|
+
};
|
|
375
|
+
break;
|
|
376
|
+
default:
|
|
377
|
+
throw new Error(`Unsupported SAML flow: ${options.flow}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
throw new Error(`Unsupported protocol: ${options.protocol}`);
|
|
382
|
+
}
|
|
383
|
+
const accessToken = existingConn?.authorizationToken;
|
|
384
|
+
const refreshToken = existingAuth?.refreshToken;
|
|
385
|
+
if ((result.protocol === 'oidc' ||
|
|
386
|
+
(result.protocol === 'saml2' && result.flow === 'bearer')) &&
|
|
387
|
+
(accessToken || refreshToken)) {
|
|
388
|
+
result = {
|
|
389
|
+
...result,
|
|
390
|
+
config: {
|
|
391
|
+
...result.config,
|
|
392
|
+
accessToken,
|
|
393
|
+
refreshToken,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|