@mcp-abap-adt/auth-broker 1.0.0 → 1.0.3

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 CHANGED
@@ -11,6 +11,27 @@ Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the co
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.0.3] - 2026-02-11
15
+
16
+ ### Fixed
17
+ - `mcp-sso`: prompt for passcode when not provided and tighten CLI typing.
18
+
19
+
20
+ ## [1.0.2] - 2026-02-11
21
+
22
+ ### Added
23
+ - New `mcp-sso` CLI for SSO flows (OIDC browser/device/password/token_exchange and SAML2 bearer/pure).
24
+
25
+ ### Changed
26
+ - Bumped `@mcp-abap-adt/auth-providers` to ^1.0.2.
27
+
28
+
29
+
30
+ ## [1.0.1] - 2026-02-10
31
+
32
+ ### Fixed
33
+ - Include `tsconfig.cli.json` in git so `npm run build` works in CI.
34
+
14
35
  ## [1.0.0] - 2026-02-10
15
36
 
16
37
  ### Added
package/README.md CHANGED
@@ -676,6 +676,55 @@ mcp-auth --service-key ./mcp.json --output ./mcp.env --type xsuaa --credential
676
676
  mcp-auth --env ./mcp.env --service-key ./mcp.json --output ./mcp.env --type xsuaa
677
677
  ```
678
678
 
679
+ ### CLI: mcp-sso
680
+
681
+ Get tokens via SSO providers (OIDC/SAML) and generate `.env`/JSON output:
682
+
683
+ ```bash
684
+ mcp-sso --protocol <oidc|saml2> --flow <flow> --output <path> [--type abap|xsuaa] [--format env|json] [--env <path>] [--config <path>]
685
+ ```
686
+
687
+ **Supported flows:**
688
+ - OIDC: `browser`, `device`, `password`, `token_exchange`
689
+ - SAML2: `bearer`, `pure`
690
+
691
+ **Examples:**
692
+ ```bash
693
+ # OIDC browser flow
694
+ mcp-sso --protocol oidc --flow browser --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
695
+
696
+ # OIDC browser flow (manual code / OOB)
697
+ mcp-sso --protocol oidc --flow browser --token-endpoint https://issuer/token --client-id my-client --code <auth_code> --redirect-uri urn:ietf:wg:oauth:2.0:oob --output ./sso.env --type xsuaa
698
+
699
+ # OIDC device flow
700
+ mcp-sso --protocol oidc --flow device --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
701
+
702
+ # OIDC password flow (CF passcode)
703
+ mcp-sso --protocol oidc --flow password --cf-api https://api.cf.eu10-004.hana.ondemand.com --client-id cf --passcode <code> --output ./sso.env --type xsuaa
704
+
705
+ # OIDC token exchange
706
+ mcp-sso --protocol oidc --flow token_exchange --issuer https://issuer --client-id my-client --subject-token <token> --output ./sso.env --type xsuaa
707
+
708
+ # SAML bearer flow (assertion -> token)
709
+ mcp-sso --protocol saml2 --flow bearer --idp-sso-url https://idp/sso --sp-entity-id my-sp --token-endpoint https://uaa.example/oauth/token --assertion <base64> --output ./sso.env --type xsuaa
710
+
711
+ # SAML pure flow (cookie)
712
+ mcp-sso --protocol saml2 --flow pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --assertion <base64> --cookie "SAP_SESSION=..." --output ./sso.env --type abap
713
+ ```
714
+
715
+ **Config file:**
716
+ You can pass a JSON file with provider config:
717
+
718
+ ```json
719
+ {
720
+ "protocol": "oidc",
721
+ "flow": "device",
722
+ "issuerUrl": "https://issuer",
723
+ "clientId": "my-client",
724
+ "scopes": ["openid", "profile"]
725
+ }
726
+ ```
727
+
679
728
  ### Utility Script
680
729
 
681
730
  Generate `.env` files from service keys:
@@ -53,14 +53,13 @@ var __importStar = (this && this.__importStar) || (function () {
53
53
  };
54
54
  })();
55
55
  Object.defineProperty(exports, "__esModule", { value: true });
56
- const path = __importStar(require("path"));
57
56
  const fs = __importStar(require("fs"));
57
+ const path = __importStar(require("path"));
58
58
  // Use require for CommonJS dist files with absolute path
59
59
  const distPath = path.resolve(__dirname, '..', 'index.js');
60
60
  const { AuthBroker } = require(distPath);
61
- const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
62
61
  const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
63
- const auth_stores_2 = require("@mcp-abap-adt/auth-stores");
62
+ const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
64
63
  function getVersion() {
65
64
  try {
66
65
  const candidates = [
@@ -199,7 +198,15 @@ function parseArgs() {
199
198
  }
200
199
  else if (args[i] === '--browser' && i + 1 < args.length) {
201
200
  browser = args[i + 1];
202
- if (!['none', 'chrome', 'edge', 'firefox', 'system', 'headless', 'auto'].includes(browser)) {
201
+ if (![
202
+ 'none',
203
+ 'chrome',
204
+ 'edge',
205
+ 'firefox',
206
+ 'system',
207
+ 'headless',
208
+ 'auto',
209
+ ].includes(browser)) {
203
210
  console.error(`Invalid browser: ${browser}. Must be one of: none, chrome, edge, firefox, system, headless, auto`);
204
211
  process.exit(1);
205
212
  }
@@ -271,20 +278,20 @@ function writeEnvFile(outputPath, authType, token, refreshToken, serviceUrl, uaa
271
278
  if (authType === 'abap') {
272
279
  // ABAP format
273
280
  if (serviceUrl) {
274
- lines.push(`${auth_stores_2.ABAP_CONNECTION_VARS.SERVICE_URL}=${serviceUrl}`);
281
+ lines.push(`${auth_stores_1.ABAP_CONNECTION_VARS.SERVICE_URL}=${serviceUrl}`);
275
282
  }
276
- lines.push(`${auth_stores_2.ABAP_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token}`);
283
+ lines.push(`${auth_stores_1.ABAP_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token}`);
277
284
  if (refreshToken) {
278
- lines.push(`${auth_stores_2.ABAP_AUTHORIZATION_VARS.REFRESH_TOKEN}=${refreshToken}`);
285
+ lines.push(`${auth_stores_1.ABAP_AUTHORIZATION_VARS.REFRESH_TOKEN}=${refreshToken}`);
279
286
  }
280
287
  if (uaaUrl) {
281
- lines.push(`${auth_stores_2.ABAP_AUTHORIZATION_VARS.UAA_URL}=${uaaUrl}`);
288
+ lines.push(`${auth_stores_1.ABAP_AUTHORIZATION_VARS.UAA_URL}=${uaaUrl}`);
282
289
  }
283
290
  if (uaaClientId) {
284
- lines.push(`${auth_stores_2.ABAP_AUTHORIZATION_VARS.UAA_CLIENT_ID}=${uaaClientId}`);
291
+ lines.push(`${auth_stores_1.ABAP_AUTHORIZATION_VARS.UAA_CLIENT_ID}=${uaaClientId}`);
285
292
  }
286
293
  if (uaaClientSecret) {
287
- lines.push(`${auth_stores_2.ABAP_AUTHORIZATION_VARS.UAA_CLIENT_SECRET}=${uaaClientSecret}`);
294
+ lines.push(`${auth_stores_1.ABAP_AUTHORIZATION_VARS.UAA_CLIENT_SECRET}=${uaaClientSecret}`);
288
295
  }
289
296
  }
290
297
  else {
@@ -292,18 +299,18 @@ function writeEnvFile(outputPath, authType, token, refreshToken, serviceUrl, uaa
292
299
  if (serviceUrl) {
293
300
  lines.push(`XSUAA_MCP_URL=${serviceUrl}`);
294
301
  }
295
- lines.push(`${auth_stores_2.XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token}`);
302
+ lines.push(`${auth_stores_1.XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token}`);
296
303
  if (refreshToken) {
297
- lines.push(`${auth_stores_2.XSUAA_AUTHORIZATION_VARS.REFRESH_TOKEN}=${refreshToken}`);
304
+ lines.push(`${auth_stores_1.XSUAA_AUTHORIZATION_VARS.REFRESH_TOKEN}=${refreshToken}`);
298
305
  }
299
306
  if (uaaUrl) {
300
- lines.push(`${auth_stores_2.XSUAA_AUTHORIZATION_VARS.UAA_URL}=${uaaUrl}`);
307
+ lines.push(`${auth_stores_1.XSUAA_AUTHORIZATION_VARS.UAA_URL}=${uaaUrl}`);
301
308
  }
302
309
  if (uaaClientId) {
303
- lines.push(`${auth_stores_2.XSUAA_AUTHORIZATION_VARS.UAA_CLIENT_ID}=${uaaClientId}`);
310
+ lines.push(`${auth_stores_1.XSUAA_AUTHORIZATION_VARS.UAA_CLIENT_ID}=${uaaClientId}`);
304
311
  }
305
312
  if (uaaClientSecret) {
306
- lines.push(`${auth_stores_2.XSUAA_AUTHORIZATION_VARS.UAA_CLIENT_SECRET}=${uaaClientSecret}`);
313
+ lines.push(`${auth_stores_1.XSUAA_AUTHORIZATION_VARS.UAA_CLIENT_SECRET}=${uaaClientSecret}`);
307
314
  }
308
315
  }
309
316
  // Ensure output directory exists
@@ -481,7 +488,8 @@ async function main() {
481
488
  let serviceKeyAuthConfig = null;
482
489
  if (serviceKeyStore?.getAuthorizationConfig) {
483
490
  try {
484
- serviceKeyAuthConfig = await serviceKeyStore.getAuthorizationConfig(destination);
491
+ serviceKeyAuthConfig =
492
+ await serviceKeyStore.getAuthorizationConfig(destination);
485
493
  }
486
494
  catch (e) {
487
495
  // Service key parsing might fail - try fallback parsing from raw JSON
@@ -554,20 +562,20 @@ async function main() {
554
562
  console.log(`📋 .env file contains:`);
555
563
  if (options.authType === 'abap') {
556
564
  if (finalServiceUrl) {
557
- console.log(` - ${auth_stores_2.ABAP_CONNECTION_VARS.SERVICE_URL}=${finalServiceUrl}`);
565
+ console.log(` - ${auth_stores_1.ABAP_CONNECTION_VARS.SERVICE_URL}=${finalServiceUrl}`);
558
566
  }
559
- console.log(` - ${auth_stores_2.ABAP_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`);
567
+ console.log(` - ${auth_stores_1.ABAP_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`);
560
568
  if (authConfig?.refreshToken) {
561
- console.log(` - ${auth_stores_2.ABAP_AUTHORIZATION_VARS.REFRESH_TOKEN}=${authConfig.refreshToken.substring(0, 50)}...`);
569
+ console.log(` - ${auth_stores_1.ABAP_AUTHORIZATION_VARS.REFRESH_TOKEN}=${authConfig.refreshToken.substring(0, 50)}...`);
562
570
  }
563
571
  }
564
572
  else {
565
573
  if (finalServiceUrl) {
566
574
  console.log(` - XSUAA_MCP_URL=${finalServiceUrl}`);
567
575
  }
568
- console.log(` - ${auth_stores_2.XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`);
576
+ console.log(` - ${auth_stores_1.XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`);
569
577
  if (authConfig?.refreshToken) {
570
- console.log(` - ${auth_stores_2.XSUAA_AUTHORIZATION_VARS.REFRESH_TOKEN}=${authConfig.refreshToken.substring(0, 50)}...`);
578
+ console.log(` - ${auth_stores_1.XSUAA_AUTHORIZATION_VARS.REFRESH_TOKEN}=${authConfig.refreshToken.substring(0, 50)}...`);
571
579
  }
572
580
  }
573
581
  }
@@ -0,0 +1,817 @@
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 --protocol <oidc|saml2> --flow <flow> --output <path> [options]
8
+ *
9
+ * Examples:
10
+ * # OIDC browser flow (authorization code with local callback)
11
+ * mcp-sso --protocol oidc --flow browser --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
12
+ *
13
+ * # OIDC device flow
14
+ * mcp-sso --protocol oidc --flow device --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
15
+ *
16
+ * # OIDC password flow (passcode)
17
+ * mcp-sso --protocol oidc --flow password --token-endpoint https://uaa.example/oauth/token --client-id cf --passcode <code> --output ./sso.env --type xsuaa
18
+ *
19
+ * # OIDC token exchange
20
+ * mcp-sso --protocol oidc --flow token_exchange --issuer https://issuer --client-id my-client --subject-token <token> --output ./sso.env --type xsuaa
21
+ *
22
+ * # SAML bearer flow
23
+ * mcp-sso --protocol saml2 --flow bearer --idp-sso-url https://idp/sso --sp-entity-id my-sp --token-endpoint https://uaa.example/oauth/token --assertion <base64> --output ./sso.env --type xsuaa
24
+ *
25
+ * # SAML pure flow (cookie)
26
+ * mcp-sso --protocol saml2 --flow pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --assertion <base64> --cookie "SAP_SESSION=..." --output ./sso.env --type abap
27
+ */
28
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ var desc = Object.getOwnPropertyDescriptor(m, k);
31
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
32
+ desc = { enumerable: true, get: function() { return m[k]; } };
33
+ }
34
+ Object.defineProperty(o, k2, desc);
35
+ }) : (function(o, m, k, k2) {
36
+ if (k2 === undefined) k2 = k;
37
+ o[k2] = m[k];
38
+ }));
39
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
40
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
41
+ }) : function(o, v) {
42
+ o["default"] = v;
43
+ });
44
+ var __importStar = (this && this.__importStar) || (function () {
45
+ var ownKeys = function(o) {
46
+ ownKeys = Object.getOwnPropertyNames || function (o) {
47
+ var ar = [];
48
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
49
+ return ar;
50
+ };
51
+ return ownKeys(o);
52
+ };
53
+ return function (mod) {
54
+ if (mod && mod.__esModule) return mod;
55
+ var result = {};
56
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
57
+ __setModuleDefault(result, mod);
58
+ return result;
59
+ };
60
+ })();
61
+ var __importDefault = (this && this.__importDefault) || function (mod) {
62
+ return (mod && mod.__esModule) ? mod : { "default": mod };
63
+ };
64
+ Object.defineProperty(exports, "__esModule", { value: true });
65
+ const node_readline_1 = require("node:readline");
66
+ const axios_1 = __importDefault(require("axios"));
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
+ function getVersion() {
75
+ try {
76
+ const candidates = [
77
+ path.join(__dirname, 'package.json'),
78
+ path.join(__dirname, '..', 'package.json'),
79
+ path.join(__dirname, '..', '..', 'package.json'),
80
+ ];
81
+ for (const candidate of candidates) {
82
+ if (fs.existsSync(candidate)) {
83
+ const packageJson = JSON.parse(fs.readFileSync(candidate, 'utf8'));
84
+ return packageJson.version || 'unknown';
85
+ }
86
+ }
87
+ const localRequire = require('module').createRequire(__filename);
88
+ const resolved = localRequire.resolve('@mcp-abap-adt/auth-broker/package.json');
89
+ const packageJson = JSON.parse(fs.readFileSync(resolved, 'utf8'));
90
+ return packageJson.version || 'unknown';
91
+ }
92
+ catch {
93
+ return 'unknown';
94
+ }
95
+ }
96
+ function showHelp() {
97
+ console.log('MCP SSO - Get tokens via SSO providers and generate .env files');
98
+ console.log('');
99
+ console.log('Usage:');
100
+ console.log(' mcp-sso --protocol <oidc|saml2> --flow <flow> --output <path> [options]');
101
+ console.log('');
102
+ console.log('Required Options:');
103
+ console.log(' --output <path> Output file path');
104
+ console.log(' --protocol <oidc|saml2> Protocol');
105
+ console.log(' --flow <flow> Flow for protocol');
106
+ console.log('');
107
+ console.log('Common Options:');
108
+ console.log(' --type <abap|xsuaa> Output type (default: abap)');
109
+ console.log(' --format <env|json> Output format (default: env)');
110
+ console.log(' --env <path> Optional existing env file (used for refresh)');
111
+ console.log(' --destination <name> Destination name (default: output file base)');
112
+ console.log(' --service-url <url> Service URL (ABAP: SAP URL, XSUAA: MCP URL)');
113
+ console.log(' --config <path> JSON config file (SSO provider config)');
114
+ console.log(' --browser <browser> Browser: auto|none|system|chrome|edge|firefox');
115
+ console.log(' --redirect-port <port> Redirect port for browser flows (default: 3001)');
116
+ console.log(' --redirect-uri <uri> Custom redirect URI (OOB/manual code flows)');
117
+ console.log('');
118
+ console.log('OIDC Options:');
119
+ console.log(' --issuer <url> OIDC issuer/discovery URL');
120
+ console.log(' --authorization-endpoint <url> Authorization endpoint');
121
+ console.log(' --token-endpoint <url> Token endpoint');
122
+ console.log(' --device-authorization-endpoint <url> Device auth endpoint');
123
+ console.log(' --client-id <id> OAuth client id');
124
+ console.log(' --client-secret <secret> OAuth client secret');
125
+ console.log(' --scopes <csv> Scopes list (comma or space-separated)');
126
+ console.log(' --scope <value> Scope for token exchange');
127
+ console.log(' --code <value> Authorization code (manual)');
128
+ console.log(' --username <value> Username for password flow');
129
+ console.log(' --password <value> Password for password flow');
130
+ console.log(' --passcode <value> Passcode (alias for password, username=passcode)');
131
+ console.log(' --subject-token <token> Subject token for token exchange');
132
+ console.log(' --subject-token-type <type> Subject token type (default: access_token)');
133
+ console.log(' --audience <value> Audience for token exchange');
134
+ console.log(' --actor-token <token> Actor token for token exchange');
135
+ console.log(' --actor-token-type <type> Actor token type for token exchange');
136
+ console.log(' --uaa-url <url> UAA base URL (used to build token endpoint)');
137
+ console.log(' --cf-api <url> CF API URL (resolve login/uaa endpoints)');
138
+ console.log('');
139
+ console.log('SAML Options:');
140
+ console.log(' --idp-sso-url <url> IdP SSO URL');
141
+ console.log(' --sp-entity-id <id> SP Entity ID');
142
+ console.log(' --acs-url <url> ACS URL (default: http://localhost:<port>/callback)');
143
+ console.log(' --relay-state <value> RelayState (optional)');
144
+ console.log(' --assertion-flow <flow> browser|manual|assertion (default: browser)');
145
+ console.log(' --assertion <base64> SAMLResponse (base64)');
146
+ console.log(' --cookie <value> Session cookies (for pure SAML)');
147
+ console.log(' --token-endpoint <url> Token endpoint for SAML bearer exchange');
148
+ console.log('');
149
+ console.log(' --version, -v Show version number');
150
+ console.log(' --help, -h Show this help message');
151
+ }
152
+ function parseScopes(value) {
153
+ if (!value)
154
+ return undefined;
155
+ const parts = value
156
+ .split(/[,\s]+/)
157
+ .map((p) => p.trim())
158
+ .filter(Boolean);
159
+ return parts.length > 0 ? parts : undefined;
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
+ function parseArgs() {
174
+ const args = process.argv.slice(2);
175
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
176
+ showHelp();
177
+ process.exit(0);
178
+ }
179
+ if (args.includes('--version') || args.includes('-v')) {
180
+ console.log(getVersion());
181
+ process.exit(0);
182
+ }
183
+ let outputFile;
184
+ let envFilePath;
185
+ let destination;
186
+ let authType = 'abap';
187
+ let format = 'env';
188
+ let protocol;
189
+ let flow;
190
+ let configPath;
191
+ let serviceUrl;
192
+ let browser;
193
+ let redirectPort;
194
+ let redirectUri;
195
+ let issuerUrl;
196
+ let authorizationEndpoint;
197
+ let tokenEndpoint;
198
+ let deviceAuthorizationEndpoint;
199
+ let clientId;
200
+ let clientSecret;
201
+ let scopes;
202
+ let scope;
203
+ let code;
204
+ let username;
205
+ let password;
206
+ let passcode;
207
+ let subjectToken;
208
+ let subjectTokenType;
209
+ let audience;
210
+ let actorToken;
211
+ let actorTokenType;
212
+ let idpSsoUrl;
213
+ let spEntityId;
214
+ let acsUrl;
215
+ let relayState;
216
+ let assertionFlow;
217
+ let assertion;
218
+ let cookie;
219
+ let uaaUrl;
220
+ let cfApi;
221
+ for (let i = 0; i < args.length; i++) {
222
+ const arg = args[i];
223
+ const next = i + 1 < args.length ? args[i + 1] : undefined;
224
+ switch (arg) {
225
+ case '--output':
226
+ outputFile = next;
227
+ i++;
228
+ break;
229
+ case '--env':
230
+ envFilePath = next;
231
+ i++;
232
+ break;
233
+ case '--destination':
234
+ destination = next;
235
+ i++;
236
+ break;
237
+ case '--type':
238
+ if (next === 'abap' || next === 'xsuaa') {
239
+ authType = next;
240
+ }
241
+ else {
242
+ console.error(`Invalid type: ${next}. Use abap or xsuaa.`);
243
+ process.exit(1);
244
+ }
245
+ i++;
246
+ break;
247
+ case '--format':
248
+ if (next === 'env' || next === 'json') {
249
+ format = next;
250
+ }
251
+ else {
252
+ console.error(`Invalid format: ${next}. Use env or json.`);
253
+ process.exit(1);
254
+ }
255
+ i++;
256
+ break;
257
+ case '--protocol':
258
+ if (next === 'oidc' || next === 'saml2') {
259
+ protocol = next;
260
+ }
261
+ else {
262
+ console.error(`Invalid protocol: ${next}. Use oidc or saml2.`);
263
+ process.exit(1);
264
+ }
265
+ i++;
266
+ break;
267
+ case '--flow':
268
+ flow = next;
269
+ i++;
270
+ break;
271
+ case '--config':
272
+ configPath = next;
273
+ i++;
274
+ break;
275
+ case '--service-url':
276
+ serviceUrl = next;
277
+ i++;
278
+ break;
279
+ case '--browser':
280
+ browser = next;
281
+ i++;
282
+ break;
283
+ case '--redirect-port':
284
+ if (!next)
285
+ break;
286
+ redirectPort = parseInt(next, 10);
287
+ if (Number.isNaN(redirectPort) ||
288
+ redirectPort < 1 ||
289
+ redirectPort > 65535) {
290
+ console.error(`Invalid redirect port: ${next}`);
291
+ process.exit(1);
292
+ }
293
+ i++;
294
+ break;
295
+ case '--redirect-uri':
296
+ redirectUri = next;
297
+ i++;
298
+ break;
299
+ case '--issuer':
300
+ issuerUrl = next;
301
+ i++;
302
+ break;
303
+ case '--authorization-endpoint':
304
+ authorizationEndpoint = next;
305
+ i++;
306
+ break;
307
+ case '--token-endpoint':
308
+ tokenEndpoint = next;
309
+ i++;
310
+ break;
311
+ case '--device-authorization-endpoint':
312
+ deviceAuthorizationEndpoint = next;
313
+ i++;
314
+ break;
315
+ case '--client-id':
316
+ clientId = next;
317
+ i++;
318
+ break;
319
+ case '--client-secret':
320
+ clientSecret = next;
321
+ i++;
322
+ break;
323
+ case '--scopes':
324
+ scopes = parseScopes(next);
325
+ i++;
326
+ break;
327
+ case '--scope':
328
+ scope = next;
329
+ i++;
330
+ break;
331
+ case '--code':
332
+ code = next;
333
+ i++;
334
+ break;
335
+ case '--username':
336
+ username = next;
337
+ i++;
338
+ break;
339
+ case '--password':
340
+ password = next;
341
+ i++;
342
+ break;
343
+ case '--passcode':
344
+ passcode = next;
345
+ i++;
346
+ break;
347
+ case '--subject-token':
348
+ subjectToken = next;
349
+ i++;
350
+ break;
351
+ case '--subject-token-type':
352
+ subjectTokenType = next;
353
+ i++;
354
+ break;
355
+ case '--audience':
356
+ audience = next;
357
+ i++;
358
+ break;
359
+ case '--actor-token':
360
+ actorToken = next;
361
+ i++;
362
+ break;
363
+ case '--actor-token-type':
364
+ actorTokenType = next;
365
+ i++;
366
+ break;
367
+ case '--idp-sso-url':
368
+ idpSsoUrl = next;
369
+ i++;
370
+ break;
371
+ case '--sp-entity-id':
372
+ spEntityId = next;
373
+ i++;
374
+ break;
375
+ case '--acs-url':
376
+ acsUrl = next;
377
+ i++;
378
+ break;
379
+ case '--relay-state':
380
+ relayState = next;
381
+ i++;
382
+ break;
383
+ case '--assertion-flow':
384
+ if (next === 'browser' || next === 'manual' || next === 'assertion') {
385
+ assertionFlow = next;
386
+ }
387
+ else {
388
+ console.error(`Invalid assertion flow: ${next}. Use browser, manual, or assertion.`);
389
+ process.exit(1);
390
+ }
391
+ i++;
392
+ break;
393
+ case '--assertion':
394
+ assertion = next;
395
+ i++;
396
+ break;
397
+ case '--cookie':
398
+ cookie = next;
399
+ i++;
400
+ break;
401
+ case '--uaa-url':
402
+ uaaUrl = next;
403
+ i++;
404
+ break;
405
+ case '--cf-api':
406
+ cfApi = next;
407
+ i++;
408
+ break;
409
+ default:
410
+ break;
411
+ }
412
+ }
413
+ return {
414
+ outputFile,
415
+ envFilePath,
416
+ destination,
417
+ authType,
418
+ format,
419
+ protocol,
420
+ flow: flow,
421
+ configPath,
422
+ serviceUrl,
423
+ browser,
424
+ redirectPort,
425
+ redirectUri,
426
+ issuerUrl,
427
+ authorizationEndpoint,
428
+ tokenEndpoint,
429
+ deviceAuthorizationEndpoint,
430
+ clientId,
431
+ clientSecret,
432
+ scopes,
433
+ scope,
434
+ code,
435
+ username,
436
+ password,
437
+ passcode,
438
+ subjectToken,
439
+ subjectTokenType,
440
+ audience,
441
+ actorToken,
442
+ actorTokenType,
443
+ idpSsoUrl,
444
+ spEntityId,
445
+ acsUrl,
446
+ relayState,
447
+ assertionFlow,
448
+ assertion,
449
+ cookie,
450
+ uaaUrl,
451
+ cfApi,
452
+ };
453
+ }
454
+ function normalizeProviderConfig(raw) {
455
+ if (!raw || typeof raw !== 'object') {
456
+ return null;
457
+ }
458
+ if (raw.provider) {
459
+ return raw.provider;
460
+ }
461
+ if (raw.protocol && raw.flow) {
462
+ const { protocol, flow, config, ...rest } = raw;
463
+ return {
464
+ protocol,
465
+ flow,
466
+ config: config ?? rest,
467
+ };
468
+ }
469
+ return null;
470
+ }
471
+ function mergeConfig(target, source) {
472
+ const result = { ...target };
473
+ for (const [key, value] of Object.entries(source)) {
474
+ if (value !== undefined) {
475
+ result[key] = value;
476
+ }
477
+ }
478
+ return result;
479
+ }
480
+ function buildOidcConfig(options) {
481
+ const scopeList = options.scopes;
482
+ const tokenEndpoint = options.tokenEndpoint ||
483
+ (options.uaaUrl
484
+ ? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
485
+ : undefined);
486
+ const passcode = options.passcode;
487
+ const username = options.username || (passcode ? 'passcode' : undefined);
488
+ const password = options.password || passcode;
489
+ return {
490
+ issuerUrl: options.issuerUrl,
491
+ authorizationEndpoint: options.authorizationEndpoint,
492
+ tokenEndpoint,
493
+ deviceAuthorizationEndpoint: options.deviceAuthorizationEndpoint,
494
+ clientId: options.clientId,
495
+ clientSecret: options.clientSecret,
496
+ scopes: scopeList,
497
+ scope: options.scope,
498
+ authorizationCode: options.code,
499
+ username,
500
+ password,
501
+ subjectToken: options.subjectToken,
502
+ subjectTokenType: options.subjectTokenType ||
503
+ 'urn:ietf:params:oauth:token-type:access_token',
504
+ audience: options.audience,
505
+ actorToken: options.actorToken,
506
+ actorTokenType: options.actorTokenType,
507
+ browser: options.browser,
508
+ redirectPort: options.redirectPort,
509
+ redirectUri: options.redirectUri,
510
+ };
511
+ }
512
+ function buildSamlConfig(options) {
513
+ const assertionFlow = options.assertionFlow || (options.assertion ? 'assertion' : 'browser');
514
+ const tokenUrl = options.tokenEndpoint ||
515
+ (options.uaaUrl
516
+ ? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
517
+ : undefined);
518
+ const assertionProvider = options.assertion
519
+ ? async () => options.assertion
520
+ : assertionFlow === 'assertion'
521
+ ? async () => readManualInput('Paste SAMLResponse: ')
522
+ : undefined;
523
+ const cookieProvider = async () => {
524
+ if (options.cookie) {
525
+ return options.cookie;
526
+ }
527
+ return readManualInput('Paste session cookies: ');
528
+ };
529
+ return {
530
+ idpSsoUrl: options.idpSsoUrl,
531
+ spEntityId: options.spEntityId,
532
+ acsUrl: options.acsUrl,
533
+ relayState: options.relayState,
534
+ assertionFlow,
535
+ assertionProvider,
536
+ tokenUrl,
537
+ uaaUrl: options.uaaUrl,
538
+ clientId: options.clientId,
539
+ clientSecret: options.clientSecret,
540
+ browser: options.browser,
541
+ redirectPort: options.redirectPort,
542
+ cookieProvider,
543
+ };
544
+ }
545
+ function buildProviderConfig(options, existingAuth, existingConn, fileConfig) {
546
+ let configFromCli = null;
547
+ if (options.protocol && options.flow) {
548
+ if (options.protocol === 'oidc') {
549
+ switch (options.flow) {
550
+ case 'browser':
551
+ configFromCli = {
552
+ protocol: 'oidc',
553
+ flow: 'browser',
554
+ config: buildOidcConfig(options),
555
+ };
556
+ break;
557
+ case 'device':
558
+ configFromCli = {
559
+ protocol: 'oidc',
560
+ flow: 'device',
561
+ config: buildOidcConfig(options),
562
+ };
563
+ break;
564
+ case 'password':
565
+ configFromCli = {
566
+ protocol: 'oidc',
567
+ flow: 'password',
568
+ config: buildOidcConfig(options),
569
+ };
570
+ break;
571
+ case 'token_exchange':
572
+ configFromCli = {
573
+ protocol: 'oidc',
574
+ flow: 'token_exchange',
575
+ config: buildOidcConfig(options),
576
+ };
577
+ break;
578
+ default:
579
+ throw new Error(`Unsupported OIDC flow: ${options.flow}`);
580
+ }
581
+ }
582
+ else if (options.protocol === 'saml2') {
583
+ switch (options.flow) {
584
+ case 'bearer':
585
+ configFromCli = {
586
+ protocol: 'saml2',
587
+ flow: 'bearer',
588
+ config: buildSamlConfig(options),
589
+ };
590
+ break;
591
+ case 'pure':
592
+ configFromCli = {
593
+ protocol: 'saml2',
594
+ flow: 'pure',
595
+ config: buildSamlConfig(options),
596
+ };
597
+ break;
598
+ default:
599
+ throw new Error(`Unsupported SAML flow: ${options.flow}`);
600
+ }
601
+ }
602
+ }
603
+ const base = fileConfig ?? configFromCli;
604
+ if (!base) {
605
+ throw new Error('Provider config is missing. Use --config or --protocol/--flow options.');
606
+ }
607
+ let result = base;
608
+ if (options.protocol) {
609
+ result = { ...result, protocol: options.protocol };
610
+ }
611
+ if (options.flow) {
612
+ result = { ...result, flow: options.flow };
613
+ }
614
+ if (configFromCli) {
615
+ result = {
616
+ ...result,
617
+ config: mergeConfig(result.config || {}, configFromCli.config || {}),
618
+ };
619
+ }
620
+ const accessToken = existingConn?.authorizationToken;
621
+ const refreshToken = existingAuth?.refreshToken;
622
+ if ((result.protocol === 'oidc' ||
623
+ (result.protocol === 'saml2' && result.flow === 'bearer')) &&
624
+ (accessToken || refreshToken)) {
625
+ result = {
626
+ ...result,
627
+ config: mergeConfig(result.config || {}, {
628
+ accessToken,
629
+ refreshToken,
630
+ }),
631
+ };
632
+ }
633
+ return result;
634
+ }
635
+ async function resolveCfEndpoints(apiUrl) {
636
+ const response = await axios_1.default.get(apiUrl);
637
+ const data = response.data;
638
+ const loginUrl = data?.links?.login?.href;
639
+ const uaaUrl = data?.links?.uaa?.href;
640
+ return { loginUrl, uaaUrl };
641
+ }
642
+ async function main() {
643
+ const options = parseArgs();
644
+ if (!options) {
645
+ return;
646
+ }
647
+ if (!options.outputFile) {
648
+ console.error('❌ Missing required --output');
649
+ process.exit(1);
650
+ }
651
+ const resolvedOutputPath = path.resolve(options.outputFile);
652
+ const resolvedEnvPath = options.envFilePath
653
+ ? path.resolve(options.envFilePath)
654
+ : undefined;
655
+ let destination = options.destination;
656
+ if (!destination) {
657
+ destination = path.basename(resolvedOutputPath, path.extname(resolvedOutputPath));
658
+ }
659
+ if (resolvedEnvPath) {
660
+ const envName = path.basename(resolvedEnvPath, path.extname(resolvedEnvPath));
661
+ if (destination && envName !== destination) {
662
+ console.error(`❌ Destination mismatch: env file (${envName}) vs output (${destination})`);
663
+ process.exit(1);
664
+ }
665
+ }
666
+ let providerConfigFromFile = null;
667
+ if (options.configPath) {
668
+ const resolvedConfigPath = path.resolve(options.configPath);
669
+ if (!fs.existsSync(resolvedConfigPath)) {
670
+ console.error(`❌ Config file not found: ${resolvedConfigPath}`);
671
+ process.exit(1);
672
+ }
673
+ const raw = JSON.parse(fs.readFileSync(resolvedConfigPath, 'utf8'));
674
+ providerConfigFromFile = normalizeProviderConfig(raw);
675
+ if (!providerConfigFromFile) {
676
+ console.error(`❌ Config file does not contain provider config`);
677
+ process.exit(1);
678
+ }
679
+ }
680
+ if (options.cfApi) {
681
+ const cfInfo = await resolveCfEndpoints(options.cfApi);
682
+ if (cfInfo.uaaUrl && !options.uaaUrl) {
683
+ options.uaaUrl = cfInfo.uaaUrl;
684
+ }
685
+ if (cfInfo.loginUrl) {
686
+ console.log(`🔗 CF passcode URL: ${cfInfo.loginUrl.replace(/\/+$/, '')}/passcode`);
687
+ }
688
+ }
689
+ if (options.protocol === 'oidc' && options.flow) {
690
+ const valid = ['browser', 'device', 'password', 'token_exchange'];
691
+ if (!valid.includes(options.flow)) {
692
+ console.error(`❌ Invalid OIDC flow: ${options.flow}. Use one of: ${valid.join(', ')}`);
693
+ process.exit(1);
694
+ }
695
+ }
696
+ if (options.protocol === 'saml2' && options.flow) {
697
+ const valid = ['bearer', 'pure'];
698
+ if (!valid.includes(options.flow)) {
699
+ console.error(`❌ Invalid SAML flow: ${options.flow}. Use one of: ${valid.join(', ')}`);
700
+ process.exit(1);
701
+ }
702
+ }
703
+ if (options.protocol === 'oidc' && options.flow === 'password') {
704
+ if (!options.passcode && !options.password) {
705
+ options.passcode = await readManualInput('Paste passcode: ');
706
+ }
707
+ }
708
+ const tempSessionDir = path.join(path.dirname(resolvedOutputPath), '.tmp');
709
+ if (!fs.existsSync(tempSessionDir)) {
710
+ fs.mkdirSync(tempSessionDir, { recursive: true });
711
+ }
712
+ if (resolvedEnvPath && fs.existsSync(resolvedEnvPath)) {
713
+ const tempEnvPath = path.join(tempSessionDir, `${destination}.env`);
714
+ fs.copyFileSync(resolvedEnvPath, tempEnvPath);
715
+ }
716
+ const defaultServiceUrl = options.serviceUrl || '';
717
+ const sessionStore = options.authType === 'xsuaa'
718
+ ? new auth_stores_1.XsuaaSessionStore(tempSessionDir, defaultServiceUrl)
719
+ : new auth_stores_1.AbapSessionStore(tempSessionDir);
720
+ const existingConn = await sessionStore.getConnectionConfig(destination);
721
+ const existingAuth = await sessionStore.getAuthorizationConfig(destination);
722
+ const serviceUrl = options.serviceUrl || existingConn?.serviceUrl;
723
+ if (options.authType === 'abap' && !serviceUrl) {
724
+ console.error('❌ ABAP requires --service-url or existing env with SAP URL');
725
+ process.exit(1);
726
+ }
727
+ if (options.protocol === 'saml2' &&
728
+ options.flow === 'pure' &&
729
+ options.authType === 'xsuaa') {
730
+ console.error('❌ SAML pure flow is only supported for ABAP sessions (cookies)');
731
+ process.exit(1);
732
+ }
733
+ await sessionStore.setConnectionConfig(destination, {
734
+ serviceUrl: serviceUrl ||
735
+ (options.authType === 'xsuaa' ? defaultServiceUrl : undefined),
736
+ authorizationToken: existingConn?.authorizationToken,
737
+ sessionCookies: existingConn?.sessionCookies,
738
+ });
739
+ const providerConfig = buildProviderConfig(options, existingAuth, existingConn, providerConfigFromFile);
740
+ const tokenProvider = auth_providers_1.SsoProviderFactory.create(providerConfig);
741
+ const broker = new AuthBroker({
742
+ sessionStore,
743
+ tokenProvider,
744
+ }, options.browser);
745
+ console.log(`🔐 Getting token for destination "${destination}"...`);
746
+ await broker.getToken(destination);
747
+ console.log(`✅ Token obtained successfully`);
748
+ const connConfig = await sessionStore.getConnectionConfig(destination);
749
+ const authConfig = await sessionStore.getAuthorizationConfig(destination);
750
+ if (!connConfig) {
751
+ throw new Error('Connection config not found after authentication');
752
+ }
753
+ const isSaml = !!connConfig.sessionCookies && !connConfig.authorizationToken;
754
+ const token = isSaml
755
+ ? connConfig.sessionCookies
756
+ : connConfig.authorizationToken;
757
+ if (!token) {
758
+ throw new Error('Token provider did not return authorization token');
759
+ }
760
+ if (options.format === 'env') {
761
+ const tempEnvPath = path.join(tempSessionDir, `${destination}.env`);
762
+ if (!fs.existsSync(tempEnvPath)) {
763
+ throw new Error(`Temp env file not found: ${tempEnvPath}`);
764
+ }
765
+ const outputDir = path.dirname(resolvedOutputPath);
766
+ if (!fs.existsSync(outputDir)) {
767
+ fs.mkdirSync(outputDir, { recursive: true });
768
+ }
769
+ fs.copyFileSync(tempEnvPath, resolvedOutputPath);
770
+ console.log(`✅ .env file created: ${resolvedOutputPath}`);
771
+ }
772
+ else {
773
+ const outputData = {
774
+ tokenType: isSaml ? 'saml' : 'jwt',
775
+ };
776
+ if (isSaml) {
777
+ outputData.sessionCookies = token;
778
+ }
779
+ else {
780
+ outputData.accessToken = token;
781
+ }
782
+ if (authConfig?.refreshToken) {
783
+ outputData.refreshToken = authConfig.refreshToken;
784
+ }
785
+ if (serviceUrl) {
786
+ outputData.serviceUrl = serviceUrl;
787
+ }
788
+ if (authConfig?.uaaUrl) {
789
+ outputData.uaaUrl = authConfig.uaaUrl;
790
+ }
791
+ if (authConfig?.uaaClientId) {
792
+ outputData.uaaClientId = authConfig.uaaClientId;
793
+ }
794
+ if (authConfig?.uaaClientSecret) {
795
+ outputData.uaaClientSecret = authConfig.uaaClientSecret;
796
+ }
797
+ const outputDir = path.dirname(resolvedOutputPath);
798
+ if (!fs.existsSync(outputDir)) {
799
+ fs.mkdirSync(outputDir, { recursive: true });
800
+ }
801
+ fs.writeFileSync(resolvedOutputPath, JSON.stringify(outputData, null, 2), 'utf8');
802
+ console.log(`✅ JSON file created: ${resolvedOutputPath}`);
803
+ }
804
+ try {
805
+ fs.rmSync(tempSessionDir, { recursive: true, force: true });
806
+ }
807
+ catch {
808
+ // ignore cleanup errors
809
+ }
810
+ }
811
+ main().catch((error) => {
812
+ console.error(`❌ Error: ${error.message}`);
813
+ if (error.stack) {
814
+ console.error(error.stack);
815
+ }
816
+ process.exit(1);
817
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-broker",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "JWT authentication broker for MCP ABAP ADT - manages tokens based on destination headers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,25 +50,26 @@
50
50
  "generate-env": "tsx bin/generate-env-from-service-key.ts"
51
51
  },
52
52
  "bin": {
53
- "mcp-auth": "./dist/bin/mcp-auth.js"
53
+ "mcp-auth": "./dist/bin/mcp-auth.js",
54
+ "mcp-sso": "./dist/bin/mcp-sso.js"
54
55
  },
55
56
  "engines": {
56
57
  "node": ">=18.0.0"
57
58
  },
58
59
  "dependencies": {
59
- "@mcp-abap-adt/auth-providers": "^1.0.0",
60
- "@mcp-abap-adt/auth-stores": "^1.0.0",
60
+ "@mcp-abap-adt/auth-providers": "^1.0.2",
61
+ "@mcp-abap-adt/auth-stores": "^1.0.1",
61
62
  "@mcp-abap-adt/interfaces": "^2.3.0",
62
- "axios": "^1.13.4",
63
+ "axios": "^1.13.5",
63
64
  "tsx": "^4.21.0"
64
65
  },
65
66
  "devDependencies": {
66
- "@biomejs/biome": "^2.3.13",
67
+ "@biomejs/biome": "^2.3.14",
67
68
  "@mcp-abap-adt/logger": "^0.1.4",
68
69
  "@types/express": "^5.0.5",
69
70
  "@types/jest": "^30.0.0",
70
71
  "@types/js-yaml": "^4.0.9",
71
- "@types/node": "^24.2.1",
72
+ "@types/node": "^25.2.3",
72
73
  "jest": "^30.2.0",
73
74
  "jest-util": "^30.2.0",
74
75
  "js-yaml": "^4.1.1",