@mcp-abap-adt/auth-broker 3.0.3 → 3.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 +70 -1
- package/README.md +34 -312
- package/package.json +14 -41
- package/CONTRIBUTORS.md +0 -33
- package/dist/__tests__/helpers/configHelpers.d.ts +0 -59
- package/dist/__tests__/helpers/configHelpers.d.ts.map +0 -1
- package/dist/__tests__/helpers/configHelpers.js +0 -206
- package/dist/__tests__/helpers/netHelpers.d.ts +0 -3
- package/dist/__tests__/helpers/netHelpers.d.ts.map +0 -1
- package/dist/__tests__/helpers/netHelpers.js +0 -63
- package/dist/__tests__/helpers/testLogger.d.ts +0 -7
- package/dist/__tests__/helpers/testLogger.d.ts.map +0 -1
- package/dist/__tests__/helpers/testLogger.js +0 -54
- package/dist/bin/mcp-auth.js +0 -752
- package/dist/bin/mcp-sso.js +0 -825
- package/dist/bin/mcpSsoConfig.js +0 -566
- package/dist/bin/samlMetadata.js +0 -175
- package/dist/bin/workDir.js +0 -79
package/dist/bin/mcpSsoConfig.js
DELETED
|
@@ -1,566 +0,0 @@
|
|
|
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.readIdpCertificateFile = readIdpCertificateFile;
|
|
19
|
-
exports.resolveIdpCertificates = resolveIdpCertificates;
|
|
20
|
-
exports.parseSamlTrustArg = parseSamlTrustArg;
|
|
21
|
-
exports.buildOidcBrowserConfig = buildOidcBrowserConfig;
|
|
22
|
-
exports.buildOidcDeviceConfig = buildOidcDeviceConfig;
|
|
23
|
-
exports.buildOidcPasswordConfig = buildOidcPasswordConfig;
|
|
24
|
-
exports.buildOidcTokenExchangeConfig = buildOidcTokenExchangeConfig;
|
|
25
|
-
exports.buildSamlBearerConfig = buildSamlBearerConfig;
|
|
26
|
-
exports.buildSamlPureConfig = buildSamlPureConfig;
|
|
27
|
-
exports.buildProviderConfig = buildProviderConfig;
|
|
28
|
-
const node_fs_1 = require("node:fs");
|
|
29
|
-
const node_path_1 = require("node:path");
|
|
30
|
-
const node_readline_1 = require("node:readline");
|
|
31
|
-
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
32
|
-
/**
|
|
33
|
-
* A person completes these logins at a browser; the library's own default
|
|
34
|
-
* (30s) is sized for an unattended caller instead.
|
|
35
|
-
*/
|
|
36
|
-
exports.INTERACTIVE_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
37
|
-
function readManualInput(prompt) {
|
|
38
|
-
const rl = (0, node_readline_1.createInterface)({
|
|
39
|
-
input: process.stdin,
|
|
40
|
-
output: process.stdout,
|
|
41
|
-
});
|
|
42
|
-
// A closed stdin never answers the question, and a promise that never
|
|
43
|
-
// settles lets the event loop drain: the process exited 0 with nothing
|
|
44
|
-
// written, which a script reads as success. End of input is a refusal.
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
let answered = false;
|
|
47
|
-
rl.on('close', () => {
|
|
48
|
-
if (!answered)
|
|
49
|
-
reject(new Error(`no input: stdin closed at "${prompt.trim()}"`));
|
|
50
|
-
});
|
|
51
|
-
rl.question(prompt, (answer) => {
|
|
52
|
-
answered = true;
|
|
53
|
-
rl.close();
|
|
54
|
-
resolve(answer.trim());
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
function normalizeProviderConfig(raw) {
|
|
59
|
-
if (!raw || typeof raw !== 'object') {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
if (raw.provider) {
|
|
63
|
-
return raw.provider;
|
|
64
|
-
}
|
|
65
|
-
if (raw.protocol && raw.flow) {
|
|
66
|
-
const { protocol, flow, config, ...rest } = raw;
|
|
67
|
-
return {
|
|
68
|
-
protocol,
|
|
69
|
-
flow,
|
|
70
|
-
config: config ?? rest,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Pre-2.0.0 provider config fields that named a JavaScript function
|
|
77
|
-
* (`() => Promise<string>` or similar) rather than a value. JSON cannot
|
|
78
|
-
* carry a function, so a `--config` file can never legitimately supply one —
|
|
79
|
-
* if a key like this shows up, the file is stale in a way no conversion can
|
|
80
|
-
* fix. Refuse rather than silently drop it, and say what to write instead.
|
|
81
|
-
*/
|
|
82
|
-
const UNSUPPORTED_LEGACY_CONFIG_FIELDS = {
|
|
83
|
-
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.",
|
|
84
|
-
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.",
|
|
85
|
-
manualInput: "'manualInput' (a function) cannot be expressed in a --config file. Use --assertion-flow manual to prompt on this CLI's own stdin instead.",
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* `--config` file fields that map 1:1 onto a `McpSsoOptions` field of the
|
|
89
|
-
* same name — every pre-2.0.0 config field the strategies still need, other
|
|
90
|
-
* than `authorizationCode` (see `applyFileConfig`), lands here.
|
|
91
|
-
*/
|
|
92
|
-
const CONFIG_BACKFILL_FIELDS = [
|
|
93
|
-
'browser',
|
|
94
|
-
'redirectPort',
|
|
95
|
-
'redirectUri',
|
|
96
|
-
'issuerUrl',
|
|
97
|
-
'authorizationEndpoint',
|
|
98
|
-
'tokenEndpoint',
|
|
99
|
-
'deviceAuthorizationEndpoint',
|
|
100
|
-
'clientId',
|
|
101
|
-
'clientSecret',
|
|
102
|
-
'scopes',
|
|
103
|
-
'scope',
|
|
104
|
-
'username',
|
|
105
|
-
'password',
|
|
106
|
-
'passcode',
|
|
107
|
-
'subjectToken',
|
|
108
|
-
'subjectTokenType',
|
|
109
|
-
'audience',
|
|
110
|
-
'actorToken',
|
|
111
|
-
'actorTokenType',
|
|
112
|
-
'idpSsoUrl',
|
|
113
|
-
'spEntityId',
|
|
114
|
-
'acsUrl',
|
|
115
|
-
'relayState',
|
|
116
|
-
'assertionFlow',
|
|
117
|
-
'assertion',
|
|
118
|
-
'cookie',
|
|
119
|
-
'uaaUrl',
|
|
120
|
-
'idpEntityId',
|
|
121
|
-
'idpMetadata',
|
|
122
|
-
'authnRequestId',
|
|
123
|
-
];
|
|
124
|
-
/**
|
|
125
|
-
* Backfills `options` in place from a `--config` file's raw `config` object.
|
|
126
|
-
* CLI flags always win — a field already set on `options` is left alone; the
|
|
127
|
-
* file only fills gaps.
|
|
128
|
-
*
|
|
129
|
-
* This is what makes a config-file-only run (no --protocol/--flow on the
|
|
130
|
-
* CLI) reach the same strategy-building code as a CLI-flag run: without it,
|
|
131
|
-
* the file's fields — including a `browser`/`redirectPort` a 2.0.0 provider
|
|
132
|
-
* no longer accepts directly — would reach `SsoProviderFactory.create()`
|
|
133
|
-
* untouched and be silently ignored.
|
|
134
|
-
*
|
|
135
|
-
* Every field a pre-2.0.0 config could carry either lands on its
|
|
136
|
-
* `McpSsoOptions` equivalent here (which the strategy built from `options`
|
|
137
|
-
* then consumes) or the run is refused via `process.exit(1)` naming what to
|
|
138
|
-
* write instead. Nothing may reach the provider without one of those two
|
|
139
|
-
* happening.
|
|
140
|
-
*
|
|
141
|
-
* Also backfills `options.protocol`/`options.flow` from the file when the
|
|
142
|
-
* CLI didn't set them — a `--config`-only run (no `--protocol`/`--flow`
|
|
143
|
-
* flags at all) must still resolve to a real flow, or nothing downstream
|
|
144
|
-
* ever calls a builder at all.
|
|
145
|
-
*
|
|
146
|
-
* A no-op when `fileConfig` is `null` (no `--config` was given), so callers
|
|
147
|
-
* can invoke this unconditionally.
|
|
148
|
-
*/
|
|
149
|
-
function applyFileConfig(options, fileConfig) {
|
|
150
|
-
if (!fileConfig) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
options.protocol = options.protocol ?? fileConfig.protocol;
|
|
154
|
-
options.flow = options.flow ?? fileConfig.flow;
|
|
155
|
-
const fields = (fileConfig.config ?? {});
|
|
156
|
-
for (const [field, guidance] of Object.entries(UNSUPPORTED_LEGACY_CONFIG_FIELDS)) {
|
|
157
|
-
if (fields[field] !== undefined) {
|
|
158
|
-
console.error(`❌ --config: ${guidance}`);
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Pre-2.0.0 field name for the OIDC manual/OOB code paste path; maps onto
|
|
163
|
-
// the same slot --code fills.
|
|
164
|
-
if (options.code === undefined &&
|
|
165
|
-
typeof fields.authorizationCode === 'string') {
|
|
166
|
-
options.code = fields.authorizationCode;
|
|
167
|
-
}
|
|
168
|
-
for (const field of CONFIG_BACKFILL_FIELDS) {
|
|
169
|
-
if (options[field] === undefined && fields[field] !== undefined) {
|
|
170
|
-
options[field] = fields[field];
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (fields.idpInitiated !== undefined) {
|
|
174
|
-
// A string "false" would be truthy and silently declare the opposite.
|
|
175
|
-
if (typeof fields.idpInitiated !== 'boolean') {
|
|
176
|
-
console.error("❌ --config: 'idpInitiated' must be true or false.");
|
|
177
|
-
process.exit(1);
|
|
178
|
-
}
|
|
179
|
-
options.idpInitiated = options.idpInitiated ?? fields.idpInitiated;
|
|
180
|
-
}
|
|
181
|
-
if (fields.idpCertificates !== undefined) {
|
|
182
|
-
const raw = fields.idpCertificates;
|
|
183
|
-
const list = typeof raw === 'string' ? [raw] : raw;
|
|
184
|
-
if (!Array.isArray(list) ||
|
|
185
|
-
!list.every((entry) => typeof entry === 'string')) {
|
|
186
|
-
console.error("❌ --config: 'idpCertificates' must be a string or a list of strings (PEM or base64 DER).");
|
|
187
|
-
process.exit(1);
|
|
188
|
-
}
|
|
189
|
-
// Trust is replaced, never widened: a certificate named on the CLI means
|
|
190
|
-
// the file's list is not trusted alongside it. A rotated-out certificate
|
|
191
|
-
// left in the file must not keep verifying assertions.
|
|
192
|
-
if (options.idpCertificates === undefined &&
|
|
193
|
-
options.idpCertificateFiles === undefined) {
|
|
194
|
-
options.idpCertificates = list;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const PEM_CERTIFICATE_BLOCK = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
|
199
|
-
const BASE64_TEXT = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
200
|
-
/**
|
|
201
|
-
* Reads one `--idp-cert` file into the entries `idpCertificates` takes. A
|
|
202
|
-
* PEM file may hold several certificates (a rotation bundle); each becomes
|
|
203
|
-
* its own entry, since a single string is read as one certificate and the
|
|
204
|
-
* rest would be ignored. A binary DER file (`.cer`, `.der`) is base64-encoded.
|
|
205
|
-
* Whether the result is a certificate at all is the provider's check.
|
|
206
|
-
*/
|
|
207
|
-
function readIdpCertificateFile(filePath) {
|
|
208
|
-
const resolved = (0, node_path_1.resolve)(filePath);
|
|
209
|
-
let content;
|
|
210
|
-
try {
|
|
211
|
-
content = (0, node_fs_1.readFileSync)(resolved);
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
console.error(`❌ IdP certificate file not found: ${resolved}`);
|
|
215
|
-
process.exit(1);
|
|
216
|
-
}
|
|
217
|
-
const text = content.toString('utf8');
|
|
218
|
-
const blocks = text.match(PEM_CERTIFICATE_BLOCK);
|
|
219
|
-
if (blocks) {
|
|
220
|
-
return blocks;
|
|
221
|
-
}
|
|
222
|
-
if (text.includes('-----BEGIN') ||
|
|
223
|
-
BASE64_TEXT.test(text.replace(/\s+/g, ''))) {
|
|
224
|
-
return [text.trim()];
|
|
225
|
-
}
|
|
226
|
-
return [content.toString('base64')];
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* The certificates the SAML providers are given: inline ones (a `--config`
|
|
230
|
-
* file) and those read from `--idp-cert` files. `undefined` when there are
|
|
231
|
-
* none, so the provider's own `ValidationError` names what is missing.
|
|
232
|
-
*/
|
|
233
|
-
function resolveIdpCertificates(options) {
|
|
234
|
-
const certificates = [
|
|
235
|
-
...(options.idpCertificates ?? []),
|
|
236
|
-
...(options.idpCertificateFiles ?? []).flatMap(readIdpCertificateFile),
|
|
237
|
-
];
|
|
238
|
-
return certificates.length > 0 ? certificates : undefined;
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Parses the SAML trust flags into `target`, returning how many values after
|
|
242
|
-
* `arg` were consumed (0 for a flag with no value, or an argument that is not
|
|
243
|
-
* one of these). Kept here rather than in bin/mcp-sso.ts so it can be tested.
|
|
244
|
-
*/
|
|
245
|
-
function parseSamlTrustArg(target, arg, next) {
|
|
246
|
-
switch (arg) {
|
|
247
|
-
case '--idp-cert':
|
|
248
|
-
if (!next || next.startsWith('--')) {
|
|
249
|
-
console.error('❌ --idp-cert needs a certificate file path.');
|
|
250
|
-
process.exit(1);
|
|
251
|
-
}
|
|
252
|
-
// Repeatable: every occurrence adds a certificate, for key rotation.
|
|
253
|
-
target.idpCertificateFiles = [
|
|
254
|
-
...(target.idpCertificateFiles ?? []),
|
|
255
|
-
next,
|
|
256
|
-
];
|
|
257
|
-
return 1;
|
|
258
|
-
case '--idp-entity-id':
|
|
259
|
-
target.idpEntityId = next;
|
|
260
|
-
return 1;
|
|
261
|
-
case '--idp-metadata':
|
|
262
|
-
if (!next || next.startsWith('--')) {
|
|
263
|
-
console.error('❌ --idp-metadata needs an https URL or a file path.');
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
target.idpMetadata = next;
|
|
267
|
-
return 1;
|
|
268
|
-
case '--idp-initiated':
|
|
269
|
-
// A flag with no value. Left undefined when absent, so a --config
|
|
270
|
-
// file's idpInitiated can still fill it.
|
|
271
|
-
target.idpInitiated = true;
|
|
272
|
-
return 0;
|
|
273
|
-
case '--authn-request-id':
|
|
274
|
-
target.authnRequestId = next;
|
|
275
|
-
return 1;
|
|
276
|
-
default:
|
|
277
|
-
return 0;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
function requireOption(value, flagName) {
|
|
281
|
-
if (!value) {
|
|
282
|
-
console.error(`❌ ${flagName} is required for this flow.`);
|
|
283
|
-
process.exit(1);
|
|
284
|
-
}
|
|
285
|
-
return value;
|
|
286
|
-
}
|
|
287
|
-
function resolveOidcTokenEndpoint(options) {
|
|
288
|
-
return (options.tokenEndpoint ||
|
|
289
|
-
(options.uaaUrl
|
|
290
|
-
? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
|
|
291
|
-
: undefined));
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Only the OIDC 'browser' flow opens a browser; routes `--browser`,
|
|
295
|
-
* `--redirect-port` and manual/OOB code paste into the strategy that
|
|
296
|
-
* replaces them.
|
|
297
|
-
*/
|
|
298
|
-
function buildOidcBrowserAuthorization(options) {
|
|
299
|
-
if (options.code) {
|
|
300
|
-
// The consumer already holds the code (manual paste / OOB redirect
|
|
301
|
-
// URI); no callback server is opened at all.
|
|
302
|
-
return (0, auth_providers_1.asOidcResult)((0, auth_providers_1.staticCodeStrategy)({
|
|
303
|
-
redirectUri: options.redirectUri,
|
|
304
|
-
payload: options.code,
|
|
305
|
-
}));
|
|
306
|
-
}
|
|
307
|
-
// No fallback: an omitted --redirect-port lets the strategy bind its own
|
|
308
|
-
// default port rather than this CLI pinning a number it doesn't own.
|
|
309
|
-
return (0, auth_providers_1.oidcCallbackStrategy)({
|
|
310
|
-
port: options.redirectPort,
|
|
311
|
-
browser: options.browser,
|
|
312
|
-
timeoutMs: exports.INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
function buildOidcCommon(options) {
|
|
316
|
-
return {
|
|
317
|
-
issuerUrl: options.issuerUrl,
|
|
318
|
-
clientId: requireOption(options.clientId, '--client-id'),
|
|
319
|
-
clientSecret: options.clientSecret,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
function buildOidcBrowserConfig(options) {
|
|
323
|
-
return {
|
|
324
|
-
...buildOidcCommon(options),
|
|
325
|
-
authorizationEndpoint: options.authorizationEndpoint,
|
|
326
|
-
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
327
|
-
scopes: options.scopes,
|
|
328
|
-
authorization: buildOidcBrowserAuthorization(options),
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
function buildOidcDeviceConfig(options) {
|
|
332
|
-
// Device flow never opens a browser from this process; --browser and
|
|
333
|
-
// --redirect-port have nothing to attach to here.
|
|
334
|
-
return {
|
|
335
|
-
...buildOidcCommon(options),
|
|
336
|
-
deviceAuthorizationEndpoint: options.deviceAuthorizationEndpoint,
|
|
337
|
-
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
338
|
-
scopes: options.scopes,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function buildOidcPasswordConfig(options) {
|
|
342
|
-
// Password flow never opens a browser; --browser and --redirect-port are
|
|
343
|
-
// not meaningful here either.
|
|
344
|
-
const passcode = options.passcode;
|
|
345
|
-
const username = options.username || (passcode ? 'passcode' : undefined);
|
|
346
|
-
const password = options.password || passcode;
|
|
347
|
-
return {
|
|
348
|
-
...buildOidcCommon(options),
|
|
349
|
-
username: requireOption(username, '--username (or --passcode)'),
|
|
350
|
-
password: requireOption(password, '--password (or --passcode)'),
|
|
351
|
-
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
352
|
-
scopes: options.scopes,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
function buildOidcTokenExchangeConfig(options) {
|
|
356
|
-
// Token exchange never opens a browser; --browser and --redirect-port are
|
|
357
|
-
// not meaningful here either.
|
|
358
|
-
return {
|
|
359
|
-
...buildOidcCommon(options),
|
|
360
|
-
subjectToken: requireOption(options.subjectToken, '--subject-token'),
|
|
361
|
-
subjectTokenType: options.subjectTokenType ||
|
|
362
|
-
'urn:ietf:params:oauth:token-type:access_token',
|
|
363
|
-
scope: options.scope,
|
|
364
|
-
audience: options.audience,
|
|
365
|
-
actorToken: options.actorToken,
|
|
366
|
-
actorTokenType: options.actorTokenType,
|
|
367
|
-
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Both SAML flows (bearer, pure) can open a browser; routes `--browser`,
|
|
372
|
-
* `--redirect-port` and the manual/static assertion options into the
|
|
373
|
-
* strategy that replaces them.
|
|
374
|
-
*/
|
|
375
|
-
function buildSamlAuthorization(options) {
|
|
376
|
-
if (options.assertion) {
|
|
377
|
-
// The consumer already holds the assertion; nothing is opened or asked.
|
|
378
|
-
return (0, auth_providers_1.staticCodeStrategy)({
|
|
379
|
-
redirectUri: options.acsUrl,
|
|
380
|
-
payload: options.assertion,
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
if (options.idpInitiated) {
|
|
384
|
-
return buildIdpInitiatedAuthorization(options);
|
|
385
|
-
}
|
|
386
|
-
const assertionFlow = options.assertionFlow || 'browser';
|
|
387
|
-
if (assertionFlow !== 'browser') {
|
|
388
|
-
// 'manual', and an 'assertion' flow given no value, both need a human to
|
|
389
|
-
// lift the SAMLResponse out of the POST body by hand.
|
|
390
|
-
return (0, auth_providers_1.manualSamlResponseStrategy)({
|
|
391
|
-
redirectUri: options.acsUrl,
|
|
392
|
-
read: readManualInput,
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
// No fallback: an omitted --redirect-port lets the strategy bind its own
|
|
396
|
-
// default port rather than this CLI pinning a number it doesn't own.
|
|
397
|
-
return (0, auth_providers_1.samlCallbackStrategy)({
|
|
398
|
-
port: options.redirectPort,
|
|
399
|
-
browser: options.browser,
|
|
400
|
-
timeoutMs: exports.INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* An IdP-initiated login has no URL for this CLI to open: the only one
|
|
405
|
-
* auth-providers can build carries an AuthnRequest, and with `idpInitiated`
|
|
406
|
-
* it refuses to build one. So the strategy never calls
|
|
407
|
-
* `buildAuthorizationUrl` — the user starts the login at the identity
|
|
408
|
-
* provider and pastes the SAMLResponse it posts. The browser flow, which
|
|
409
|
-
* exists to open that URL, is refused rather than left to fail after the
|
|
410
|
-
* provider is built.
|
|
411
|
-
*/
|
|
412
|
-
function buildIdpInitiatedAuthorization(options) {
|
|
413
|
-
if (options.assertionFlow === 'browser') {
|
|
414
|
-
console.error('❌ --idp-initiated cannot use --assertion-flow browser: there is no request URL to open. ' +
|
|
415
|
-
'Start the login at the identity provider and use --assertion-flow manual, or pass --assertion.');
|
|
416
|
-
process.exit(1);
|
|
417
|
-
}
|
|
418
|
-
// The ACS the assertion names as its Recipient; the same fallback the
|
|
419
|
-
// auth-providers strategies use when none is declared.
|
|
420
|
-
const redirectUri = options.acsUrl ?? `http://localhost:${auth_providers_1.DEFAULT_CALLBACK_PORT}/callback`;
|
|
421
|
-
return {
|
|
422
|
-
async authorize() {
|
|
423
|
-
const payload = await readManualInput('Start the login at your identity provider, then paste the SAMLResponse (from the POST body): ');
|
|
424
|
-
if (!payload) {
|
|
425
|
-
throw new Error('No SAMLResponse was provided');
|
|
426
|
-
}
|
|
427
|
-
return { payload, redirectUri };
|
|
428
|
-
},
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
function buildSamlCookieProvider(options) {
|
|
432
|
-
const assertionFlow = options.assertionFlow || (options.assertion ? 'assertion' : 'browser');
|
|
433
|
-
return async (samlResponse) => {
|
|
434
|
-
if (options.cookie) {
|
|
435
|
-
return options.cookie;
|
|
436
|
-
}
|
|
437
|
-
if (assertionFlow === 'assertion') {
|
|
438
|
-
return `SAMLResponse=${samlResponse}`;
|
|
439
|
-
}
|
|
440
|
-
return readManualInput('Paste session cookies: ');
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* What auth-providers 4 validates an assertion against. Absent trust material
|
|
445
|
-
* is passed as absent: the provider's constructor names what is missing.
|
|
446
|
-
*/
|
|
447
|
-
function buildSamlTrust(options) {
|
|
448
|
-
return {
|
|
449
|
-
idpCertificates: resolveIdpCertificates(options),
|
|
450
|
-
idpEntityId: options.idpEntityId,
|
|
451
|
-
idpInitiated: options.idpInitiated,
|
|
452
|
-
authnRequestId: options.authnRequestId,
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
function buildSamlBearerConfig(options) {
|
|
456
|
-
return {
|
|
457
|
-
idpSsoUrl: requireOption(options.idpSsoUrl, '--idp-sso-url'),
|
|
458
|
-
spEntityId: requireOption(options.spEntityId, '--sp-entity-id'),
|
|
459
|
-
acsUrl: options.acsUrl,
|
|
460
|
-
relayState: options.relayState,
|
|
461
|
-
...buildSamlTrust(options),
|
|
462
|
-
tokenUrl: options.tokenEndpoint,
|
|
463
|
-
uaaUrl: options.uaaUrl,
|
|
464
|
-
clientId: options.clientId,
|
|
465
|
-
clientSecret: options.clientSecret,
|
|
466
|
-
authorization: buildSamlAuthorization(options),
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
function buildSamlPureConfig(options) {
|
|
470
|
-
return {
|
|
471
|
-
idpSsoUrl: requireOption(options.idpSsoUrl, '--idp-sso-url'),
|
|
472
|
-
spEntityId: requireOption(options.spEntityId, '--sp-entity-id'),
|
|
473
|
-
acsUrl: options.acsUrl,
|
|
474
|
-
relayState: options.relayState,
|
|
475
|
-
...buildSamlTrust(options),
|
|
476
|
-
authorization: buildSamlAuthorization(options),
|
|
477
|
-
cookieProvider: buildSamlCookieProvider(options),
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Builds the effective `SsoProviderConfig` from `options` alone.
|
|
482
|
-
*
|
|
483
|
-
* `options` must already be the *merged* view — `--config` file fields
|
|
484
|
-
* backfilled beneath whatever the CLI flags set, via `applyFileConfig` — so
|
|
485
|
-
* this never needs a separate file/CLI merge step, and required-field
|
|
486
|
-
* validation (`requireOption`, inside each builder above) sees the same
|
|
487
|
-
* merged data regardless of whether a value came from the file or a flag.
|
|
488
|
-
*/
|
|
489
|
-
function buildProviderConfig(options, existingAuth, existingConn) {
|
|
490
|
-
if (!options.protocol || !options.flow) {
|
|
491
|
-
throw new Error('Provider config is missing. Use --config or --protocol/--flow options.');
|
|
492
|
-
}
|
|
493
|
-
let result;
|
|
494
|
-
if (options.protocol === 'oidc') {
|
|
495
|
-
switch (options.flow) {
|
|
496
|
-
case 'browser':
|
|
497
|
-
result = {
|
|
498
|
-
protocol: 'oidc',
|
|
499
|
-
flow: 'browser',
|
|
500
|
-
config: buildOidcBrowserConfig(options),
|
|
501
|
-
};
|
|
502
|
-
break;
|
|
503
|
-
case 'device':
|
|
504
|
-
result = {
|
|
505
|
-
protocol: 'oidc',
|
|
506
|
-
flow: 'device',
|
|
507
|
-
config: buildOidcDeviceConfig(options),
|
|
508
|
-
};
|
|
509
|
-
break;
|
|
510
|
-
case 'password':
|
|
511
|
-
result = {
|
|
512
|
-
protocol: 'oidc',
|
|
513
|
-
flow: 'password',
|
|
514
|
-
config: buildOidcPasswordConfig(options),
|
|
515
|
-
};
|
|
516
|
-
break;
|
|
517
|
-
case 'token_exchange':
|
|
518
|
-
result = {
|
|
519
|
-
protocol: 'oidc',
|
|
520
|
-
flow: 'token_exchange',
|
|
521
|
-
config: buildOidcTokenExchangeConfig(options),
|
|
522
|
-
};
|
|
523
|
-
break;
|
|
524
|
-
default:
|
|
525
|
-
throw new Error(`Unsupported OIDC flow: ${options.flow}`);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
else if (options.protocol === 'saml2') {
|
|
529
|
-
switch (options.flow) {
|
|
530
|
-
case 'bearer':
|
|
531
|
-
result = {
|
|
532
|
-
protocol: 'saml2',
|
|
533
|
-
flow: 'bearer',
|
|
534
|
-
config: buildSamlBearerConfig(options),
|
|
535
|
-
};
|
|
536
|
-
break;
|
|
537
|
-
case 'pure':
|
|
538
|
-
result = {
|
|
539
|
-
protocol: 'saml2',
|
|
540
|
-
flow: 'pure',
|
|
541
|
-
config: buildSamlPureConfig(options),
|
|
542
|
-
};
|
|
543
|
-
break;
|
|
544
|
-
default:
|
|
545
|
-
throw new Error(`Unsupported SAML flow: ${options.flow}`);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
throw new Error(`Unsupported protocol: ${options.protocol}`);
|
|
550
|
-
}
|
|
551
|
-
const accessToken = existingConn?.authorizationToken;
|
|
552
|
-
const refreshToken = existingAuth?.refreshToken;
|
|
553
|
-
if ((result.protocol === 'oidc' ||
|
|
554
|
-
(result.protocol === 'saml2' && result.flow === 'bearer')) &&
|
|
555
|
-
(accessToken || refreshToken)) {
|
|
556
|
-
result = {
|
|
557
|
-
...result,
|
|
558
|
-
config: {
|
|
559
|
-
...result.config,
|
|
560
|
-
accessToken,
|
|
561
|
-
refreshToken,
|
|
562
|
-
},
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
return result;
|
|
566
|
-
}
|