@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 +21 -0
- package/README.md +49 -0
- package/dist/bin/mcp-auth.js +29 -21
- package/dist/bin/mcp-sso.js +817 -0
- package/package.json +8 -7
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:
|
package/dist/bin/mcp-auth.js
CHANGED
|
@@ -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
|
|
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 (![
|
|
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(`${
|
|
281
|
+
lines.push(`${auth_stores_1.ABAP_CONNECTION_VARS.SERVICE_URL}=${serviceUrl}`);
|
|
275
282
|
}
|
|
276
|
-
lines.push(`${
|
|
283
|
+
lines.push(`${auth_stores_1.ABAP_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token}`);
|
|
277
284
|
if (refreshToken) {
|
|
278
|
-
lines.push(`${
|
|
285
|
+
lines.push(`${auth_stores_1.ABAP_AUTHORIZATION_VARS.REFRESH_TOKEN}=${refreshToken}`);
|
|
279
286
|
}
|
|
280
287
|
if (uaaUrl) {
|
|
281
|
-
lines.push(`${
|
|
288
|
+
lines.push(`${auth_stores_1.ABAP_AUTHORIZATION_VARS.UAA_URL}=${uaaUrl}`);
|
|
282
289
|
}
|
|
283
290
|
if (uaaClientId) {
|
|
284
|
-
lines.push(`${
|
|
291
|
+
lines.push(`${auth_stores_1.ABAP_AUTHORIZATION_VARS.UAA_CLIENT_ID}=${uaaClientId}`);
|
|
285
292
|
}
|
|
286
293
|
if (uaaClientSecret) {
|
|
287
|
-
lines.push(`${
|
|
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(`${
|
|
302
|
+
lines.push(`${auth_stores_1.XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token}`);
|
|
296
303
|
if (refreshToken) {
|
|
297
|
-
lines.push(`${
|
|
304
|
+
lines.push(`${auth_stores_1.XSUAA_AUTHORIZATION_VARS.REFRESH_TOKEN}=${refreshToken}`);
|
|
298
305
|
}
|
|
299
306
|
if (uaaUrl) {
|
|
300
|
-
lines.push(`${
|
|
307
|
+
lines.push(`${auth_stores_1.XSUAA_AUTHORIZATION_VARS.UAA_URL}=${uaaUrl}`);
|
|
301
308
|
}
|
|
302
309
|
if (uaaClientId) {
|
|
303
|
-
lines.push(`${
|
|
310
|
+
lines.push(`${auth_stores_1.XSUAA_AUTHORIZATION_VARS.UAA_CLIENT_ID}=${uaaClientId}`);
|
|
304
311
|
}
|
|
305
312
|
if (uaaClientSecret) {
|
|
306
|
-
lines.push(`${
|
|
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 =
|
|
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(` - ${
|
|
565
|
+
console.log(` - ${auth_stores_1.ABAP_CONNECTION_VARS.SERVICE_URL}=${finalServiceUrl}`);
|
|
558
566
|
}
|
|
559
|
-
console.log(` - ${
|
|
567
|
+
console.log(` - ${auth_stores_1.ABAP_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`);
|
|
560
568
|
if (authConfig?.refreshToken) {
|
|
561
|
-
console.log(` - ${
|
|
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(` - ${
|
|
576
|
+
console.log(` - ${auth_stores_1.XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN}=${token.substring(0, 50)}...`);
|
|
569
577
|
if (authConfig?.refreshToken) {
|
|
570
|
-
console.log(` - ${
|
|
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.
|
|
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.
|
|
60
|
-
"@mcp-abap-adt/auth-stores": "^1.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.
|
|
63
|
+
"axios": "^1.13.5",
|
|
63
64
|
"tsx": "^4.21.0"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
|
-
"@biomejs/biome": "^2.3.
|
|
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": "^
|
|
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",
|