@mcp-abap-adt/auth-broker 3.0.4 → 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 +52 -1
- package/README.md +34 -312
- package/package.json +15 -42
- 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/mcp-sso.js
DELETED
|
@@ -1,825 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
/**
|
|
4
|
-
* MCP SSO - Get tokens via SSO providers and generate .env files
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* mcp-sso <oidc|saml2|bearer> [options]
|
|
8
|
-
* mcp-sso --protocol <oidc|saml2> --flow <flow> --output <path> [options]
|
|
9
|
-
*
|
|
10
|
-
* Examples:
|
|
11
|
-
* # OIDC browser flow (authorization code with local callback)
|
|
12
|
-
* mcp-sso oidc --flow browser --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
|
|
13
|
-
*
|
|
14
|
-
* # OIDC device flow
|
|
15
|
-
* mcp-sso oidc --flow device --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
|
|
16
|
-
*
|
|
17
|
-
* # OIDC password flow
|
|
18
|
-
* mcp-sso oidc --flow password --token-endpoint https://issuer/oauth/token --client-id my-client --username user --password pass --output ./sso.env --type xsuaa
|
|
19
|
-
*
|
|
20
|
-
* # OIDC token exchange
|
|
21
|
-
* mcp-sso oidc --flow token_exchange --issuer https://issuer --client-id my-client --subject-token <token> --output ./sso.env --type xsuaa
|
|
22
|
-
*
|
|
23
|
-
* # SAML bearer flow with a service key: the Audience, Recipient and token alias come from
|
|
24
|
-
* # <uaa.url>/saml/metadata, the IdP's trust from its metadata
|
|
25
|
-
* mcp-sso bearer --service-key ./service-key.json --idp-metadata https://<ias-tenant>.accounts.ondemand.com/saml2/metadata --idp-initiated --output ./sso.env --type xsuaa
|
|
26
|
-
*
|
|
27
|
-
* # SAML bearer flow, everything stated (IdP-initiated assertion, as UAA/XSUAA require)
|
|
28
|
-
* mcp-sso bearer --idp-sso-url https://idp/sso --sp-entity-id <uaa-entity-id> --acs-url <uaa-bearer-acs> --idp-cert ./idp-signing.pem --idp-entity-id https://idp/metadata --idp-initiated --token-endpoint https://uaa.example/oauth/token --assertion <base64> --output ./sso.env --type xsuaa
|
|
29
|
-
*
|
|
30
|
-
* # SAML pure flow (cookie)
|
|
31
|
-
* mcp-sso saml2 --flow pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --idp-cert ./idp-signing.pem --idp-entity-id https://idp/metadata --cookie "SAP_SESSION=..." --output ./sso.env --type abap
|
|
32
|
-
*/
|
|
33
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
34
|
-
if (k2 === undefined) k2 = k;
|
|
35
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
36
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
37
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
38
|
-
}
|
|
39
|
-
Object.defineProperty(o, k2, desc);
|
|
40
|
-
}) : (function(o, m, k, k2) {
|
|
41
|
-
if (k2 === undefined) k2 = k;
|
|
42
|
-
o[k2] = m[k];
|
|
43
|
-
}));
|
|
44
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
45
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
46
|
-
}) : function(o, v) {
|
|
47
|
-
o["default"] = v;
|
|
48
|
-
});
|
|
49
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
50
|
-
var ownKeys = function(o) {
|
|
51
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
52
|
-
var ar = [];
|
|
53
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
54
|
-
return ar;
|
|
55
|
-
};
|
|
56
|
-
return ownKeys(o);
|
|
57
|
-
};
|
|
58
|
-
return function (mod) {
|
|
59
|
-
if (mod && mod.__esModule) return mod;
|
|
60
|
-
var result = {};
|
|
61
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
62
|
-
__setModuleDefault(result, mod);
|
|
63
|
-
return result;
|
|
64
|
-
};
|
|
65
|
-
})();
|
|
66
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
|
-
const fs = __importStar(require("fs"));
|
|
68
|
-
const path = __importStar(require("path"));
|
|
69
|
-
// Use require for CommonJS dist files with absolute path
|
|
70
|
-
const distPath = path.resolve(__dirname, '..', 'index.js');
|
|
71
|
-
const { AuthBroker } = require(distPath);
|
|
72
|
-
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
73
|
-
const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
|
|
74
|
-
const logger_1 = require("@mcp-abap-adt/logger");
|
|
75
|
-
const mcpSsoConfig_1 = require("./mcpSsoConfig");
|
|
76
|
-
const samlMetadata_1 = require("./samlMetadata");
|
|
77
|
-
const workDir_1 = require("./workDir");
|
|
78
|
-
function getVersion() {
|
|
79
|
-
try {
|
|
80
|
-
const candidates = [
|
|
81
|
-
path.join(__dirname, 'package.json'),
|
|
82
|
-
path.join(__dirname, '..', 'package.json'),
|
|
83
|
-
path.join(__dirname, '..', '..', 'package.json'),
|
|
84
|
-
];
|
|
85
|
-
for (const candidate of candidates) {
|
|
86
|
-
if (fs.existsSync(candidate)) {
|
|
87
|
-
const packageJson = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
88
|
-
return packageJson.version || 'unknown';
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const localRequire = require('module').createRequire(__filename);
|
|
92
|
-
const resolved = localRequire.resolve('@mcp-abap-adt/auth-broker/package.json');
|
|
93
|
-
const packageJson = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
94
|
-
return packageJson.version || 'unknown';
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
return 'unknown';
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function showHelp() {
|
|
101
|
-
console.log('MCP SSO - Get tokens via SSO providers and generate .env files');
|
|
102
|
-
console.log('');
|
|
103
|
-
console.log('Usage:');
|
|
104
|
-
console.log(' mcp-sso <oidc|saml2|bearer> [options]');
|
|
105
|
-
console.log(' mcp-sso --protocol <oidc|saml2> --flow <flow> --output <path> [options]');
|
|
106
|
-
console.log('');
|
|
107
|
-
console.log('Required Options:');
|
|
108
|
-
console.log(' --output <path> Output file path');
|
|
109
|
-
console.log(' --protocol <oidc|saml2> Protocol (if no subcommand)');
|
|
110
|
-
console.log(' --flow <flow> Flow for protocol (if no subcommand)');
|
|
111
|
-
console.log('');
|
|
112
|
-
console.log('Common Options:');
|
|
113
|
-
console.log(' --service-key <path> Service key JSON (XSUAA/ABAP)');
|
|
114
|
-
console.log(' --type <abap|xsuaa> Output type (default: abap)');
|
|
115
|
-
console.log(' --format <env|json> Output format (default: env)');
|
|
116
|
-
console.log(' --env <path> Optional existing env file (used for refresh)');
|
|
117
|
-
console.log(' --destination <name> Destination name (default: output file base)');
|
|
118
|
-
console.log(' --service-url <url> Service URL (ABAP: SAP URL, XSUAA: MCP URL)');
|
|
119
|
-
console.log(' --config <path> JSON config file (SSO provider config)');
|
|
120
|
-
console.log(' --browser <browser> Browser: auto|none|system|chrome|edge|firefox');
|
|
121
|
-
console.log(' --redirect-port <port> Redirect port for browser flows (default: from auth-providers, currently 61001)');
|
|
122
|
-
console.log(' --redirect-uri <uri> Custom redirect URI (OOB/manual code flows)');
|
|
123
|
-
console.log('');
|
|
124
|
-
console.log('OIDC Options:');
|
|
125
|
-
console.log(' --issuer <url> OIDC issuer/discovery URL');
|
|
126
|
-
console.log(' --authorization-endpoint <url> Authorization endpoint');
|
|
127
|
-
console.log(' --token-endpoint <url> Token endpoint');
|
|
128
|
-
console.log(' --device-authorization-endpoint <url> Device auth endpoint');
|
|
129
|
-
console.log(' --client-id <id> OAuth client id');
|
|
130
|
-
console.log(' --client-secret <secret> OAuth client secret');
|
|
131
|
-
console.log(' --scopes <csv> Scopes list (comma or space-separated)');
|
|
132
|
-
console.log(' --scope <value> Scope for token exchange');
|
|
133
|
-
console.log(' --code <value> Authorization code (manual)');
|
|
134
|
-
console.log(' --username <value> Username for password flow');
|
|
135
|
-
console.log(' --password <value> Password for password flow');
|
|
136
|
-
console.log(' --passcode <value> Passcode (alias for password, username=passcode)');
|
|
137
|
-
console.log(' --subject-token <token> Subject token for token exchange');
|
|
138
|
-
console.log(' --subject-token-type <type> Subject token type (default: access_token)');
|
|
139
|
-
console.log(' --audience <value> Audience for token exchange');
|
|
140
|
-
console.log(' --actor-token <token> Actor token for token exchange');
|
|
141
|
-
console.log(' --actor-token-type <type> Actor token type for token exchange');
|
|
142
|
-
console.log(' --uaa-url <url> UAA base URL (used to build token endpoint)');
|
|
143
|
-
console.log('');
|
|
144
|
-
console.log('SAML Options:');
|
|
145
|
-
console.log(' --idp-sso-url <url> IdP SSO URL');
|
|
146
|
-
console.log(' --sp-entity-id <id> SP Entity ID; also the Audience the assertion must name');
|
|
147
|
-
console.log(' --acs-url <url> ACS URL; the Recipient the assertion must name (default: http://localhost:<port>/callback)');
|
|
148
|
-
console.log(' --idp-cert <path> IdP signing certificate file (PEM or DER); repeat for key rotation');
|
|
149
|
-
console.log(' --idp-entity-id <id> IdP entityID; the Issuer the assertion must name');
|
|
150
|
-
console.log(' --idp-metadata <url|path> IdP SAML metadata (https or file): fills --idp-cert, --idp-entity-id');
|
|
151
|
-
console.log(' and --idp-sso-url where not given, e.g. https://<ias>/saml2/metadata');
|
|
152
|
-
console.log(' --idp-initiated The IdP starts the login; no AuthnRequest is sent (required for bearer');
|
|
153
|
-
console.log(' against UAA/XSUAA). Use with --assertion or --assertion-flow manual');
|
|
154
|
-
console.log(' --authn-request-id <id> AuthnRequest ID an --assertion answers (SP-initiated, request sent elsewhere)');
|
|
155
|
-
console.log(' --saml-metadata <path> XSUAA SP metadata; bearer reads it for the token alias, --acs-url and');
|
|
156
|
-
console.log(' --sp-entity-id. With --service-key, <uaa.url>/saml/metadata is read instead');
|
|
157
|
-
console.log(' --relay-state <value> RelayState (optional)');
|
|
158
|
-
console.log(' --assertion-flow <flow> browser|manual|assertion (default: browser; manual with --idp-initiated)');
|
|
159
|
-
console.log(' --assertion <base64> SAMLResponse (base64)');
|
|
160
|
-
console.log(' --cookie <value> Session cookies (for pure SAML)');
|
|
161
|
-
console.log(' --token-endpoint <url> Token endpoint for SAML bearer exchange');
|
|
162
|
-
console.log('');
|
|
163
|
-
console.log(' Every SAML assertion is validated before use. Both SAML flows require --idp-cert');
|
|
164
|
-
console.log(' and --idp-entity-id, or --idp-metadata (or idpCertificates/idpEntityId in --config). An --assertion');
|
|
165
|
-
console.log(' also needs --idp-initiated or --authn-request-id; the browser and manual flows');
|
|
166
|
-
console.log(' send their own request unless --idp-initiated is given.');
|
|
167
|
-
console.log('');
|
|
168
|
-
console.log(' version, --version, -v Show version number');
|
|
169
|
-
console.log(' help, --help, -h Show this help message');
|
|
170
|
-
}
|
|
171
|
-
function parseScopes(value) {
|
|
172
|
-
if (!value)
|
|
173
|
-
return undefined;
|
|
174
|
-
const parts = value
|
|
175
|
-
.split(/[,\s]+/)
|
|
176
|
-
.map((p) => p.trim())
|
|
177
|
-
.filter(Boolean);
|
|
178
|
-
return parts.length > 0 ? parts : undefined;
|
|
179
|
-
}
|
|
180
|
-
function createCliLogger(prefix = 'SSO') {
|
|
181
|
-
const isEnabled = () => {
|
|
182
|
-
if (process.env.DEBUG_SSO === 'false' ||
|
|
183
|
-
process.env.DEBUG_AUTH_SSO === 'false') {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
if (process.env.DEBUG_SSO === 'true' ||
|
|
187
|
-
process.env.DEBUG_AUTH_SSO === 'true' ||
|
|
188
|
-
process.env.DEBUG === 'true' ||
|
|
189
|
-
process.env.DEBUG?.includes('sso') === true ||
|
|
190
|
-
process.env.DEBUG?.includes('auth-sso') === true) {
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
return false;
|
|
194
|
-
};
|
|
195
|
-
const baseLogger = new logger_1.DefaultLogger((0, logger_1.getLogLevel)());
|
|
196
|
-
return {
|
|
197
|
-
debug: (message, meta) => {
|
|
198
|
-
if (isEnabled()) {
|
|
199
|
-
baseLogger.debug(`[${prefix}] ${message}`, meta);
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
info: (message, meta) => {
|
|
203
|
-
if (isEnabled()) {
|
|
204
|
-
baseLogger.info(`[${prefix}] ${message}`, meta);
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
warn: (message, meta) => {
|
|
208
|
-
if (isEnabled()) {
|
|
209
|
-
baseLogger.warn(`[${prefix}] ${message}`, meta);
|
|
210
|
-
}
|
|
211
|
-
},
|
|
212
|
-
error: (message, meta) => {
|
|
213
|
-
if (isEnabled()) {
|
|
214
|
-
baseLogger.error(`[${prefix}] ${message}`, meta);
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
function parseArgs() {
|
|
220
|
-
let args = process.argv.slice(2);
|
|
221
|
-
if (args.length === 0 ||
|
|
222
|
-
args[0] === 'help' ||
|
|
223
|
-
args.includes('--help') ||
|
|
224
|
-
args.includes('-h')) {
|
|
225
|
-
showHelp();
|
|
226
|
-
process.exit(0);
|
|
227
|
-
}
|
|
228
|
-
if (args[0] === 'version' ||
|
|
229
|
-
args.includes('--version') ||
|
|
230
|
-
args.includes('-v')) {
|
|
231
|
-
console.log(getVersion());
|
|
232
|
-
process.exit(0);
|
|
233
|
-
}
|
|
234
|
-
let outputFile;
|
|
235
|
-
let envFilePath;
|
|
236
|
-
let destination;
|
|
237
|
-
let serviceKeyPath;
|
|
238
|
-
let authType = 'abap';
|
|
239
|
-
let format = 'env';
|
|
240
|
-
let protocol;
|
|
241
|
-
let flow;
|
|
242
|
-
let configPath;
|
|
243
|
-
let serviceUrl;
|
|
244
|
-
let browser;
|
|
245
|
-
let redirectPort;
|
|
246
|
-
let redirectUri;
|
|
247
|
-
let issuerUrl;
|
|
248
|
-
let authorizationEndpoint;
|
|
249
|
-
let tokenEndpoint;
|
|
250
|
-
let deviceAuthorizationEndpoint;
|
|
251
|
-
let clientId;
|
|
252
|
-
let clientSecret;
|
|
253
|
-
let scopes;
|
|
254
|
-
let scope;
|
|
255
|
-
let code;
|
|
256
|
-
let username;
|
|
257
|
-
let password;
|
|
258
|
-
let passcode;
|
|
259
|
-
let subjectToken;
|
|
260
|
-
let subjectTokenType;
|
|
261
|
-
let audience;
|
|
262
|
-
let actorToken;
|
|
263
|
-
let actorTokenType;
|
|
264
|
-
let idpSsoUrl;
|
|
265
|
-
let spEntityId;
|
|
266
|
-
let acsUrl;
|
|
267
|
-
let relayState;
|
|
268
|
-
let assertionFlow;
|
|
269
|
-
let assertion;
|
|
270
|
-
let cookie;
|
|
271
|
-
let uaaUrl;
|
|
272
|
-
let samlMetadataPath;
|
|
273
|
-
const samlTrust = {};
|
|
274
|
-
const firstArg = args[0];
|
|
275
|
-
if (firstArg && !firstArg.startsWith('-')) {
|
|
276
|
-
if (firstArg === 'oidc') {
|
|
277
|
-
protocol = 'oidc';
|
|
278
|
-
}
|
|
279
|
-
else if (firstArg === 'saml2') {
|
|
280
|
-
protocol = 'saml2';
|
|
281
|
-
}
|
|
282
|
-
else if (firstArg === 'bearer') {
|
|
283
|
-
protocol = 'saml2';
|
|
284
|
-
flow = 'bearer';
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
console.error(`Unknown command: ${firstArg}`);
|
|
288
|
-
showHelp();
|
|
289
|
-
process.exit(1);
|
|
290
|
-
}
|
|
291
|
-
args = args.slice(1);
|
|
292
|
-
}
|
|
293
|
-
for (let i = 0; i < args.length; i++) {
|
|
294
|
-
const arg = args[i];
|
|
295
|
-
const next = i + 1 < args.length ? args[i + 1] : undefined;
|
|
296
|
-
switch (arg) {
|
|
297
|
-
case '--output':
|
|
298
|
-
outputFile = next;
|
|
299
|
-
i++;
|
|
300
|
-
break;
|
|
301
|
-
case '--env':
|
|
302
|
-
envFilePath = next;
|
|
303
|
-
i++;
|
|
304
|
-
break;
|
|
305
|
-
case '--service-key':
|
|
306
|
-
serviceKeyPath = next;
|
|
307
|
-
i++;
|
|
308
|
-
break;
|
|
309
|
-
case '--destination':
|
|
310
|
-
destination = next;
|
|
311
|
-
i++;
|
|
312
|
-
break;
|
|
313
|
-
case '--type':
|
|
314
|
-
if (next === 'abap' || next === 'xsuaa') {
|
|
315
|
-
authType = next;
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
console.error(`Invalid type: ${next}. Use abap or xsuaa.`);
|
|
319
|
-
process.exit(1);
|
|
320
|
-
}
|
|
321
|
-
i++;
|
|
322
|
-
break;
|
|
323
|
-
case '--format':
|
|
324
|
-
if (next === 'env' || next === 'json') {
|
|
325
|
-
format = next;
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
console.error(`Invalid format: ${next}. Use env or json.`);
|
|
329
|
-
process.exit(1);
|
|
330
|
-
}
|
|
331
|
-
i++;
|
|
332
|
-
break;
|
|
333
|
-
case '--protocol':
|
|
334
|
-
if (next === 'oidc' || next === 'saml2') {
|
|
335
|
-
protocol = next;
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
console.error(`Invalid protocol: ${next}. Use oidc or saml2.`);
|
|
339
|
-
process.exit(1);
|
|
340
|
-
}
|
|
341
|
-
i++;
|
|
342
|
-
break;
|
|
343
|
-
case '--flow':
|
|
344
|
-
flow = next;
|
|
345
|
-
i++;
|
|
346
|
-
break;
|
|
347
|
-
case '--config':
|
|
348
|
-
configPath = next;
|
|
349
|
-
i++;
|
|
350
|
-
break;
|
|
351
|
-
case '--service-url':
|
|
352
|
-
serviceUrl = next;
|
|
353
|
-
i++;
|
|
354
|
-
break;
|
|
355
|
-
case '--browser':
|
|
356
|
-
browser = next;
|
|
357
|
-
i++;
|
|
358
|
-
break;
|
|
359
|
-
case '--redirect-port':
|
|
360
|
-
if (!next)
|
|
361
|
-
break;
|
|
362
|
-
redirectPort = parseInt(next, 10);
|
|
363
|
-
if (Number.isNaN(redirectPort) ||
|
|
364
|
-
redirectPort < 1 ||
|
|
365
|
-
redirectPort > 65535) {
|
|
366
|
-
console.error(`Invalid redirect port: ${next}`);
|
|
367
|
-
process.exit(1);
|
|
368
|
-
}
|
|
369
|
-
i++;
|
|
370
|
-
break;
|
|
371
|
-
case '--redirect-uri':
|
|
372
|
-
redirectUri = next;
|
|
373
|
-
i++;
|
|
374
|
-
break;
|
|
375
|
-
case '--issuer':
|
|
376
|
-
issuerUrl = next;
|
|
377
|
-
i++;
|
|
378
|
-
break;
|
|
379
|
-
case '--authorization-endpoint':
|
|
380
|
-
authorizationEndpoint = next;
|
|
381
|
-
i++;
|
|
382
|
-
break;
|
|
383
|
-
case '--token-endpoint':
|
|
384
|
-
tokenEndpoint = next;
|
|
385
|
-
i++;
|
|
386
|
-
break;
|
|
387
|
-
case '--device-authorization-endpoint':
|
|
388
|
-
deviceAuthorizationEndpoint = next;
|
|
389
|
-
i++;
|
|
390
|
-
break;
|
|
391
|
-
case '--client-id':
|
|
392
|
-
clientId = next;
|
|
393
|
-
i++;
|
|
394
|
-
break;
|
|
395
|
-
case '--client-secret':
|
|
396
|
-
clientSecret = next;
|
|
397
|
-
i++;
|
|
398
|
-
break;
|
|
399
|
-
case '--scopes':
|
|
400
|
-
scopes = parseScopes(next);
|
|
401
|
-
i++;
|
|
402
|
-
break;
|
|
403
|
-
case '--scope':
|
|
404
|
-
scope = next;
|
|
405
|
-
i++;
|
|
406
|
-
break;
|
|
407
|
-
case '--code':
|
|
408
|
-
code = next;
|
|
409
|
-
i++;
|
|
410
|
-
break;
|
|
411
|
-
case '--username':
|
|
412
|
-
username = next;
|
|
413
|
-
i++;
|
|
414
|
-
break;
|
|
415
|
-
case '--password':
|
|
416
|
-
password = next;
|
|
417
|
-
i++;
|
|
418
|
-
break;
|
|
419
|
-
case '--passcode':
|
|
420
|
-
passcode = next;
|
|
421
|
-
i++;
|
|
422
|
-
break;
|
|
423
|
-
case '--subject-token':
|
|
424
|
-
subjectToken = next;
|
|
425
|
-
i++;
|
|
426
|
-
break;
|
|
427
|
-
case '--subject-token-type':
|
|
428
|
-
subjectTokenType = next;
|
|
429
|
-
i++;
|
|
430
|
-
break;
|
|
431
|
-
case '--audience':
|
|
432
|
-
audience = next;
|
|
433
|
-
i++;
|
|
434
|
-
break;
|
|
435
|
-
case '--actor-token':
|
|
436
|
-
actorToken = next;
|
|
437
|
-
i++;
|
|
438
|
-
break;
|
|
439
|
-
case '--actor-token-type':
|
|
440
|
-
actorTokenType = next;
|
|
441
|
-
i++;
|
|
442
|
-
break;
|
|
443
|
-
case '--idp-sso-url':
|
|
444
|
-
idpSsoUrl = next;
|
|
445
|
-
i++;
|
|
446
|
-
break;
|
|
447
|
-
case '--sp-entity-id':
|
|
448
|
-
spEntityId = next;
|
|
449
|
-
i++;
|
|
450
|
-
break;
|
|
451
|
-
case '--acs-url':
|
|
452
|
-
acsUrl = next;
|
|
453
|
-
i++;
|
|
454
|
-
break;
|
|
455
|
-
case '--relay-state':
|
|
456
|
-
relayState = next;
|
|
457
|
-
i++;
|
|
458
|
-
break;
|
|
459
|
-
case '--assertion-flow':
|
|
460
|
-
if (next === 'browser' || next === 'manual' || next === 'assertion') {
|
|
461
|
-
assertionFlow = next;
|
|
462
|
-
}
|
|
463
|
-
else {
|
|
464
|
-
console.error(`Invalid assertion flow: ${next}. Use browser, manual, or assertion.`);
|
|
465
|
-
process.exit(1);
|
|
466
|
-
}
|
|
467
|
-
i++;
|
|
468
|
-
break;
|
|
469
|
-
case '--assertion':
|
|
470
|
-
assertion = next;
|
|
471
|
-
i++;
|
|
472
|
-
break;
|
|
473
|
-
case '--cookie':
|
|
474
|
-
cookie = next;
|
|
475
|
-
i++;
|
|
476
|
-
break;
|
|
477
|
-
case '--uaa-url':
|
|
478
|
-
uaaUrl = next;
|
|
479
|
-
i++;
|
|
480
|
-
break;
|
|
481
|
-
case '--saml-metadata':
|
|
482
|
-
samlMetadataPath = next;
|
|
483
|
-
i++;
|
|
484
|
-
break;
|
|
485
|
-
default:
|
|
486
|
-
i += (0, mcpSsoConfig_1.parseSamlTrustArg)(samlTrust, arg, next);
|
|
487
|
-
break;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
return {
|
|
491
|
-
outputFile,
|
|
492
|
-
envFilePath,
|
|
493
|
-
destination,
|
|
494
|
-
serviceKeyPath,
|
|
495
|
-
authType,
|
|
496
|
-
format,
|
|
497
|
-
protocol,
|
|
498
|
-
flow: flow,
|
|
499
|
-
configPath,
|
|
500
|
-
serviceUrl,
|
|
501
|
-
browser,
|
|
502
|
-
redirectPort,
|
|
503
|
-
redirectUri,
|
|
504
|
-
issuerUrl,
|
|
505
|
-
authorizationEndpoint,
|
|
506
|
-
tokenEndpoint,
|
|
507
|
-
deviceAuthorizationEndpoint,
|
|
508
|
-
clientId,
|
|
509
|
-
clientSecret,
|
|
510
|
-
scopes,
|
|
511
|
-
scope,
|
|
512
|
-
code,
|
|
513
|
-
username,
|
|
514
|
-
password,
|
|
515
|
-
passcode,
|
|
516
|
-
subjectToken,
|
|
517
|
-
subjectTokenType,
|
|
518
|
-
audience,
|
|
519
|
-
actorToken,
|
|
520
|
-
actorTokenType,
|
|
521
|
-
idpSsoUrl,
|
|
522
|
-
spEntityId,
|
|
523
|
-
acsUrl,
|
|
524
|
-
relayState,
|
|
525
|
-
assertionFlow,
|
|
526
|
-
assertion,
|
|
527
|
-
cookie,
|
|
528
|
-
uaaUrl,
|
|
529
|
-
samlMetadataPath,
|
|
530
|
-
...samlTrust,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
async function main() {
|
|
534
|
-
const options = parseArgs();
|
|
535
|
-
if (!options) {
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
const logger = createCliLogger();
|
|
539
|
-
if (!options.outputFile) {
|
|
540
|
-
console.error('❌ Missing required --output');
|
|
541
|
-
process.exit(1);
|
|
542
|
-
}
|
|
543
|
-
const resolvedOutputPath = path.resolve(options.outputFile);
|
|
544
|
-
const resolvedEnvPath = options.envFilePath
|
|
545
|
-
? path.resolve(options.envFilePath)
|
|
546
|
-
: undefined;
|
|
547
|
-
let destination = options.destination;
|
|
548
|
-
if (options.serviceKeyPath) {
|
|
549
|
-
const resolvedServiceKeyPath = path.resolve(options.serviceKeyPath);
|
|
550
|
-
if (!fs.existsSync(resolvedServiceKeyPath)) {
|
|
551
|
-
console.error(`❌ Service key file not found: ${resolvedServiceKeyPath}`);
|
|
552
|
-
process.exit(1);
|
|
553
|
-
}
|
|
554
|
-
const serviceKeyFileName = path.basename(resolvedServiceKeyPath, path.extname(resolvedServiceKeyPath));
|
|
555
|
-
if (destination && destination !== serviceKeyFileName) {
|
|
556
|
-
console.error(`❌ Destination mismatch: service key (${serviceKeyFileName}) vs output (${destination})`);
|
|
557
|
-
process.exit(1);
|
|
558
|
-
}
|
|
559
|
-
destination = serviceKeyFileName;
|
|
560
|
-
}
|
|
561
|
-
if (!destination) {
|
|
562
|
-
destination = path.basename(resolvedOutputPath, path.extname(resolvedOutputPath));
|
|
563
|
-
}
|
|
564
|
-
if (resolvedEnvPath) {
|
|
565
|
-
const envName = path.basename(resolvedEnvPath, path.extname(resolvedEnvPath));
|
|
566
|
-
if (destination && envName !== destination) {
|
|
567
|
-
console.error(`❌ Destination mismatch: env file (${envName}) vs output (${destination})`);
|
|
568
|
-
process.exit(1);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
const allowTokenEndpointWithServiceKey = options.protocol === 'saml2' && options.flow === 'bearer';
|
|
572
|
-
const serviceKeyConflicts = options.serviceKeyPath &&
|
|
573
|
-
(options.configPath ||
|
|
574
|
-
options.issuerUrl ||
|
|
575
|
-
options.authorizationEndpoint ||
|
|
576
|
-
(!allowTokenEndpointWithServiceKey && options.tokenEndpoint) ||
|
|
577
|
-
options.deviceAuthorizationEndpoint ||
|
|
578
|
-
options.clientId ||
|
|
579
|
-
options.clientSecret ||
|
|
580
|
-
options.uaaUrl);
|
|
581
|
-
if (serviceKeyConflicts) {
|
|
582
|
-
console.error('❌ Use either --service-key or explicit OIDC/SAML parameters (issuer/token/client/uaa).');
|
|
583
|
-
process.exit(1);
|
|
584
|
-
}
|
|
585
|
-
if (options.serviceKeyPath && options.authType !== 'xsuaa') {
|
|
586
|
-
console.error('❌ --service-key is supported only for XSUAA flows.');
|
|
587
|
-
process.exit(1);
|
|
588
|
-
}
|
|
589
|
-
let providerConfigFromFile = null;
|
|
590
|
-
if (options.configPath) {
|
|
591
|
-
const resolvedConfigPath = path.resolve(options.configPath);
|
|
592
|
-
if (!fs.existsSync(resolvedConfigPath)) {
|
|
593
|
-
console.error(`❌ Config file not found: ${resolvedConfigPath}`);
|
|
594
|
-
process.exit(1);
|
|
595
|
-
}
|
|
596
|
-
const raw = JSON.parse(fs.readFileSync(resolvedConfigPath, 'utf8'));
|
|
597
|
-
providerConfigFromFile = (0, mcpSsoConfig_1.normalizeProviderConfig)(raw);
|
|
598
|
-
if (!providerConfigFromFile) {
|
|
599
|
-
console.error(`❌ Config file does not contain provider config`);
|
|
600
|
-
process.exit(1);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
// Merge the file into `options` *before* anything downstream reads
|
|
604
|
-
// options.protocol/flow or builds a strategy from them — a run driven by
|
|
605
|
-
// --config alone must reach exactly the same validation and
|
|
606
|
-
// strategy-building code a --protocol/--flow run does, or a field the file
|
|
607
|
-
// carries (including a legacy one 2.0.0 removed) can reach the provider
|
|
608
|
-
// untouched with no error and no warning. CLI flags already parsed above
|
|
609
|
-
// are left alone; the file only fills what they didn't set. A no-op when
|
|
610
|
-
// --config wasn't given.
|
|
611
|
-
(0, mcpSsoConfig_1.applyFileConfig)(options, providerConfigFromFile);
|
|
612
|
-
// The user's own --token-endpoint, before a service key sets the plain
|
|
613
|
-
// /oauth/token, which a saml2-bearer grant must not use.
|
|
614
|
-
const explicitTokenEndpoint = options.tokenEndpoint;
|
|
615
|
-
if (options.serviceKeyPath) {
|
|
616
|
-
const resolvedServiceKeyPath = path.resolve(options.serviceKeyPath);
|
|
617
|
-
const serviceKeyDir = path.dirname(resolvedServiceKeyPath);
|
|
618
|
-
const serviceKeyStore = new auth_stores_1.XsuaaServiceKeyStore(serviceKeyDir);
|
|
619
|
-
const authConfig = await serviceKeyStore.getAuthorizationConfig(destination);
|
|
620
|
-
if (!authConfig) {
|
|
621
|
-
console.error(`❌ Authorization config not found for ${destination}. Service key must contain clientid, clientsecret, and url fields.`);
|
|
622
|
-
process.exit(1);
|
|
623
|
-
}
|
|
624
|
-
const uaaUrl = authConfig.uaaUrl;
|
|
625
|
-
if (!uaaUrl) {
|
|
626
|
-
console.error(`❌ Service key missing UAA URL for ${destination}.`);
|
|
627
|
-
process.exit(1);
|
|
628
|
-
}
|
|
629
|
-
options.uaaUrl = uaaUrl;
|
|
630
|
-
options.clientId = authConfig.uaaClientId;
|
|
631
|
-
options.clientSecret = authConfig.uaaClientSecret;
|
|
632
|
-
if (!options.issuerUrl) {
|
|
633
|
-
options.issuerUrl = uaaUrl;
|
|
634
|
-
}
|
|
635
|
-
if (!options.tokenEndpoint) {
|
|
636
|
-
options.tokenEndpoint = `${uaaUrl.replace(/\/+$/, '')}/oauth/token`;
|
|
637
|
-
}
|
|
638
|
-
if (!options.authorizationEndpoint) {
|
|
639
|
-
options.authorizationEndpoint = `${uaaUrl.replace(/\/+$/, '')}/oauth/authorize`;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
// What the SAML metadata states and the caller did not: the identity
|
|
643
|
-
// provider's trust from --idp-metadata, and for saml2-bearer the Audience,
|
|
644
|
-
// Recipient and token endpoint from XSUAA's own metadata (--saml-metadata,
|
|
645
|
-
// else <uaa.url>/saml/metadata from the service key).
|
|
646
|
-
try {
|
|
647
|
-
await (0, samlMetadata_1.applySamlMetadata)(options, explicitTokenEndpoint);
|
|
648
|
-
}
|
|
649
|
-
catch (error) {
|
|
650
|
-
console.error(`❌ SAML metadata: ${error instanceof Error ? error.message : String(error)}`);
|
|
651
|
-
process.exit(1);
|
|
652
|
-
}
|
|
653
|
-
if (options.protocol === 'oidc' && options.flow === 'password') {
|
|
654
|
-
if (options.uaaUrl && !options.tokenEndpoint) {
|
|
655
|
-
options.tokenEndpoint = `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (options.protocol === 'oidc' && options.flow) {
|
|
659
|
-
const valid = ['browser', 'device', 'password', 'token_exchange'];
|
|
660
|
-
if (!valid.includes(options.flow)) {
|
|
661
|
-
console.error(`❌ Invalid OIDC flow: ${options.flow}. Use one of: ${valid.join(', ')}`);
|
|
662
|
-
process.exit(1);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
if (options.protocol === 'saml2' && options.flow) {
|
|
666
|
-
const valid = ['bearer', 'pure'];
|
|
667
|
-
if (!valid.includes(options.flow)) {
|
|
668
|
-
console.error(`❌ Invalid SAML flow: ${options.flow}. Use one of: ${valid.join(', ')}`);
|
|
669
|
-
process.exit(1);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
if (options.protocol === 'oidc' && options.flow === 'password') {
|
|
673
|
-
if (!options.passcode && !options.password) {
|
|
674
|
-
options.passcode = await (0, mcpSsoConfig_1.readManualInput)('Paste passcode: ');
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
// Removed on any exit, error and signal included: it holds the secret.
|
|
678
|
-
const tempSessionDir = (0, workDir_1.createWorkDir)('mcp-sso');
|
|
679
|
-
if (resolvedEnvPath && fs.existsSync(resolvedEnvPath)) {
|
|
680
|
-
const tempEnvPath = path.join(tempSessionDir, `${destination}.env`);
|
|
681
|
-
fs.copyFileSync(resolvedEnvPath, tempEnvPath);
|
|
682
|
-
}
|
|
683
|
-
const placeholderServiceUrl = '<SERVICE_URL>';
|
|
684
|
-
const defaultServiceUrl = options.authType === 'xsuaa'
|
|
685
|
-
? options.serviceUrl || placeholderServiceUrl
|
|
686
|
-
: options.serviceUrl || '';
|
|
687
|
-
const sessionStore = options.authType === 'xsuaa'
|
|
688
|
-
? new auth_stores_1.XsuaaSessionStore(tempSessionDir, defaultServiceUrl)
|
|
689
|
-
: new auth_stores_1.AbapSessionStore(tempSessionDir, undefined, options.serviceUrl);
|
|
690
|
-
const existingConn = await sessionStore.getConnectionConfig(destination);
|
|
691
|
-
const existingAuth = await sessionStore.getAuthorizationConfig(destination);
|
|
692
|
-
const serviceUrl = options.serviceUrl || existingConn?.serviceUrl;
|
|
693
|
-
if (options.authType === 'abap' && !serviceUrl) {
|
|
694
|
-
console.error('❌ ABAP requires --service-url or existing env with SAP URL');
|
|
695
|
-
process.exit(1);
|
|
696
|
-
}
|
|
697
|
-
if (options.protocol === 'saml2' &&
|
|
698
|
-
options.flow === 'pure' &&
|
|
699
|
-
options.authType === 'xsuaa') {
|
|
700
|
-
console.error('❌ SAML pure flow is only supported for ABAP sessions (cookies)');
|
|
701
|
-
process.exit(1);
|
|
702
|
-
}
|
|
703
|
-
const isSamlPureAbap = options.authType === 'abap' &&
|
|
704
|
-
options.protocol === 'saml2' &&
|
|
705
|
-
options.flow === 'pure';
|
|
706
|
-
await sessionStore.setConnectionConfig(destination, {
|
|
707
|
-
serviceUrl: serviceUrl ||
|
|
708
|
-
(options.authType === 'xsuaa' ? defaultServiceUrl : undefined),
|
|
709
|
-
authorizationToken: isSamlPureAbap
|
|
710
|
-
? existingConn?.authorizationToken || '__init__'
|
|
711
|
-
: existingConn?.authorizationToken,
|
|
712
|
-
sessionCookies: existingConn?.sessionCookies,
|
|
713
|
-
});
|
|
714
|
-
let stripClientSecret = false;
|
|
715
|
-
const authUaaUrl = options.uaaUrl || options.tokenEndpoint || options.issuerUrl || undefined;
|
|
716
|
-
if (options.clientId && authUaaUrl) {
|
|
717
|
-
let clientSecret = options.clientSecret;
|
|
718
|
-
if (!clientSecret) {
|
|
719
|
-
clientSecret = '__public__';
|
|
720
|
-
stripClientSecret = true;
|
|
721
|
-
}
|
|
722
|
-
await sessionStore.setAuthorizationConfig(destination, {
|
|
723
|
-
uaaUrl: authUaaUrl,
|
|
724
|
-
uaaClientId: options.clientId,
|
|
725
|
-
uaaClientSecret: clientSecret,
|
|
726
|
-
refreshToken: existingAuth?.refreshToken,
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
const withLogger = (config) => config.config
|
|
730
|
-
? {
|
|
731
|
-
...config,
|
|
732
|
-
config: {
|
|
733
|
-
...config.config,
|
|
734
|
-
logger: config.config?.logger ?? logger,
|
|
735
|
-
},
|
|
736
|
-
}
|
|
737
|
-
: config;
|
|
738
|
-
const tokenProvider = auth_providers_1.SsoProviderFactory.create(withLogger((0, mcpSsoConfig_1.buildProviderConfig)(options, existingAuth, existingConn)));
|
|
739
|
-
const broker = new AuthBroker({
|
|
740
|
-
sessionStore,
|
|
741
|
-
provider: tokenProvider,
|
|
742
|
-
}, logger);
|
|
743
|
-
console.log(`🔐 Getting token for destination "${destination}"...`);
|
|
744
|
-
await broker.getToken(destination);
|
|
745
|
-
console.log(`✅ Token obtained successfully`);
|
|
746
|
-
const connConfig = await sessionStore.getConnectionConfig(destination);
|
|
747
|
-
const authConfig = await sessionStore.getAuthorizationConfig(destination);
|
|
748
|
-
if (!connConfig) {
|
|
749
|
-
throw new Error('Connection config not found after authentication');
|
|
750
|
-
}
|
|
751
|
-
const isSaml = !!connConfig.sessionCookies && !connConfig.authorizationToken;
|
|
752
|
-
const token = isSaml
|
|
753
|
-
? connConfig.sessionCookies
|
|
754
|
-
: connConfig.authorizationToken;
|
|
755
|
-
if (!token) {
|
|
756
|
-
throw new Error('Token provider did not return authorization token');
|
|
757
|
-
}
|
|
758
|
-
if (options.format === 'env') {
|
|
759
|
-
const tempEnvPath = path.join(tempSessionDir, `${destination}.env`);
|
|
760
|
-
if (!fs.existsSync(tempEnvPath)) {
|
|
761
|
-
throw new Error(`Temp env file not found: ${tempEnvPath}`);
|
|
762
|
-
}
|
|
763
|
-
const outputDir = path.dirname(resolvedOutputPath);
|
|
764
|
-
if (!fs.existsSync(outputDir)) {
|
|
765
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
766
|
-
}
|
|
767
|
-
let envContent = fs.readFileSync(tempEnvPath, 'utf8');
|
|
768
|
-
if (options.authType === 'xsuaa' && !serviceUrl) {
|
|
769
|
-
const lines = envContent
|
|
770
|
-
.split('\n')
|
|
771
|
-
.filter((line) => !line.startsWith('XSUAA_MCP_URL='));
|
|
772
|
-
envContent = `${lines.join('\n')}\n`;
|
|
773
|
-
}
|
|
774
|
-
if (stripClientSecret) {
|
|
775
|
-
const lines = envContent
|
|
776
|
-
.split('\n')
|
|
777
|
-
.filter((line) => !line.startsWith('XSUAA_UAA_CLIENT_SECRET=') &&
|
|
778
|
-
!line.startsWith('SAP_UAA_CLIENT_SECRET='));
|
|
779
|
-
envContent = `${lines.join('\n')}\n`;
|
|
780
|
-
}
|
|
781
|
-
fs.writeFileSync(resolvedOutputPath, envContent, 'utf8');
|
|
782
|
-
console.log(`✅ .env file created: ${resolvedOutputPath}`);
|
|
783
|
-
}
|
|
784
|
-
else {
|
|
785
|
-
const outputData = {
|
|
786
|
-
tokenType: isSaml ? 'saml' : 'jwt',
|
|
787
|
-
};
|
|
788
|
-
if (isSaml) {
|
|
789
|
-
outputData.sessionCookies = token;
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
outputData.accessToken = token;
|
|
793
|
-
}
|
|
794
|
-
if (authConfig?.refreshToken) {
|
|
795
|
-
outputData.refreshToken = authConfig.refreshToken;
|
|
796
|
-
}
|
|
797
|
-
if (serviceUrl) {
|
|
798
|
-
outputData.serviceUrl = serviceUrl;
|
|
799
|
-
}
|
|
800
|
-
if (authConfig?.uaaUrl) {
|
|
801
|
-
outputData.uaaUrl = authConfig.uaaUrl;
|
|
802
|
-
}
|
|
803
|
-
if (authConfig?.uaaClientId) {
|
|
804
|
-
outputData.uaaClientId = authConfig.uaaClientId;
|
|
805
|
-
}
|
|
806
|
-
if (authConfig?.uaaClientSecret) {
|
|
807
|
-
if (!stripClientSecret) {
|
|
808
|
-
outputData.uaaClientSecret = authConfig.uaaClientSecret;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
const outputDir = path.dirname(resolvedOutputPath);
|
|
812
|
-
if (!fs.existsSync(outputDir)) {
|
|
813
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
814
|
-
}
|
|
815
|
-
fs.writeFileSync(resolvedOutputPath, JSON.stringify(outputData, null, 2), 'utf8');
|
|
816
|
-
console.log(`✅ JSON file created: ${resolvedOutputPath}`);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
main().catch((error) => {
|
|
820
|
-
console.error(`❌ Error: ${error.message}`);
|
|
821
|
-
if (error.stack) {
|
|
822
|
-
console.error(error.stack);
|
|
823
|
-
}
|
|
824
|
-
process.exit(1);
|
|
825
|
-
});
|