@mcp-abap-adt/auth-broker 1.0.3 → 1.0.6
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 +25 -0
- package/README.md +60 -8
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +46 -5
- package/dist/bin/mcp-auth.js +85 -3
- package/dist/bin/mcp-sso.js +222 -40
- package/package.json +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,31 @@ Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the co
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.0.6] - 2026-06-02
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `getToken`: no longer retries via the `serviceKey` strategy after the `session` strategy fails with an *interactive* browser-login error (timeout / `BROWSER_AUTH_ERROR`). Both strategies call the same `tokenProvider.getTokens()`, so the retry could not succeed and merely started a duplicate browser login on the same redirect port — surfacing a misleading `Port <n> is already in use` instead of the real cause. Transient/non-interactive session failures still fall through to the `serviceKey` attempt.
|
|
18
|
+
|
|
19
|
+
## [1.0.5] - 2026-02-12
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `mcp-auth` subcommands: `auth-code`, `oidc`, `saml2-pure`, `saml2-bearer` (delegates to `mcp-sso`).
|
|
23
|
+
- `mcp-auth saml2-bearer` requires `--dev` and warns otherwise (in progress).
|
|
24
|
+
- `mcp-sso` subcommands: `oidc`, `saml2`, `bearer`.
|
|
25
|
+
- `mcp-sso`: `--saml-metadata` to resolve XSUAA token alias endpoint from SP metadata.
|
|
26
|
+
- `tests/sso-demo`: service key helper script with `--service/--key/--out` overrides.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `mcp-sso`: allow `--token-endpoint` with `--service-key` for SAML bearer.
|
|
30
|
+
- Docs and test scripts updated for new subcommands.
|
|
31
|
+
- Bumped `@mcp-abap-adt/auth-providers` to `^1.0.5`.
|
|
32
|
+
|
|
33
|
+
## [1.0.4] - 2026-02-11
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- `mcp-sso`: ensure auth config is seeded for broker and avoid writing placeholder secrets.
|
|
37
|
+
|
|
38
|
+
|
|
14
39
|
## [1.0.3] - 2026-02-11
|
|
15
40
|
|
|
16
41
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# @mcp-abap-adt/auth-broker
|
|
2
|
+
[](https://stand-with-ukraine.pp.ua)
|
|
2
3
|
|
|
3
4
|
JWT authentication broker for MCP ABAP ADT server. Manages authentication tokens based on destination headers, automatically loading tokens from `.env` files and refreshing them using service keys when needed.
|
|
4
5
|
|
|
@@ -644,6 +645,7 @@ const btpBrokerFull = new AuthBroker({
|
|
|
644
645
|
Generate or refresh `.env`/JSON output using AuthBroker + stores:
|
|
645
646
|
|
|
646
647
|
```bash
|
|
648
|
+
mcp-auth <auth-code|oidc|saml2-pure|saml2-bearer> [options]
|
|
647
649
|
mcp-auth --service-key <path> --output <path> [--env <path>] [--type abap|xsuaa] [--credential] [--browser auto|none|system|chrome|edge|firefox] [--format json|env]
|
|
648
650
|
```
|
|
649
651
|
|
|
@@ -660,6 +662,18 @@ mcp-auth --service-key <path> --output <path> [--env <path>] [--type abap|xsuaa]
|
|
|
660
662
|
|
|
661
663
|
**Examples:**
|
|
662
664
|
```bash
|
|
665
|
+
# Auth code (default via service key)
|
|
666
|
+
mcp-auth auth-code --service-key ./abap.json --output ./abap.env --type abap
|
|
667
|
+
|
|
668
|
+
# OIDC SSO (device flow example)
|
|
669
|
+
mcp-auth oidc --flow device --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
|
|
670
|
+
|
|
671
|
+
# SAML2 pure (cookie)
|
|
672
|
+
mcp-auth saml2-pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --output ./saml.env --type abap
|
|
673
|
+
|
|
674
|
+
# SAML2 bearer (in progress, requires --dev)
|
|
675
|
+
mcp-auth saml2-bearer --dev --service-key ./mcp.json --assertion <base64> --output ./sso.env --type xsuaa
|
|
676
|
+
|
|
663
677
|
# ABAP: authorization_code (default, opens browser)
|
|
664
678
|
mcp-auth --service-key ./abap.json --output ./abap.env --type abap
|
|
665
679
|
|
|
@@ -681,6 +695,7 @@ mcp-auth --env ./mcp.env --service-key ./mcp.json --output ./mcp.env --type xsua
|
|
|
681
695
|
Get tokens via SSO providers (OIDC/SAML) and generate `.env`/JSON output:
|
|
682
696
|
|
|
683
697
|
```bash
|
|
698
|
+
mcp-sso <oidc|saml2|bearer> [options]
|
|
684
699
|
mcp-sso --protocol <oidc|saml2> --flow <flow> --output <path> [--type abap|xsuaa] [--format env|json] [--env <path>] [--config <path>]
|
|
685
700
|
```
|
|
686
701
|
|
|
@@ -691,27 +706,64 @@ mcp-sso --protocol <oidc|saml2> --flow <flow> --output <path> [--type abap|xsuaa
|
|
|
691
706
|
**Examples:**
|
|
692
707
|
```bash
|
|
693
708
|
# OIDC browser flow
|
|
694
|
-
mcp-sso
|
|
709
|
+
mcp-sso oidc --flow browser --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
|
|
695
710
|
|
|
696
711
|
# OIDC browser flow (manual code / OOB)
|
|
697
|
-
mcp-sso
|
|
712
|
+
mcp-sso 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
713
|
|
|
699
714
|
# OIDC device flow
|
|
700
|
-
mcp-sso
|
|
715
|
+
mcp-sso oidc --flow device --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
|
|
701
716
|
|
|
702
|
-
# OIDC password flow
|
|
703
|
-
mcp-sso
|
|
717
|
+
# OIDC password flow
|
|
718
|
+
mcp-sso oidc --flow password --token-endpoint https://issuer/oauth/token --client-id my-client --username user --password pass --output ./sso.env --type xsuaa
|
|
704
719
|
|
|
705
720
|
# OIDC token exchange
|
|
706
|
-
mcp-sso
|
|
721
|
+
mcp-sso oidc --flow token_exchange --issuer https://issuer --client-id my-client --subject-token <token> --output ./sso.env --type xsuaa
|
|
707
722
|
|
|
708
723
|
# SAML bearer flow (assertion -> token)
|
|
709
|
-
mcp-sso
|
|
724
|
+
mcp-sso 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
725
|
|
|
711
726
|
# SAML pure flow (cookie)
|
|
712
|
-
mcp-sso
|
|
727
|
+
mcp-sso saml2 --flow pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --assertion <base64> --cookie "SAP_SESSION=..." --output ./sso.env --type abap
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**SAML token alias (XSUAA):**
|
|
731
|
+
If your IdP requires the token alias endpoint, pass SAML metadata XML:
|
|
732
|
+
|
|
733
|
+
```bash
|
|
734
|
+
mcp-sso bearer --saml-metadata ./saml-sp.xml --assertion <base64> --service-key ./service-key.json --output ./sso.env --type xsuaa
|
|
713
735
|
```
|
|
714
736
|
|
|
737
|
+
### Local Keycloak (OIDC + SAML Tests)
|
|
738
|
+
|
|
739
|
+
For local testing of `mcp-sso`, a ready-to-run Keycloak setup is included
|
|
740
|
+
(OIDC browser/password/device + SAML assertion capture).
|
|
741
|
+
|
|
742
|
+
```bash
|
|
743
|
+
cd tests/keycloak
|
|
744
|
+
docker compose up -d
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
Then use:
|
|
748
|
+
```bash
|
|
749
|
+
node dist/bin/mcp-sso.js \
|
|
750
|
+
oidc \
|
|
751
|
+
--flow browser \
|
|
752
|
+
--issuer http://localhost:8080/realms/mcp-sso \
|
|
753
|
+
--client-id mcp-sso-cli \
|
|
754
|
+
--scopes openid,profile,email \
|
|
755
|
+
--output /tmp/keycloak.env \
|
|
756
|
+
--type xsuaa
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
See `tests/keycloak/README.md` for device flow and SAML examples.
|
|
760
|
+
|
|
761
|
+
### XSUAA Demo (CAP)
|
|
762
|
+
|
|
763
|
+
A minimal CAP app for testing XSUAA flows is included at `tests/sso-demo`.
|
|
764
|
+
It enables `authorization_code` and `saml2-bearer` grant types and provides a
|
|
765
|
+
simple `CatalogService`. See `tests/sso-demo/readme.md` for deploy steps.
|
|
766
|
+
|
|
715
767
|
**Config file:**
|
|
716
768
|
You can pass a JSON file with provider config:
|
|
717
769
|
|
package/dist/AuthBroker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,eAAe,EAGrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACd,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,eAAe,EAGrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACd,MAAM,qBAAqB,CAAC;AA8D7B;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,YAAY,EAAE,aAAa,CAAC;IAC5B,uEAAuE;IACvE,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,4IAA4I;IAC5I,aAAa,EAAE,cAAc,CAAC;IAC9B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAiB;IACtC,OAAO,CAAC,gBAAgB,CAAU;IAElC;;;;;;;;;;;OAWG;gBACS,MAAM,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO;IAsFxE;;OAEG;YACW,eAAe;IA0D7B;;OAEG;YACW,aAAa;IAoD3B;;OAEG;YACW,oCAAoC;IA4ClD;;OAEG;YACW,kBAAkB;YA4ClB,aAAa;YA0Db,kBAAkB;IAkDhC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACG,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAsLpD;;;;;OAKG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASxD;;;;OAIG;IACG,sBAAsB,CAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAoEvC;;;;OAIG;IACG,mBAAmB,CACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAuEpC;;;;;;;;;;;;;;;;OAgBG;IACH,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe;CAqB3D"}
|
package/dist/AuthBroker.js
CHANGED
|
@@ -32,6 +32,20 @@ function hasErrorCode(error) {
|
|
|
32
32
|
function getErrorMessage(error) {
|
|
33
33
|
return error instanceof Error ? error.message : String(error);
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether an error represents a failed *interactive* browser login (the user
|
|
37
|
+
* did not complete the OAuth flow / it timed out), as opposed to a transient or
|
|
38
|
+
* configuration error. Such failures must not be retried via a second
|
|
39
|
+
* provider.getTokens() — that would start a duplicate browser login on the same
|
|
40
|
+
* redirect port and mask the real cause with a "Port in use" error.
|
|
41
|
+
*/
|
|
42
|
+
function isInteractiveAuthFailure(error) {
|
|
43
|
+
if (hasErrorCode(error) && error.code === 'BROWSER_AUTH_ERROR') {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const message = getErrorMessage(error);
|
|
47
|
+
return /authentication timeout|browser authentication|already in use/i.test(message);
|
|
48
|
+
}
|
|
35
49
|
/**
|
|
36
50
|
* AuthBroker manages JWT authentication tokens for destinations
|
|
37
51
|
*/
|
|
@@ -244,12 +258,19 @@ class AuthBroker {
|
|
|
244
258
|
this.logger?.error(`Failed to save connection config to session for ${destination}: ${getErrorMessage(error)}`);
|
|
245
259
|
throw new Error(`Failed to save connection config for destination "${destination}": ${getErrorMessage(error)}`);
|
|
246
260
|
}
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
if (authorizationConfig.uaaUrl &&
|
|
262
|
+
authorizationConfig.uaaClientId &&
|
|
263
|
+
authorizationConfig.uaaClientSecret) {
|
|
264
|
+
try {
|
|
265
|
+
await this.sessionStore.setAuthorizationConfig(destination, authorizationConfig);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
this.logger?.error(`Failed to save authorization config to session for ${destination}: ${getErrorMessage(error)}`);
|
|
269
|
+
throw new Error(`Failed to save authorization config for destination "${destination}": ${getErrorMessage(error)}`);
|
|
270
|
+
}
|
|
249
271
|
}
|
|
250
|
-
|
|
251
|
-
this.logger?.
|
|
252
|
-
throw new Error(`Failed to save authorization config for destination "${destination}": ${getErrorMessage(error)}`);
|
|
272
|
+
else {
|
|
273
|
+
this.logger?.debug(`Skipping authorization config save for ${destination}: missing UAA fields`);
|
|
253
274
|
}
|
|
254
275
|
}
|
|
255
276
|
async requestTokens(destination, sourceLabel) {
|
|
@@ -433,11 +454,31 @@ class AuthBroker {
|
|
|
433
454
|
throw error;
|
|
434
455
|
}
|
|
435
456
|
if (!this.serviceKeyStore) {
|
|
457
|
+
const tokenType = this.tokenProvider?.tokenType;
|
|
458
|
+
if (tokenType === 'saml') {
|
|
459
|
+
const tokenResult = await this.requestTokens(destination, 'session');
|
|
460
|
+
await this.persistTokenResult(destination, serviceUrl, connConfig, authConfig || {}, tokenResult);
|
|
461
|
+
this.logger?.info(`[AuthBroker] Token retrieved for ${destination} (SAML without auth config)`, {
|
|
462
|
+
authorizationToken: (0, formatting_1.formatToken)(tokenResult.authorizationToken),
|
|
463
|
+
});
|
|
464
|
+
return tokenResult.authorizationToken;
|
|
465
|
+
}
|
|
436
466
|
if (lastError) {
|
|
437
467
|
throw lastError;
|
|
438
468
|
}
|
|
439
469
|
throw new Error(`Authorization config not found for ${destination}. Session has no auth config and serviceKeyStore is not available.`);
|
|
440
470
|
}
|
|
471
|
+
// If the session attempt already performed an *interactive* browser login
|
|
472
|
+
// and it failed (the user didn't complete it / it timed out), the serviceKey
|
|
473
|
+
// strategy would call the SAME provider.getTokens() again and start a
|
|
474
|
+
// duplicate browser login on the same redirect port — surfacing a misleading
|
|
475
|
+
// "Port in use" instead of the real cause. Don't retry interactive failures;
|
|
476
|
+
// propagate the original error. Transient/non-interactive session failures
|
|
477
|
+
// still fall through to the serviceKey attempt below.
|
|
478
|
+
if (lastError && isInteractiveAuthFailure(lastError)) {
|
|
479
|
+
this.logger?.debug(`Step 2: session login failed interactively for ${destination}; not retrying via service key (${getErrorMessage(lastError)})`);
|
|
480
|
+
throw lastError;
|
|
481
|
+
}
|
|
441
482
|
const serviceKeyAuthConfig = await this.getAuthorizationConfigFromServiceKey(destination);
|
|
442
483
|
const tokenResult = await this.requestTokens(destination, 'serviceKey');
|
|
443
484
|
await this.persistTokenResult(destination, serviceUrl, connConfig, serviceKeyAuthConfig, tokenResult);
|
package/dist/bin/mcp-auth.js
CHANGED
|
@@ -53,6 +53,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
53
53
|
};
|
|
54
54
|
})();
|
|
55
55
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
+
const child_process_1 = require("child_process");
|
|
56
57
|
const fs = __importStar(require("fs"));
|
|
57
58
|
const path = __importStar(require("path"));
|
|
58
59
|
// Use require for CommonJS dist files with absolute path
|
|
@@ -86,6 +87,7 @@ function showHelp() {
|
|
|
86
87
|
console.log('MCP Auth - Get tokens and generate .env files from service keys');
|
|
87
88
|
console.log('');
|
|
88
89
|
console.log('Usage:');
|
|
90
|
+
console.log(' mcp-auth <auth-code|oidc|saml2-pure|saml2-bearer> [options]');
|
|
89
91
|
console.log(' mcp-auth --service-key <path> --output <path> [options]');
|
|
90
92
|
console.log('');
|
|
91
93
|
console.log('Required Options:');
|
|
@@ -97,6 +99,7 @@ function showHelp() {
|
|
|
97
99
|
console.log('');
|
|
98
100
|
console.log('Optional Options:');
|
|
99
101
|
console.log(' --type <type> Auth type: abap or xsuaa (default: abap)');
|
|
102
|
+
console.log(' --dev Enable in-progress commands (saml2-bearer)');
|
|
100
103
|
console.log(' --credential Use client_credentials flow (clientId/clientSecret, no browser)');
|
|
101
104
|
console.log(' By default uses authorization_code flow');
|
|
102
105
|
console.log(' --browser <browser> Browser for authorization_code flow (default: auto):');
|
|
@@ -111,6 +114,18 @@ function showHelp() {
|
|
|
111
114
|
console.log(' --help, -h Show this help message');
|
|
112
115
|
console.log('');
|
|
113
116
|
console.log('Examples:');
|
|
117
|
+
console.log(' # Auth code (default flow via service key)');
|
|
118
|
+
console.log(' mcp-auth auth-code --service-key ./service-key.json --output ./mcp.env --type xsuaa');
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(' # OIDC SSO (device/password/browser/token-exchange)');
|
|
121
|
+
console.log(' mcp-auth oidc --flow device --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa');
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(' # SAML2 pure (cookies)');
|
|
124
|
+
console.log(' mcp-auth saml2-pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --output ./saml.env --type abap');
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log(' # SAML2 bearer (in progress, requires --dev)');
|
|
127
|
+
console.log(' mcp-auth saml2-bearer --dev --service-key ./service-key.json --assertion <base64> --output ./sso.env --type xsuaa');
|
|
128
|
+
console.log('');
|
|
114
129
|
console.log(' # XSUAA with authorization_code (default, opens browser)');
|
|
115
130
|
console.log(' mcp-auth --service-key ./service-key.json --output ./mcp.env --type xsuaa');
|
|
116
131
|
console.log('');
|
|
@@ -151,8 +166,7 @@ function showHelp() {
|
|
|
151
166
|
console.log(' - For ABAP, serviceUrl (SAP URL) is required - can be provided via --service-url or service key');
|
|
152
167
|
console.log(' - SAP_URL/XSUAA_MCP_URL is written to .env (from --service-url, service key, or placeholder)');
|
|
153
168
|
}
|
|
154
|
-
function parseArgs() {
|
|
155
|
-
const args = process.argv.slice(2);
|
|
169
|
+
function parseArgs(args = process.argv.slice(2)) {
|
|
156
170
|
// Handle --version and --help first
|
|
157
171
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
158
172
|
showHelp();
|
|
@@ -348,8 +362,76 @@ function writeJsonFile(outputPath, token, refreshToken, serviceUrl, uaaUrl, uaaC
|
|
|
348
362
|
// Write to file
|
|
349
363
|
fs.writeFileSync(outputPath, JSON.stringify(outputData, null, 2), 'utf8');
|
|
350
364
|
}
|
|
365
|
+
function runMcpSso(args) {
|
|
366
|
+
const mcpSsoPath = path.resolve(__dirname, 'mcp-sso.js');
|
|
367
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [mcpSsoPath, ...args], {
|
|
368
|
+
stdio: 'inherit',
|
|
369
|
+
});
|
|
370
|
+
if (result.error) {
|
|
371
|
+
throw result.error;
|
|
372
|
+
}
|
|
373
|
+
process.exit(result.status ?? 1);
|
|
374
|
+
}
|
|
351
375
|
async function main() {
|
|
352
|
-
const
|
|
376
|
+
const rawArgs = process.argv.slice(2);
|
|
377
|
+
const subcommand = rawArgs[0];
|
|
378
|
+
const hasSubcommand = subcommand && !subcommand.startsWith('-') && subcommand.length > 0;
|
|
379
|
+
if (hasSubcommand) {
|
|
380
|
+
const remaining = rawArgs.slice(1);
|
|
381
|
+
const ensureNoProtocol = () => {
|
|
382
|
+
if (remaining.includes('--protocol')) {
|
|
383
|
+
console.error('❌ --protocol is not supported with subcommands.');
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
switch (subcommand) {
|
|
388
|
+
case 'auth-code': {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case 'oidc': {
|
|
392
|
+
ensureNoProtocol();
|
|
393
|
+
runMcpSso(['oidc', ...remaining]);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
case 'saml2-pure': {
|
|
397
|
+
ensureNoProtocol();
|
|
398
|
+
if (remaining.includes('--flow')) {
|
|
399
|
+
const idx = remaining.indexOf('--flow');
|
|
400
|
+
const flow = remaining[idx + 1];
|
|
401
|
+
if (flow && flow !== 'pure') {
|
|
402
|
+
console.error('❌ saml2-pure requires --flow pure.');
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
remaining.unshift('pure');
|
|
408
|
+
remaining.unshift('--flow');
|
|
409
|
+
}
|
|
410
|
+
runMcpSso(['saml2', ...remaining]);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
case 'saml2-bearer': {
|
|
414
|
+
ensureNoProtocol();
|
|
415
|
+
if (!remaining.includes('--dev')) {
|
|
416
|
+
console.error('⚠️ saml2-bearer is in progress. Re-run with --dev to enable.');
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
const filtered = remaining.filter((arg) => arg !== '--dev');
|
|
420
|
+
if (filtered.includes('--flow')) {
|
|
421
|
+
console.error('❌ saml2-bearer does not accept --flow.');
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
runMcpSso(['bearer', ...filtered]);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
default: {
|
|
428
|
+
console.error(`Unknown command: ${subcommand}`);
|
|
429
|
+
showHelp();
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const options = parseArgs(hasSubcommand ? rawArgs.slice(1) : rawArgs);
|
|
353
435
|
if (!options) {
|
|
354
436
|
// Help or version was shown, exit already handled
|
|
355
437
|
return;
|
package/dist/bin/mcp-sso.js
CHANGED
|
@@ -4,26 +4,27 @@
|
|
|
4
4
|
* MCP SSO - Get tokens via SSO providers and generate .env files
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
+
* mcp-sso <oidc|saml2|bearer> [options]
|
|
7
8
|
* mcp-sso --protocol <oidc|saml2> --flow <flow> --output <path> [options]
|
|
8
9
|
*
|
|
9
10
|
* Examples:
|
|
10
11
|
* # OIDC browser flow (authorization code with local callback)
|
|
11
|
-
* mcp-sso
|
|
12
|
+
* mcp-sso oidc --flow browser --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
|
|
12
13
|
*
|
|
13
14
|
* # OIDC device flow
|
|
14
|
-
* mcp-sso
|
|
15
|
+
* mcp-sso oidc --flow device --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
|
|
15
16
|
*
|
|
16
|
-
* # OIDC password flow
|
|
17
|
-
* mcp-sso
|
|
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
|
|
18
19
|
*
|
|
19
20
|
* # OIDC token exchange
|
|
20
|
-
* mcp-sso
|
|
21
|
+
* mcp-sso oidc --flow token_exchange --issuer https://issuer --client-id my-client --subject-token <token> --output ./sso.env --type xsuaa
|
|
21
22
|
*
|
|
22
23
|
* # SAML bearer flow
|
|
23
|
-
* mcp-sso
|
|
24
|
+
* mcp-sso 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
|
*
|
|
25
26
|
* # SAML pure flow (cookie)
|
|
26
|
-
* mcp-sso
|
|
27
|
+
* mcp-sso 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
|
*/
|
|
28
29
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
29
30
|
if (k2 === undefined) k2 = k;
|
|
@@ -58,17 +59,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
58
59
|
return result;
|
|
59
60
|
};
|
|
60
61
|
})();
|
|
61
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
62
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
63
|
-
};
|
|
64
62
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
65
63
|
const node_readline_1 = require("node:readline");
|
|
66
|
-
const axios_1 = __importDefault(require("axios"));
|
|
67
64
|
const fs = __importStar(require("fs"));
|
|
68
65
|
const path = __importStar(require("path"));
|
|
69
66
|
// Use require for CommonJS dist files with absolute path
|
|
70
67
|
const distPath = path.resolve(__dirname, '..', 'index.js');
|
|
71
68
|
const { AuthBroker } = require(distPath);
|
|
69
|
+
const logger_1 = require("@mcp-abap-adt/logger");
|
|
72
70
|
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
73
71
|
const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
|
|
74
72
|
function getVersion() {
|
|
@@ -97,14 +95,16 @@ function showHelp() {
|
|
|
97
95
|
console.log('MCP SSO - Get tokens via SSO providers and generate .env files');
|
|
98
96
|
console.log('');
|
|
99
97
|
console.log('Usage:');
|
|
98
|
+
console.log(' mcp-sso <oidc|saml2|bearer> [options]');
|
|
100
99
|
console.log(' mcp-sso --protocol <oidc|saml2> --flow <flow> --output <path> [options]');
|
|
101
100
|
console.log('');
|
|
102
101
|
console.log('Required Options:');
|
|
103
102
|
console.log(' --output <path> Output file path');
|
|
104
|
-
console.log(' --protocol <oidc|saml2> Protocol');
|
|
105
|
-
console.log(' --flow <flow> Flow for protocol');
|
|
103
|
+
console.log(' --protocol <oidc|saml2> Protocol (if no subcommand)');
|
|
104
|
+
console.log(' --flow <flow> Flow for protocol (if no subcommand)');
|
|
106
105
|
console.log('');
|
|
107
106
|
console.log('Common Options:');
|
|
107
|
+
console.log(' --service-key <path> Service key JSON (XSUAA/ABAP)');
|
|
108
108
|
console.log(' --type <abap|xsuaa> Output type (default: abap)');
|
|
109
109
|
console.log(' --format <env|json> Output format (default: env)');
|
|
110
110
|
console.log(' --env <path> Optional existing env file (used for refresh)');
|
|
@@ -134,12 +134,12 @@ function showHelp() {
|
|
|
134
134
|
console.log(' --actor-token <token> Actor token for token exchange');
|
|
135
135
|
console.log(' --actor-token-type <type> Actor token type for token exchange');
|
|
136
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
137
|
console.log('');
|
|
139
138
|
console.log('SAML Options:');
|
|
140
139
|
console.log(' --idp-sso-url <url> IdP SSO URL');
|
|
141
140
|
console.log(' --sp-entity-id <id> SP Entity ID');
|
|
142
141
|
console.log(' --acs-url <url> ACS URL (default: http://localhost:<port>/callback)');
|
|
142
|
+
console.log(' --saml-metadata <path> SAML metadata XML (to resolve token alias)');
|
|
143
143
|
console.log(' --relay-state <value> RelayState (optional)');
|
|
144
144
|
console.log(' --assertion-flow <flow> browser|manual|assertion (default: browser)');
|
|
145
145
|
console.log(' --assertion <base64> SAMLResponse (base64)');
|
|
@@ -170,8 +170,47 @@ function readManualInput(prompt) {
|
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
|
+
function createCliLogger(prefix = 'SSO') {
|
|
174
|
+
const isEnabled = () => {
|
|
175
|
+
if (process.env.DEBUG_SSO === 'false' ||
|
|
176
|
+
process.env.DEBUG_AUTH_SSO === 'false') {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (process.env.DEBUG_SSO === 'true' ||
|
|
180
|
+
process.env.DEBUG_AUTH_SSO === 'true' ||
|
|
181
|
+
process.env.DEBUG === 'true' ||
|
|
182
|
+
process.env.DEBUG?.includes('sso') === true ||
|
|
183
|
+
process.env.DEBUG?.includes('auth-sso') === true) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
};
|
|
188
|
+
const baseLogger = new logger_1.DefaultLogger((0, logger_1.getLogLevel)());
|
|
189
|
+
return {
|
|
190
|
+
debug: (message, meta) => {
|
|
191
|
+
if (isEnabled()) {
|
|
192
|
+
baseLogger.debug(`[${prefix}] ${message}`, meta);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
info: (message, meta) => {
|
|
196
|
+
if (isEnabled()) {
|
|
197
|
+
baseLogger.info(`[${prefix}] ${message}`, meta);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
warn: (message, meta) => {
|
|
201
|
+
if (isEnabled()) {
|
|
202
|
+
baseLogger.warn(`[${prefix}] ${message}`, meta);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
error: (message, meta) => {
|
|
206
|
+
if (isEnabled()) {
|
|
207
|
+
baseLogger.error(`[${prefix}] ${message}`, meta);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
173
212
|
function parseArgs() {
|
|
174
|
-
|
|
213
|
+
let args = process.argv.slice(2);
|
|
175
214
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
176
215
|
showHelp();
|
|
177
216
|
process.exit(0);
|
|
@@ -183,6 +222,7 @@ function parseArgs() {
|
|
|
183
222
|
let outputFile;
|
|
184
223
|
let envFilePath;
|
|
185
224
|
let destination;
|
|
225
|
+
let serviceKeyPath;
|
|
186
226
|
let authType = 'abap';
|
|
187
227
|
let format = 'env';
|
|
188
228
|
let protocol;
|
|
@@ -217,7 +257,26 @@ function parseArgs() {
|
|
|
217
257
|
let assertion;
|
|
218
258
|
let cookie;
|
|
219
259
|
let uaaUrl;
|
|
220
|
-
let
|
|
260
|
+
let samlMetadataPath;
|
|
261
|
+
const firstArg = args[0];
|
|
262
|
+
if (firstArg && !firstArg.startsWith('-')) {
|
|
263
|
+
if (firstArg === 'oidc') {
|
|
264
|
+
protocol = 'oidc';
|
|
265
|
+
}
|
|
266
|
+
else if (firstArg === 'saml2') {
|
|
267
|
+
protocol = 'saml2';
|
|
268
|
+
}
|
|
269
|
+
else if (firstArg === 'bearer') {
|
|
270
|
+
protocol = 'saml2';
|
|
271
|
+
flow = 'bearer';
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.error(`Unknown command: ${firstArg}`);
|
|
275
|
+
showHelp();
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
args = args.slice(1);
|
|
279
|
+
}
|
|
221
280
|
for (let i = 0; i < args.length; i++) {
|
|
222
281
|
const arg = args[i];
|
|
223
282
|
const next = i + 1 < args.length ? args[i + 1] : undefined;
|
|
@@ -230,6 +289,10 @@ function parseArgs() {
|
|
|
230
289
|
envFilePath = next;
|
|
231
290
|
i++;
|
|
232
291
|
break;
|
|
292
|
+
case '--service-key':
|
|
293
|
+
serviceKeyPath = next;
|
|
294
|
+
i++;
|
|
295
|
+
break;
|
|
233
296
|
case '--destination':
|
|
234
297
|
destination = next;
|
|
235
298
|
i++;
|
|
@@ -402,8 +465,8 @@ function parseArgs() {
|
|
|
402
465
|
uaaUrl = next;
|
|
403
466
|
i++;
|
|
404
467
|
break;
|
|
405
|
-
case '--
|
|
406
|
-
|
|
468
|
+
case '--saml-metadata':
|
|
469
|
+
samlMetadataPath = next;
|
|
407
470
|
i++;
|
|
408
471
|
break;
|
|
409
472
|
default:
|
|
@@ -414,6 +477,7 @@ function parseArgs() {
|
|
|
414
477
|
outputFile,
|
|
415
478
|
envFilePath,
|
|
416
479
|
destination,
|
|
480
|
+
serviceKeyPath,
|
|
417
481
|
authType,
|
|
418
482
|
format,
|
|
419
483
|
protocol,
|
|
@@ -448,9 +512,14 @@ function parseArgs() {
|
|
|
448
512
|
assertion,
|
|
449
513
|
cookie,
|
|
450
514
|
uaaUrl,
|
|
451
|
-
|
|
515
|
+
samlMetadataPath,
|
|
452
516
|
};
|
|
453
517
|
}
|
|
518
|
+
function resolveSamlTokenAlias(metadataXml) {
|
|
519
|
+
const regex = /<md:AssertionConsumerService[^>]*Location="([^"]*\/oauth\/token\/alias\/[^"]+)"/i;
|
|
520
|
+
const match = metadataXml.match(regex);
|
|
521
|
+
return match?.[1];
|
|
522
|
+
}
|
|
454
523
|
function normalizeProviderConfig(raw) {
|
|
455
524
|
if (!raw || typeof raw !== 'object') {
|
|
456
525
|
return null;
|
|
@@ -520,10 +589,13 @@ function buildSamlConfig(options) {
|
|
|
520
589
|
: assertionFlow === 'assertion'
|
|
521
590
|
? async () => readManualInput('Paste SAMLResponse: ')
|
|
522
591
|
: undefined;
|
|
523
|
-
const cookieProvider = async () => {
|
|
592
|
+
const cookieProvider = async (samlResponse) => {
|
|
524
593
|
if (options.cookie) {
|
|
525
594
|
return options.cookie;
|
|
526
595
|
}
|
|
596
|
+
if (assertionFlow === 'assertion') {
|
|
597
|
+
return `SAMLResponse=${samlResponse}`;
|
|
598
|
+
}
|
|
527
599
|
return readManualInput('Paste session cookies: ');
|
|
528
600
|
};
|
|
529
601
|
return {
|
|
@@ -632,18 +704,12 @@ function buildProviderConfig(options, existingAuth, existingConn, fileConfig) {
|
|
|
632
704
|
}
|
|
633
705
|
return result;
|
|
634
706
|
}
|
|
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
707
|
async function main() {
|
|
643
708
|
const options = parseArgs();
|
|
644
709
|
if (!options) {
|
|
645
710
|
return;
|
|
646
711
|
}
|
|
712
|
+
const logger = createCliLogger();
|
|
647
713
|
if (!options.outputFile) {
|
|
648
714
|
console.error('❌ Missing required --output');
|
|
649
715
|
process.exit(1);
|
|
@@ -653,6 +719,19 @@ async function main() {
|
|
|
653
719
|
? path.resolve(options.envFilePath)
|
|
654
720
|
: undefined;
|
|
655
721
|
let destination = options.destination;
|
|
722
|
+
if (options.serviceKeyPath) {
|
|
723
|
+
const resolvedServiceKeyPath = path.resolve(options.serviceKeyPath);
|
|
724
|
+
if (!fs.existsSync(resolvedServiceKeyPath)) {
|
|
725
|
+
console.error(`❌ Service key file not found: ${resolvedServiceKeyPath}`);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
const serviceKeyFileName = path.basename(resolvedServiceKeyPath, path.extname(resolvedServiceKeyPath));
|
|
729
|
+
if (destination && destination !== serviceKeyFileName) {
|
|
730
|
+
console.error(`❌ Destination mismatch: service key (${serviceKeyFileName}) vs output (${destination})`);
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
destination = serviceKeyFileName;
|
|
734
|
+
}
|
|
656
735
|
if (!destination) {
|
|
657
736
|
destination = path.basename(resolvedOutputPath, path.extname(resolvedOutputPath));
|
|
658
737
|
}
|
|
@@ -663,6 +742,24 @@ async function main() {
|
|
|
663
742
|
process.exit(1);
|
|
664
743
|
}
|
|
665
744
|
}
|
|
745
|
+
const allowTokenEndpointWithServiceKey = options.protocol === 'saml2' && options.flow === 'bearer';
|
|
746
|
+
const serviceKeyConflicts = options.serviceKeyPath &&
|
|
747
|
+
(options.configPath ||
|
|
748
|
+
options.issuerUrl ||
|
|
749
|
+
options.authorizationEndpoint ||
|
|
750
|
+
(!allowTokenEndpointWithServiceKey && options.tokenEndpoint) ||
|
|
751
|
+
options.deviceAuthorizationEndpoint ||
|
|
752
|
+
options.clientId ||
|
|
753
|
+
options.clientSecret ||
|
|
754
|
+
options.uaaUrl);
|
|
755
|
+
if (serviceKeyConflicts) {
|
|
756
|
+
console.error('❌ Use either --service-key or explicit OIDC/SAML parameters (issuer/token/client/uaa).');
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
if (options.serviceKeyPath && options.authType !== 'xsuaa') {
|
|
760
|
+
console.error('❌ --service-key is supported only for XSUAA flows.');
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
666
763
|
let providerConfigFromFile = null;
|
|
667
764
|
if (options.configPath) {
|
|
668
765
|
const resolvedConfigPath = path.resolve(options.configPath);
|
|
@@ -677,13 +774,50 @@ async function main() {
|
|
|
677
774
|
process.exit(1);
|
|
678
775
|
}
|
|
679
776
|
}
|
|
680
|
-
if (options.
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
777
|
+
if (options.serviceKeyPath) {
|
|
778
|
+
const resolvedServiceKeyPath = path.resolve(options.serviceKeyPath);
|
|
779
|
+
const serviceKeyDir = path.dirname(resolvedServiceKeyPath);
|
|
780
|
+
const serviceKeyStore = new auth_stores_1.XsuaaServiceKeyStore(serviceKeyDir);
|
|
781
|
+
const authConfig = await serviceKeyStore.getAuthorizationConfig(destination);
|
|
782
|
+
if (!authConfig) {
|
|
783
|
+
console.error(`❌ Authorization config not found for ${destination}. Service key must contain clientid, clientsecret, and url fields.`);
|
|
784
|
+
process.exit(1);
|
|
684
785
|
}
|
|
685
|
-
|
|
686
|
-
|
|
786
|
+
const uaaUrl = authConfig.uaaUrl;
|
|
787
|
+
if (!uaaUrl) {
|
|
788
|
+
console.error(`❌ Service key missing UAA URL for ${destination}.`);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
options.uaaUrl = uaaUrl;
|
|
792
|
+
options.clientId = authConfig.uaaClientId;
|
|
793
|
+
options.clientSecret = authConfig.uaaClientSecret;
|
|
794
|
+
if (!options.issuerUrl) {
|
|
795
|
+
options.issuerUrl = uaaUrl;
|
|
796
|
+
}
|
|
797
|
+
if (!options.tokenEndpoint) {
|
|
798
|
+
options.tokenEndpoint = `${uaaUrl.replace(/\/+$/, '')}/oauth/token`;
|
|
799
|
+
}
|
|
800
|
+
if (!options.authorizationEndpoint) {
|
|
801
|
+
options.authorizationEndpoint = `${uaaUrl.replace(/\/+$/, '')}/oauth/authorize`;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (options.samlMetadataPath) {
|
|
805
|
+
const resolvedMetadataPath = path.resolve(options.samlMetadataPath);
|
|
806
|
+
if (!fs.existsSync(resolvedMetadataPath)) {
|
|
807
|
+
console.error(`❌ SAML metadata file not found: ${resolvedMetadataPath}`);
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
const metadataXml = fs.readFileSync(resolvedMetadataPath, 'utf8');
|
|
811
|
+
const aliasUrl = resolveSamlTokenAlias(metadataXml);
|
|
812
|
+
if (!aliasUrl) {
|
|
813
|
+
console.error('❌ SAML metadata does not contain token alias endpoint.');
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
options.tokenEndpoint = aliasUrl;
|
|
817
|
+
}
|
|
818
|
+
if (options.protocol === 'oidc' && options.flow === 'password') {
|
|
819
|
+
if (options.uaaUrl && !options.tokenEndpoint) {
|
|
820
|
+
options.tokenEndpoint = `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`;
|
|
687
821
|
}
|
|
688
822
|
}
|
|
689
823
|
if (options.protocol === 'oidc' && options.flow) {
|
|
@@ -713,10 +847,13 @@ async function main() {
|
|
|
713
847
|
const tempEnvPath = path.join(tempSessionDir, `${destination}.env`);
|
|
714
848
|
fs.copyFileSync(resolvedEnvPath, tempEnvPath);
|
|
715
849
|
}
|
|
716
|
-
const
|
|
850
|
+
const placeholderServiceUrl = '<SERVICE_URL>';
|
|
851
|
+
const defaultServiceUrl = options.authType === 'xsuaa'
|
|
852
|
+
? options.serviceUrl || placeholderServiceUrl
|
|
853
|
+
: options.serviceUrl || '';
|
|
717
854
|
const sessionStore = options.authType === 'xsuaa'
|
|
718
855
|
? new auth_stores_1.XsuaaSessionStore(tempSessionDir, defaultServiceUrl)
|
|
719
|
-
: new auth_stores_1.AbapSessionStore(tempSessionDir);
|
|
856
|
+
: new auth_stores_1.AbapSessionStore(tempSessionDir, undefined, options.serviceUrl);
|
|
720
857
|
const existingConn = await sessionStore.getConnectionConfig(destination);
|
|
721
858
|
const existingAuth = await sessionStore.getAuthorizationConfig(destination);
|
|
722
859
|
const serviceUrl = options.serviceUrl || existingConn?.serviceUrl;
|
|
@@ -730,18 +867,47 @@ async function main() {
|
|
|
730
867
|
console.error('❌ SAML pure flow is only supported for ABAP sessions (cookies)');
|
|
731
868
|
process.exit(1);
|
|
732
869
|
}
|
|
870
|
+
const isSamlPureAbap = options.authType === 'abap' &&
|
|
871
|
+
options.protocol === 'saml2' &&
|
|
872
|
+
options.flow === 'pure';
|
|
733
873
|
await sessionStore.setConnectionConfig(destination, {
|
|
734
874
|
serviceUrl: serviceUrl ||
|
|
735
875
|
(options.authType === 'xsuaa' ? defaultServiceUrl : undefined),
|
|
736
|
-
authorizationToken:
|
|
876
|
+
authorizationToken: isSamlPureAbap
|
|
877
|
+
? existingConn?.authorizationToken || '__init__'
|
|
878
|
+
: existingConn?.authorizationToken,
|
|
737
879
|
sessionCookies: existingConn?.sessionCookies,
|
|
738
880
|
});
|
|
881
|
+
let stripClientSecret = false;
|
|
882
|
+
const authUaaUrl = options.uaaUrl || options.tokenEndpoint || options.issuerUrl || undefined;
|
|
883
|
+
if (options.clientId && authUaaUrl) {
|
|
884
|
+
let clientSecret = options.clientSecret;
|
|
885
|
+
if (!clientSecret) {
|
|
886
|
+
clientSecret = '__public__';
|
|
887
|
+
stripClientSecret = true;
|
|
888
|
+
}
|
|
889
|
+
await sessionStore.setAuthorizationConfig(destination, {
|
|
890
|
+
uaaUrl: authUaaUrl,
|
|
891
|
+
uaaClientId: options.clientId,
|
|
892
|
+
uaaClientSecret: clientSecret,
|
|
893
|
+
refreshToken: existingAuth?.refreshToken,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
739
896
|
const providerConfig = buildProviderConfig(options, existingAuth, existingConn, providerConfigFromFile);
|
|
740
|
-
const
|
|
897
|
+
const providerConfigWithLogger = providerConfig.config
|
|
898
|
+
? {
|
|
899
|
+
...providerConfig,
|
|
900
|
+
config: {
|
|
901
|
+
...providerConfig.config,
|
|
902
|
+
logger: providerConfig.config?.logger ?? logger,
|
|
903
|
+
},
|
|
904
|
+
}
|
|
905
|
+
: providerConfig;
|
|
906
|
+
const tokenProvider = auth_providers_1.SsoProviderFactory.create(providerConfigWithLogger);
|
|
741
907
|
const broker = new AuthBroker({
|
|
742
908
|
sessionStore,
|
|
743
909
|
tokenProvider,
|
|
744
|
-
}, options.browser);
|
|
910
|
+
}, options.browser, logger);
|
|
745
911
|
console.log(`🔐 Getting token for destination "${destination}"...`);
|
|
746
912
|
await broker.getToken(destination);
|
|
747
913
|
console.log(`✅ Token obtained successfully`);
|
|
@@ -766,7 +932,21 @@ async function main() {
|
|
|
766
932
|
if (!fs.existsSync(outputDir)) {
|
|
767
933
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
768
934
|
}
|
|
769
|
-
fs.
|
|
935
|
+
let envContent = fs.readFileSync(tempEnvPath, 'utf8');
|
|
936
|
+
if (options.authType === 'xsuaa' && !serviceUrl) {
|
|
937
|
+
const lines = envContent
|
|
938
|
+
.split('\n')
|
|
939
|
+
.filter((line) => !line.startsWith('XSUAA_MCP_URL='));
|
|
940
|
+
envContent = `${lines.join('\n')}\n`;
|
|
941
|
+
}
|
|
942
|
+
if (stripClientSecret) {
|
|
943
|
+
const lines = envContent
|
|
944
|
+
.split('\n')
|
|
945
|
+
.filter((line) => !line.startsWith('XSUAA_UAA_CLIENT_SECRET=') &&
|
|
946
|
+
!line.startsWith('SAP_UAA_CLIENT_SECRET='));
|
|
947
|
+
envContent = `${lines.join('\n')}\n`;
|
|
948
|
+
}
|
|
949
|
+
fs.writeFileSync(resolvedOutputPath, envContent, 'utf8');
|
|
770
950
|
console.log(`✅ .env file created: ${resolvedOutputPath}`);
|
|
771
951
|
}
|
|
772
952
|
else {
|
|
@@ -792,7 +972,9 @@ async function main() {
|
|
|
792
972
|
outputData.uaaClientId = authConfig.uaaClientId;
|
|
793
973
|
}
|
|
794
974
|
if (authConfig?.uaaClientSecret) {
|
|
795
|
-
|
|
975
|
+
if (!stripClientSecret) {
|
|
976
|
+
outputData.uaaClientSecret = authConfig.uaaClientSecret;
|
|
977
|
+
}
|
|
796
978
|
}
|
|
797
979
|
const outputDir = path.dirname(resolvedOutputPath);
|
|
798
980
|
if (!fs.existsSync(outputDir)) {
|
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.6",
|
|
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",
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
"build:fast": "tsc -p tsconfig.json && tsc -p tsconfig.cli.json",
|
|
46
46
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
47
47
|
"test:check": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
48
|
+
"test:mcp-auth": "sh tests/sso-demo/run-mcp-auth.sh",
|
|
49
|
+
"test:mcp-sso": "sh tests/sso-demo/run-mcp-sso.sh",
|
|
50
|
+
"test:device-code": "sh tests/keycloak/run-oidc.sh",
|
|
51
|
+
"test:saml-pure": "sh tests/keycloak/run-saml.sh",
|
|
52
|
+
"test:sso": "sh tests/keycloak/run-interactive.sh",
|
|
48
53
|
"prepublishOnly": "npm run build",
|
|
49
54
|
"prepack": "npm run build",
|
|
50
55
|
"generate-env": "tsx bin/generate-env-from-service-key.ts"
|
|
@@ -57,8 +62,8 @@
|
|
|
57
62
|
"node": ">=18.0.0"
|
|
58
63
|
},
|
|
59
64
|
"dependencies": {
|
|
60
|
-
"@mcp-abap-adt/auth-providers": "^1.0.
|
|
61
|
-
"@mcp-abap-adt/auth-stores": "^1.0.
|
|
65
|
+
"@mcp-abap-adt/auth-providers": "^1.0.5",
|
|
66
|
+
"@mcp-abap-adt/auth-stores": "^1.0.2",
|
|
62
67
|
"@mcp-abap-adt/interfaces": "^2.3.0",
|
|
63
68
|
"axios": "^1.13.5",
|
|
64
69
|
"tsx": "^4.21.0"
|