@mcp-abap-adt/auth-broker 1.0.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/README.md +59 -17
- package/dist/bin/mcp-auth.js +23 -6
- package/dist/bin/mcp-sso.js +15 -202
- package/dist/bin/mcpSsoConfig.js +398 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,89 @@ Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the co
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [2.0.0] - 2026-08-02
|
|
15
|
+
|
|
16
|
+
### Breaking Changes
|
|
17
|
+
|
|
18
|
+
**⚠️ The default OAuth callback port for `mcp-auth` and `mcp-sso` moves from `3001` to
|
|
19
|
+
whatever `@mcp-abap-adt/auth-providers` supplies (currently `61001`).** `3001` was never a
|
|
20
|
+
default this package chose: neither CLI listens on a port of its own — `auth-broker` is a
|
|
21
|
+
library plus these two CLIs, not a server — so `3001` only ever arrived by accident, carried in
|
|
22
|
+
from inside the token-provider library's old internal default. That default was itself moved,
|
|
23
|
+
deliberately, in `auth-providers` 2.0.0, to sit above the ephemeral port range and clear of the
|
|
24
|
+
`3001`/`3333` range application servers and proxies typically use. **If you registered
|
|
25
|
+
`http://localhost:3001/callback` (or any other specific port) with your identity provider, pass
|
|
26
|
+
`--redirect-port <that-port>` explicitly from now on** — omitting it no longer lands on `3001`.
|
|
27
|
+
|
|
28
|
+
- Requires `@mcp-abap-adt/auth-providers` `^2.0.0` (was `^1.2.0`). Every provider config's
|
|
29
|
+
`browser`/`redirectPort` fields are gone; how a login is conducted is now an
|
|
30
|
+
`IAuthorizationStrategy` passed as `authorization`. `AuthBroker` itself never imported those
|
|
31
|
+
fields — the exposure was entirely in the `mcp-auth`/`mcp-sso` CLIs and
|
|
32
|
+
`generate-env-from-service-key` (`bin/`), which construct providers directly.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- **`mcp-auth`, `mcp-sso`, `npm run generate-env`**: every browser-opening flow (`mcp-auth`'s
|
|
36
|
+
default `authorization_code` flow, `mcp-sso oidc --flow browser`, `mcp-sso saml2`/`bearer`
|
|
37
|
+
with the default browser assertion flow) now routes `--browser` and `--redirect-port` into
|
|
38
|
+
`browserCallbackStrategy` / `oidcCallbackStrategy` / `samlCallbackStrategy` instead of a
|
|
39
|
+
removed provider field. `--redirect-port` still overrides the port when given; see Breaking
|
|
40
|
+
Changes above for what happens when it is omitted.
|
|
41
|
+
|
|
42
|
+
- **Login timeout for these three CLIs raised from 30s to 5 minutes.** 30s was the token
|
|
43
|
+
provider's own default, sized for an unattended caller. These CLIs hand the login to a person
|
|
44
|
+
at a keyboard, who needs time to reach the browser, authenticate — possibly through SSO/MFA —
|
|
45
|
+
and get redirected back.
|
|
46
|
+
|
|
47
|
+
- `mcp-sso oidc --flow browser --code <value>`: manual/OOB code paste now goes through the
|
|
48
|
+
library's `staticCodeStrategy` (adapted with `asOidcResult`) instead of a removed
|
|
49
|
+
`authorizationCode` field. Behaviour for this combination is unchanged.
|
|
50
|
+
|
|
51
|
+
- `--redirect-uri` now only takes effect on the manual/static (OOB) code path. Combining it
|
|
52
|
+
with the listening browser flow is no longer meaningful: the local callback server always
|
|
53
|
+
derives its own redirect URI from `--redirect-port`.
|
|
54
|
+
|
|
55
|
+
- OIDC `device`/`password`/`token_exchange` flows never accepted `--browser`/`--redirect-port`
|
|
56
|
+
in the underlying library, before or after this change — `mcp-sso` was building them into the
|
|
57
|
+
config regardless, where they were silently ignored (masked by a cast, see below). They are
|
|
58
|
+
no longer passed at all; passing them for these flows now has visibly no effect, same as it
|
|
59
|
+
always did.
|
|
60
|
+
|
|
61
|
+
- `mcp-sso`'s per-flow config builders now construct the real `@mcp-abap-adt/auth-providers`
|
|
62
|
+
config types directly (`OidcBrowserProviderConfig`, `Saml2BearerProviderConfig`, etc.)
|
|
63
|
+
instead of building an untyped bag and force-casting it with `as unknown as X`. That cast is
|
|
64
|
+
exactly what let the `browser`/`redirectPort` regression above compile silently. Required
|
|
65
|
+
fields (`--client-id`, `--idp-sso-url`, `--sp-entity-id`, `--subject-token`,
|
|
66
|
+
`--username`/`--password`) are now checked and reported per flow instead of reaching the
|
|
67
|
+
provider as `undefined` behind the cast.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
- **`mcp-sso --config <path.json>` now reaches the strategy-building code at all.** Until this
|
|
71
|
+
fix, `buildProviderConfig` only built a config when `--protocol`/`--flow` were given on the
|
|
72
|
+
command line; a run driven by `--config` alone (the documented, supported way to use this flag)
|
|
73
|
+
left the file's fields completely untouched, and they reached `SsoProviderFactory.create()`
|
|
74
|
+
exactly as written — including a `browser`, `redirectPort`, `authorizationCode`, or
|
|
75
|
+
`assertionFlow` a 2.0.0 provider no longer accepts directly, with no error and no warning. The
|
|
76
|
+
merge between the file and CLI flags now happens once, before any validation or strategy
|
|
77
|
+
building, so `--config` alone, `--protocol`/`--flow` alone, and any combination of the two all
|
|
78
|
+
go through the same code. `--protocol`/`--flow`/CLI option flags still override the
|
|
79
|
+
corresponding value from the file. Every field a pre-2.0.0 config could carry either converts
|
|
80
|
+
onto the strategy (`browser`, `redirectPort`, `authorizationCode`, `assertionFlow`) or the run
|
|
81
|
+
is refused with a message naming the replacement (`authorizationCodeProvider`,
|
|
82
|
+
`assertionProvider`, `manualInput` — all function-valued, so a JSON file could never carry a
|
|
83
|
+
working one regardless).
|
|
84
|
+
- Relatedly, required-field validation (`--client-id`, `--idp-sso-url`, etc.) now runs against
|
|
85
|
+
the *merged* result. Previously it ran only against CLI options before the file was merged in,
|
|
86
|
+
so `--config file-with-credentials.json --protocol oidc --flow browser --redirect-port 3001`
|
|
87
|
+
demanded credentials that were already in the file.
|
|
88
|
+
- `mcp-sso saml2`/`bearer --assertion-flow manual`, and an `--assertion-flow assertion` with no
|
|
89
|
+
`--assertion` value supplied, now announce the authorization URL before prompting for a pasted
|
|
90
|
+
`SAMLResponse` (via `manualSamlResponseStrategy`). The old fallback for the latter combination
|
|
91
|
+
prompted without ever showing the URL the user needed to visit.
|
|
92
|
+
|
|
93
|
+
### Notes
|
|
94
|
+
- `AuthBroker`'s own constructor and public API are unchanged; this is a realignment with the
|
|
95
|
+
provider library plus the CLI-level fixes above.
|
|
96
|
+
|
|
14
97
|
## [1.0.8] - 2026-07-29
|
|
15
98
|
|
|
16
99
|
### Changed
|
package/README.md
CHANGED
|
@@ -26,13 +26,16 @@ AuthBroker requires a token provider configured for the destination:
|
|
|
26
26
|
|
|
27
27
|
```typescript
|
|
28
28
|
import { AuthBroker, AbapSessionStore } from '@mcp-abap-adt/auth-broker';
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
AuthorizationCodeProvider,
|
|
31
|
+
browserCallbackStrategy,
|
|
32
|
+
} from '@mcp-abap-adt/auth-providers';
|
|
30
33
|
|
|
31
34
|
const tokenProvider = new AuthorizationCodeProvider({
|
|
32
35
|
uaaUrl: 'https://...authentication...hana.ondemand.com',
|
|
33
36
|
clientId: '...',
|
|
34
37
|
clientSecret: '...',
|
|
35
|
-
browser: 'system',
|
|
38
|
+
authorization: browserCallbackStrategy({ browser: 'system' }),
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
const broker = new AuthBroker({
|
|
@@ -53,7 +56,10 @@ import {
|
|
|
53
56
|
AbapServiceKeyStore,
|
|
54
57
|
AbapSessionStore,
|
|
55
58
|
} from '@mcp-abap-adt/auth-broker';
|
|
56
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
AuthorizationCodeProvider,
|
|
61
|
+
browserCallbackStrategy,
|
|
62
|
+
} from '@mcp-abap-adt/auth-providers';
|
|
57
63
|
|
|
58
64
|
const broker = new AuthBroker({
|
|
59
65
|
sessionStore: new AbapSessionStore('/path/to/destinations'),
|
|
@@ -62,7 +68,7 @@ const broker = new AuthBroker({
|
|
|
62
68
|
uaaUrl: 'https://...authentication...hana.ondemand.com',
|
|
63
69
|
clientId: '...',
|
|
64
70
|
clientSecret: '...',
|
|
65
|
-
browser: 'system',
|
|
71
|
+
authorization: browserCallbackStrategy({ browser: 'system' }),
|
|
66
72
|
}),
|
|
67
73
|
}, 'chrome', logger);
|
|
68
74
|
|
|
@@ -74,7 +80,7 @@ const brokerNoBrowser = new AuthBroker({
|
|
|
74
80
|
uaaUrl: 'https://...authentication...hana.ondemand.com',
|
|
75
81
|
clientId: '...',
|
|
76
82
|
clientSecret: '...',
|
|
77
|
-
browser: 'none',
|
|
83
|
+
authorization: browserCallbackStrategy({ browser: 'none' }),
|
|
78
84
|
}),
|
|
79
85
|
allowBrowserAuth: false, // Throws BROWSER_AUTH_REQUIRED if browser auth needed
|
|
80
86
|
}, 'chrome', logger);
|
|
@@ -92,7 +98,7 @@ const broker = new AuthBroker({
|
|
|
92
98
|
uaaUrl: 'https://...authentication...hana.ondemand.com',
|
|
93
99
|
clientId: '...',
|
|
94
100
|
clientSecret: '...',
|
|
95
|
-
browser: 'system',
|
|
101
|
+
authorization: browserCallbackStrategy({ browser: 'system' }),
|
|
96
102
|
}),
|
|
97
103
|
});
|
|
98
104
|
```
|
|
@@ -111,7 +117,13 @@ const broker = new AuthBroker({
|
|
|
111
117
|
|
|
112
118
|
### Custom Browser Auth Port
|
|
113
119
|
|
|
114
|
-
|
|
120
|
+
How a login is conducted — including which port the local OAuth2 callback
|
|
121
|
+
listens on — is an `IAuthorizationStrategy` passed as `authorization`, not a
|
|
122
|
+
field on the provider config. `browserCallbackStrategy` from
|
|
123
|
+
`@mcp-abap-adt/auth-providers` builds the ready-made one; its default port is
|
|
124
|
+
`61001`, chosen to sit well clear of the range application servers and
|
|
125
|
+
proxies typically use (e.g. `3001`/`3333`). Pass `port` to avoid conflicts
|
|
126
|
+
with a specific redirect URI registered at the identity provider:
|
|
115
127
|
|
|
116
128
|
```typescript
|
|
117
129
|
const broker = new AuthBroker({
|
|
@@ -121,8 +133,7 @@ const broker = new AuthBroker({
|
|
|
121
133
|
uaaUrl: 'https://...authentication...hana.ondemand.com',
|
|
122
134
|
clientId: '...',
|
|
123
135
|
clientSecret: '...',
|
|
124
|
-
browser: 'system',
|
|
125
|
-
redirectPort: 4001,
|
|
136
|
+
authorization: browserCallbackStrategy({ browser: 'system', port: 4001 }),
|
|
126
137
|
}),
|
|
127
138
|
}, 'chrome');
|
|
128
139
|
```
|
|
@@ -572,10 +583,13 @@ The package uses the `ITokenProvider` interface for token acquisition. Provider
|
|
|
572
583
|
- No refresh token provided
|
|
573
584
|
|
|
574
585
|
- **`AuthorizationCodeProvider`** - For BTP/ABAP authentication (full scope)
|
|
575
|
-
-
|
|
576
|
-
|
|
577
|
-
-
|
|
578
|
-
|
|
586
|
+
- How the login is conducted is an `authorization?: IAuthorizationStrategy<string>`, not a
|
|
587
|
+
provider field. Omitted, it defaults to a browser callback on port `61001`
|
|
588
|
+
- `browserCallbackStrategy({ browser?, port?, timeoutMs? })` from `@mcp-abap-adt/auth-providers`
|
|
589
|
+
builds the ready-made strategy; pass `port` to avoid conflicts when running alongside other
|
|
590
|
+
services (e.g. a proxy server) or to match a redirect URI registered at the identity provider
|
|
591
|
+
- The callback port is held only for the duration of a login and released when it ends —
|
|
592
|
+
by success, failure, timeout, or cancellation
|
|
579
593
|
- Uses browser-based OAuth2 flow (if no refresh token)
|
|
580
594
|
- Uses refresh token if available
|
|
581
595
|
- Provides refresh token for future use
|
|
@@ -593,6 +607,7 @@ import {
|
|
|
593
607
|
import {
|
|
594
608
|
ClientCredentialsProvider,
|
|
595
609
|
AuthorizationCodeProvider,
|
|
610
|
+
browserCallbackStrategy,
|
|
596
611
|
} from '@mcp-abap-adt/auth-providers';
|
|
597
612
|
|
|
598
613
|
// XSUAA authentication
|
|
@@ -623,7 +638,7 @@ const btpBroker = new AuthBroker({
|
|
|
623
638
|
uaaUrl: 'https://auth.example.com',
|
|
624
639
|
clientId: '...',
|
|
625
640
|
clientSecret: '...',
|
|
626
|
-
browser: 'system',
|
|
641
|
+
authorization: browserCallbackStrategy({ browser: 'system' }),
|
|
627
642
|
}),
|
|
628
643
|
});
|
|
629
644
|
|
|
@@ -635,7 +650,7 @@ const btpBrokerFull = new AuthBroker({
|
|
|
635
650
|
uaaUrl: 'https://auth.example.com',
|
|
636
651
|
clientId: '...',
|
|
637
652
|
clientSecret: '...',
|
|
638
|
-
browser: 'system',
|
|
653
|
+
authorization: browserCallbackStrategy({ browser: 'system' }),
|
|
639
654
|
}),
|
|
640
655
|
});
|
|
641
656
|
```
|
|
@@ -660,6 +675,14 @@ mcp-auth --service-key <path> --output <path> [--env <path>] [--type abap|xsuaa]
|
|
|
660
675
|
- `none`: Show URL in console and wait for callback (no browser)
|
|
661
676
|
- `system/chrome/edge/firefox`: Open specific browser
|
|
662
677
|
|
|
678
|
+
`--browser` and `--redirect-port` are routed into `browserCallbackStrategy`. This CLI has no
|
|
679
|
+
default of its own for the callback port: `--redirect-port` overrides it when given; omitted,
|
|
680
|
+
the port comes from `auth-providers` (currently `61001`, chosen to sit above the ephemeral range
|
|
681
|
+
and clear of the `3001`/`3333` range servers and proxies typically use). If you registered a
|
|
682
|
+
redirect URI with a specific port at your identity provider, pass `--redirect-port` to match it.
|
|
683
|
+
A login is given 5 minutes to complete (this is a person switching to a browser and signing in
|
|
684
|
+
by hand, not an unattended caller).
|
|
685
|
+
|
|
663
686
|
**Examples:**
|
|
664
687
|
```bash
|
|
665
688
|
# Auth code (default via service key)
|
|
@@ -703,12 +726,22 @@ mcp-sso --protocol <oidc|saml2> --flow <flow> --output <path> [--type abap|xsuaa
|
|
|
703
726
|
- OIDC: `browser`, `device`, `password`, `token_exchange`
|
|
704
727
|
- SAML2: `bearer`, `pure`
|
|
705
728
|
|
|
729
|
+
Only the flows that actually open a browser (OIDC `browser`; SAML2 `bearer`/`pure` with the
|
|
730
|
+
default `--assertion-flow browser`) use `--browser` and `--redirect-port` — they are routed
|
|
731
|
+
into `browserCallbackStrategy`/`oidcCallbackStrategy`/`samlCallbackStrategy`. This CLI has no
|
|
732
|
+
default of its own for the callback port: `--redirect-port` overrides it when given; omitted,
|
|
733
|
+
the port comes from `auth-providers` (currently `61001`). A login is given 5 minutes to
|
|
734
|
+
complete. `device`, `password`, and `token_exchange` never open a browser from this process, so
|
|
735
|
+
`--browser`/`--redirect-port` have no effect for them. This applies the same way whether
|
|
736
|
+
`--protocol`/`--flow` come from CLI flags or from `--config` (below) — a field's origin doesn't
|
|
737
|
+
change how it's handled, and CLI flags always take precedence over the same field in a file.
|
|
738
|
+
|
|
706
739
|
**Examples:**
|
|
707
740
|
```bash
|
|
708
741
|
# OIDC browser flow
|
|
709
742
|
mcp-sso oidc --flow browser --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
|
|
710
743
|
|
|
711
|
-
# OIDC browser flow (manual code / OOB)
|
|
744
|
+
# OIDC browser flow (manual code / OOB — no callback server is opened for this combination)
|
|
712
745
|
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
|
|
713
746
|
|
|
714
747
|
# OIDC device flow
|
|
@@ -765,7 +798,8 @@ It enables `authorization_code` and `saml2-bearer` grant types and provides a
|
|
|
765
798
|
simple `CatalogService`. See `tests/sso-demo/readme.md` for deploy steps.
|
|
766
799
|
|
|
767
800
|
**Config file:**
|
|
768
|
-
You can pass a JSON file with provider config
|
|
801
|
+
You can pass a JSON file with provider config instead of (or alongside) `--protocol`/`--flow`
|
|
802
|
+
and the OIDC/SAML flags — `--config` alone is enough to run a flow, with no other flags required:
|
|
769
803
|
|
|
770
804
|
```json
|
|
771
805
|
{
|
|
@@ -777,6 +811,14 @@ You can pass a JSON file with provider config:
|
|
|
777
811
|
}
|
|
778
812
|
```
|
|
779
813
|
|
|
814
|
+
Any CLI flag given alongside `--config` overrides the same field in the file; a field the file
|
|
815
|
+
sets and no flag overrides is used as-is. A file written for a pre-2.0.0 config still works:
|
|
816
|
+
`browser` and `redirectPort` are routed into the strategy exactly as the equivalent CLI flags
|
|
817
|
+
are, and `authorizationCode`/`assertionFlow` are honored the same way `--code`/`--assertion-flow`
|
|
818
|
+
are. A file that sets `authorizationCodeProvider`, `assertionProvider`, or `manualInput` — all
|
|
819
|
+
functions, which JSON cannot express — is refused with an error naming the CLI flag to use
|
|
820
|
+
instead, rather than having the field silently dropped.
|
|
821
|
+
|
|
780
822
|
### Utility Script
|
|
781
823
|
|
|
782
824
|
Generate `.env` files from service keys:
|
package/dist/bin/mcp-auth.js
CHANGED
|
@@ -61,6 +61,11 @@ const distPath = path.resolve(__dirname, '..', 'index.js');
|
|
|
61
61
|
const { AuthBroker } = require(distPath);
|
|
62
62
|
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
63
63
|
const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
|
|
64
|
+
/**
|
|
65
|
+
* A person completes this login at a browser; the provider's own default
|
|
66
|
+
* (30s) is sized for an unattended caller instead.
|
|
67
|
+
*/
|
|
68
|
+
const INTERACTIVE_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
64
69
|
function getVersion() {
|
|
65
70
|
try {
|
|
66
71
|
const candidates = [
|
|
@@ -108,7 +113,8 @@ function showHelp() {
|
|
|
108
113
|
console.log(' - system/chrome/edge/firefox: Open specific browser');
|
|
109
114
|
console.log(' --format <format> Output format: json or env (default: env)');
|
|
110
115
|
console.log(' --service-url <url> Service URL (SAP URL for ABAP, MCP URL for XSUAA). For XSUAA, optional.');
|
|
111
|
-
console.log(' --redirect-port <port> Port for OAuth redirect URI (default:
|
|
116
|
+
console.log(' --redirect-port <port> Port for OAuth redirect URI (default: from auth-providers, currently 61001).');
|
|
117
|
+
console.log(' Must match XSUAA redirect-uris config.');
|
|
112
118
|
console.log('');
|
|
113
119
|
console.log(' --version, -v Show version number');
|
|
114
120
|
console.log(' --help, -h Show this help message');
|
|
@@ -159,9 +165,9 @@ function showHelp() {
|
|
|
159
165
|
console.log(' * none/headless: Show URL in console and wait for callback');
|
|
160
166
|
console.log(' * system/chrome/edge/firefox: Open specific browser');
|
|
161
167
|
console.log(' - Both providers (xsuaa and abap) support both flows');
|
|
162
|
-
console.log(' - --redirect-port: Port for OAuth redirect URI (default:
|
|
168
|
+
console.log(' - --redirect-port: Port for OAuth redirect URI (default: from auth-providers, currently 61001)');
|
|
163
169
|
console.log(' * Must match redirect_uri configured in XSUAA/ABAP OAuth2 settings');
|
|
164
|
-
console.log(' *
|
|
170
|
+
console.log(' * If your registered redirect URI uses a different port (e.g. 3001, 8080), pass it explicitly');
|
|
165
171
|
console.log(' - For XSUAA, serviceUrl (MCP URL) is optional - can be provided via --service-url or service key');
|
|
166
172
|
console.log(' - For ABAP, serviceUrl (SAP URL) is required - can be provided via --service-url or service key');
|
|
167
173
|
console.log(' - SAP_URL/XSUAA_MCP_URL is written to .env (from --service-url, service key, or placeholder)');
|
|
@@ -599,7 +605,12 @@ async function main() {
|
|
|
599
605
|
}
|
|
600
606
|
// Default: authorization_code flow with browser
|
|
601
607
|
// --credential: client_credentials flow (no browser needed)
|
|
602
|
-
|
|
608
|
+
// The callback port is the provider's own choice (DEFAULT_CALLBACK_PORT,
|
|
609
|
+
// currently 61001) unless the user overrides it with --redirect-port.
|
|
610
|
+
// Only resolved to a concrete number here for the URL preview below —
|
|
611
|
+
// the strategy itself re-derives the same default independently when
|
|
612
|
+
// `port` is left undefined.
|
|
613
|
+
const redirectPort = options.redirectPort ?? auth_providers_1.DEFAULT_CALLBACK_PORT;
|
|
603
614
|
// Log authorization URL for debugging
|
|
604
615
|
if (!options.credential) {
|
|
605
616
|
const redirectUri = `http://localhost:${redirectPort}/callback`;
|
|
@@ -618,8 +629,14 @@ async function main() {
|
|
|
618
629
|
clientId: authConfig.uaaClientId,
|
|
619
630
|
clientSecret: authConfig.uaaClientSecret,
|
|
620
631
|
refreshToken: authConfig.refreshToken,
|
|
621
|
-
|
|
622
|
-
|
|
632
|
+
// Passing `options.redirectPort` (not the resolved `redirectPort`
|
|
633
|
+
// above) so an omitted --redirect-port lets the strategy bind its
|
|
634
|
+
// own default rather than this CLI pinning a number it doesn't own.
|
|
635
|
+
authorization: (0, auth_providers_1.browserCallbackStrategy)({
|
|
636
|
+
browser: options.browser,
|
|
637
|
+
port: options.redirectPort,
|
|
638
|
+
timeoutMs: INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
639
|
+
}),
|
|
623
640
|
});
|
|
624
641
|
const broker = new AuthBroker({
|
|
625
642
|
sessionStore,
|
package/dist/bin/mcp-sso.js
CHANGED
|
@@ -60,15 +60,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
60
60
|
};
|
|
61
61
|
})();
|
|
62
62
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
63
|
-
const node_readline_1 = require("node:readline");
|
|
64
63
|
const fs = __importStar(require("fs"));
|
|
65
64
|
const path = __importStar(require("path"));
|
|
66
65
|
// Use require for CommonJS dist files with absolute path
|
|
67
66
|
const distPath = path.resolve(__dirname, '..', 'index.js');
|
|
68
67
|
const { AuthBroker } = require(distPath);
|
|
69
|
-
const logger_1 = require("@mcp-abap-adt/logger");
|
|
70
68
|
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
71
69
|
const auth_stores_1 = require("@mcp-abap-adt/auth-stores");
|
|
70
|
+
const logger_1 = require("@mcp-abap-adt/logger");
|
|
71
|
+
const mcpSsoConfig_1 = require("./mcpSsoConfig");
|
|
72
72
|
function getVersion() {
|
|
73
73
|
try {
|
|
74
74
|
const candidates = [
|
|
@@ -112,7 +112,7 @@ function showHelp() {
|
|
|
112
112
|
console.log(' --service-url <url> Service URL (ABAP: SAP URL, XSUAA: MCP URL)');
|
|
113
113
|
console.log(' --config <path> JSON config file (SSO provider config)');
|
|
114
114
|
console.log(' --browser <browser> Browser: auto|none|system|chrome|edge|firefox');
|
|
115
|
-
console.log(' --redirect-port <port> Redirect port for browser flows (default:
|
|
115
|
+
console.log(' --redirect-port <port> Redirect port for browser flows (default: from auth-providers, currently 61001)');
|
|
116
116
|
console.log(' --redirect-uri <uri> Custom redirect URI (OOB/manual code flows)');
|
|
117
117
|
console.log('');
|
|
118
118
|
console.log('OIDC Options:');
|
|
@@ -158,18 +158,6 @@ function parseScopes(value) {
|
|
|
158
158
|
.filter(Boolean);
|
|
159
159
|
return parts.length > 0 ? parts : undefined;
|
|
160
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
161
|
function createCliLogger(prefix = 'SSO') {
|
|
174
162
|
const isEnabled = () => {
|
|
175
163
|
if (process.env.DEBUG_SSO === 'false' ||
|
|
@@ -520,190 +508,6 @@ function resolveSamlTokenAlias(metadataXml) {
|
|
|
520
508
|
const match = metadataXml.match(regex);
|
|
521
509
|
return match?.[1];
|
|
522
510
|
}
|
|
523
|
-
function normalizeProviderConfig(raw) {
|
|
524
|
-
if (!raw || typeof raw !== 'object') {
|
|
525
|
-
return null;
|
|
526
|
-
}
|
|
527
|
-
if (raw.provider) {
|
|
528
|
-
return raw.provider;
|
|
529
|
-
}
|
|
530
|
-
if (raw.protocol && raw.flow) {
|
|
531
|
-
const { protocol, flow, config, ...rest } = raw;
|
|
532
|
-
return {
|
|
533
|
-
protocol,
|
|
534
|
-
flow,
|
|
535
|
-
config: config ?? rest,
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
540
|
-
function mergeConfig(target, source) {
|
|
541
|
-
const result = { ...target };
|
|
542
|
-
for (const [key, value] of Object.entries(source)) {
|
|
543
|
-
if (value !== undefined) {
|
|
544
|
-
result[key] = value;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return result;
|
|
548
|
-
}
|
|
549
|
-
function buildOidcConfig(options) {
|
|
550
|
-
const scopeList = options.scopes;
|
|
551
|
-
const tokenEndpoint = options.tokenEndpoint ||
|
|
552
|
-
(options.uaaUrl
|
|
553
|
-
? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
|
|
554
|
-
: undefined);
|
|
555
|
-
const passcode = options.passcode;
|
|
556
|
-
const username = options.username || (passcode ? 'passcode' : undefined);
|
|
557
|
-
const password = options.password || passcode;
|
|
558
|
-
return {
|
|
559
|
-
issuerUrl: options.issuerUrl,
|
|
560
|
-
authorizationEndpoint: options.authorizationEndpoint,
|
|
561
|
-
tokenEndpoint,
|
|
562
|
-
deviceAuthorizationEndpoint: options.deviceAuthorizationEndpoint,
|
|
563
|
-
clientId: options.clientId,
|
|
564
|
-
clientSecret: options.clientSecret,
|
|
565
|
-
scopes: scopeList,
|
|
566
|
-
scope: options.scope,
|
|
567
|
-
authorizationCode: options.code,
|
|
568
|
-
username,
|
|
569
|
-
password,
|
|
570
|
-
subjectToken: options.subjectToken,
|
|
571
|
-
subjectTokenType: options.subjectTokenType ||
|
|
572
|
-
'urn:ietf:params:oauth:token-type:access_token',
|
|
573
|
-
audience: options.audience,
|
|
574
|
-
actorToken: options.actorToken,
|
|
575
|
-
actorTokenType: options.actorTokenType,
|
|
576
|
-
browser: options.browser,
|
|
577
|
-
redirectPort: options.redirectPort,
|
|
578
|
-
redirectUri: options.redirectUri,
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
function buildSamlConfig(options) {
|
|
582
|
-
const assertionFlow = options.assertionFlow || (options.assertion ? 'assertion' : 'browser');
|
|
583
|
-
const tokenUrl = options.tokenEndpoint ||
|
|
584
|
-
(options.uaaUrl
|
|
585
|
-
? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
|
|
586
|
-
: undefined);
|
|
587
|
-
const assertionProvider = options.assertion
|
|
588
|
-
? async () => options.assertion
|
|
589
|
-
: assertionFlow === 'assertion'
|
|
590
|
-
? async () => readManualInput('Paste SAMLResponse: ')
|
|
591
|
-
: undefined;
|
|
592
|
-
const cookieProvider = async (samlResponse) => {
|
|
593
|
-
if (options.cookie) {
|
|
594
|
-
return options.cookie;
|
|
595
|
-
}
|
|
596
|
-
if (assertionFlow === 'assertion') {
|
|
597
|
-
return `SAMLResponse=${samlResponse}`;
|
|
598
|
-
}
|
|
599
|
-
return readManualInput('Paste session cookies: ');
|
|
600
|
-
};
|
|
601
|
-
return {
|
|
602
|
-
idpSsoUrl: options.idpSsoUrl,
|
|
603
|
-
spEntityId: options.spEntityId,
|
|
604
|
-
acsUrl: options.acsUrl,
|
|
605
|
-
relayState: options.relayState,
|
|
606
|
-
assertionFlow,
|
|
607
|
-
assertionProvider,
|
|
608
|
-
tokenUrl,
|
|
609
|
-
uaaUrl: options.uaaUrl,
|
|
610
|
-
clientId: options.clientId,
|
|
611
|
-
clientSecret: options.clientSecret,
|
|
612
|
-
browser: options.browser,
|
|
613
|
-
redirectPort: options.redirectPort,
|
|
614
|
-
cookieProvider,
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
function buildProviderConfig(options, existingAuth, existingConn, fileConfig) {
|
|
618
|
-
let configFromCli = null;
|
|
619
|
-
if (options.protocol && options.flow) {
|
|
620
|
-
if (options.protocol === 'oidc') {
|
|
621
|
-
switch (options.flow) {
|
|
622
|
-
case 'browser':
|
|
623
|
-
configFromCli = {
|
|
624
|
-
protocol: 'oidc',
|
|
625
|
-
flow: 'browser',
|
|
626
|
-
config: buildOidcConfig(options),
|
|
627
|
-
};
|
|
628
|
-
break;
|
|
629
|
-
case 'device':
|
|
630
|
-
configFromCli = {
|
|
631
|
-
protocol: 'oidc',
|
|
632
|
-
flow: 'device',
|
|
633
|
-
config: buildOidcConfig(options),
|
|
634
|
-
};
|
|
635
|
-
break;
|
|
636
|
-
case 'password':
|
|
637
|
-
configFromCli = {
|
|
638
|
-
protocol: 'oidc',
|
|
639
|
-
flow: 'password',
|
|
640
|
-
config: buildOidcConfig(options),
|
|
641
|
-
};
|
|
642
|
-
break;
|
|
643
|
-
case 'token_exchange':
|
|
644
|
-
configFromCli = {
|
|
645
|
-
protocol: 'oidc',
|
|
646
|
-
flow: 'token_exchange',
|
|
647
|
-
config: buildOidcConfig(options),
|
|
648
|
-
};
|
|
649
|
-
break;
|
|
650
|
-
default:
|
|
651
|
-
throw new Error(`Unsupported OIDC flow: ${options.flow}`);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
else if (options.protocol === 'saml2') {
|
|
655
|
-
switch (options.flow) {
|
|
656
|
-
case 'bearer':
|
|
657
|
-
configFromCli = {
|
|
658
|
-
protocol: 'saml2',
|
|
659
|
-
flow: 'bearer',
|
|
660
|
-
config: buildSamlConfig(options),
|
|
661
|
-
};
|
|
662
|
-
break;
|
|
663
|
-
case 'pure':
|
|
664
|
-
configFromCli = {
|
|
665
|
-
protocol: 'saml2',
|
|
666
|
-
flow: 'pure',
|
|
667
|
-
config: buildSamlConfig(options),
|
|
668
|
-
};
|
|
669
|
-
break;
|
|
670
|
-
default:
|
|
671
|
-
throw new Error(`Unsupported SAML flow: ${options.flow}`);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
const base = fileConfig ?? configFromCli;
|
|
676
|
-
if (!base) {
|
|
677
|
-
throw new Error('Provider config is missing. Use --config or --protocol/--flow options.');
|
|
678
|
-
}
|
|
679
|
-
let result = base;
|
|
680
|
-
if (options.protocol) {
|
|
681
|
-
result = { ...result, protocol: options.protocol };
|
|
682
|
-
}
|
|
683
|
-
if (options.flow) {
|
|
684
|
-
result = { ...result, flow: options.flow };
|
|
685
|
-
}
|
|
686
|
-
if (configFromCli) {
|
|
687
|
-
result = {
|
|
688
|
-
...result,
|
|
689
|
-
config: mergeConfig(result.config || {}, configFromCli.config || {}),
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
const accessToken = existingConn?.authorizationToken;
|
|
693
|
-
const refreshToken = existingAuth?.refreshToken;
|
|
694
|
-
if ((result.protocol === 'oidc' ||
|
|
695
|
-
(result.protocol === 'saml2' && result.flow === 'bearer')) &&
|
|
696
|
-
(accessToken || refreshToken)) {
|
|
697
|
-
result = {
|
|
698
|
-
...result,
|
|
699
|
-
config: mergeConfig(result.config || {}, {
|
|
700
|
-
accessToken,
|
|
701
|
-
refreshToken,
|
|
702
|
-
}),
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
return result;
|
|
706
|
-
}
|
|
707
511
|
async function main() {
|
|
708
512
|
const options = parseArgs();
|
|
709
513
|
if (!options) {
|
|
@@ -768,12 +572,21 @@ async function main() {
|
|
|
768
572
|
process.exit(1);
|
|
769
573
|
}
|
|
770
574
|
const raw = JSON.parse(fs.readFileSync(resolvedConfigPath, 'utf8'));
|
|
771
|
-
providerConfigFromFile = normalizeProviderConfig(raw);
|
|
575
|
+
providerConfigFromFile = (0, mcpSsoConfig_1.normalizeProviderConfig)(raw);
|
|
772
576
|
if (!providerConfigFromFile) {
|
|
773
577
|
console.error(`❌ Config file does not contain provider config`);
|
|
774
578
|
process.exit(1);
|
|
775
579
|
}
|
|
776
580
|
}
|
|
581
|
+
// Merge the file into `options` *before* anything downstream reads
|
|
582
|
+
// options.protocol/flow or builds a strategy from them — a run driven by
|
|
583
|
+
// --config alone must reach exactly the same validation and
|
|
584
|
+
// strategy-building code a --protocol/--flow run does, or a field the file
|
|
585
|
+
// carries (including a legacy one 2.0.0 removed) can reach the provider
|
|
586
|
+
// untouched with no error and no warning. CLI flags already parsed above
|
|
587
|
+
// are left alone; the file only fills what they didn't set. A no-op when
|
|
588
|
+
// --config wasn't given.
|
|
589
|
+
(0, mcpSsoConfig_1.applyFileConfig)(options, providerConfigFromFile);
|
|
777
590
|
if (options.serviceKeyPath) {
|
|
778
591
|
const resolvedServiceKeyPath = path.resolve(options.serviceKeyPath);
|
|
779
592
|
const serviceKeyDir = path.dirname(resolvedServiceKeyPath);
|
|
@@ -836,7 +649,7 @@ async function main() {
|
|
|
836
649
|
}
|
|
837
650
|
if (options.protocol === 'oidc' && options.flow === 'password') {
|
|
838
651
|
if (!options.passcode && !options.password) {
|
|
839
|
-
options.passcode = await readManualInput('Paste passcode: ');
|
|
652
|
+
options.passcode = await (0, mcpSsoConfig_1.readManualInput)('Paste passcode: ');
|
|
840
653
|
}
|
|
841
654
|
}
|
|
842
655
|
const tempSessionDir = path.join(path.dirname(resolvedOutputPath), '.tmp');
|
|
@@ -893,7 +706,7 @@ async function main() {
|
|
|
893
706
|
refreshToken: existingAuth?.refreshToken,
|
|
894
707
|
});
|
|
895
708
|
}
|
|
896
|
-
const providerConfig = buildProviderConfig(options, existingAuth, existingConn
|
|
709
|
+
const providerConfig = (0, mcpSsoConfig_1.buildProviderConfig)(options, existingAuth, existingConn);
|
|
897
710
|
const providerConfigWithLogger = providerConfig.config
|
|
898
711
|
? {
|
|
899
712
|
...providerConfig,
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure config-building logic for `mcp-sso`, split out from bin/mcp-sso.ts so
|
|
4
|
+
* it can be imported by tests directly.
|
|
5
|
+
*
|
|
6
|
+
* bin/mcp-sso.ts itself cannot be `import`ed safely: it `require()`s the
|
|
7
|
+
* compiled `dist/index.js` at module load time (assuming it is always run
|
|
8
|
+
* from `dist/bin/`) and calls `main()` at the bottom of the file. Neither is
|
|
9
|
+
* true when a test loads the TypeScript source directly, so anything that
|
|
10
|
+
* needs unit coverage — building an `SsoProviderConfig` from CLI options and
|
|
11
|
+
* an optional `--config` file, and merging the two — lives here instead.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.INTERACTIVE_LOGIN_TIMEOUT_MS = void 0;
|
|
15
|
+
exports.readManualInput = readManualInput;
|
|
16
|
+
exports.normalizeProviderConfig = normalizeProviderConfig;
|
|
17
|
+
exports.applyFileConfig = applyFileConfig;
|
|
18
|
+
exports.buildOidcBrowserConfig = buildOidcBrowserConfig;
|
|
19
|
+
exports.buildOidcDeviceConfig = buildOidcDeviceConfig;
|
|
20
|
+
exports.buildOidcPasswordConfig = buildOidcPasswordConfig;
|
|
21
|
+
exports.buildOidcTokenExchangeConfig = buildOidcTokenExchangeConfig;
|
|
22
|
+
exports.buildSamlBearerConfig = buildSamlBearerConfig;
|
|
23
|
+
exports.buildSamlPureConfig = buildSamlPureConfig;
|
|
24
|
+
exports.buildProviderConfig = buildProviderConfig;
|
|
25
|
+
const node_readline_1 = require("node:readline");
|
|
26
|
+
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
27
|
+
/**
|
|
28
|
+
* A person completes these logins at a browser; the library's own default
|
|
29
|
+
* (30s) is sized for an unattended caller instead.
|
|
30
|
+
*/
|
|
31
|
+
exports.INTERACTIVE_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
32
|
+
function readManualInput(prompt) {
|
|
33
|
+
const rl = (0, node_readline_1.createInterface)({
|
|
34
|
+
input: process.stdin,
|
|
35
|
+
output: process.stdout,
|
|
36
|
+
});
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
rl.question(prompt, (answer) => {
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve(answer.trim());
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function normalizeProviderConfig(raw) {
|
|
45
|
+
if (!raw || typeof raw !== 'object') {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (raw.provider) {
|
|
49
|
+
return raw.provider;
|
|
50
|
+
}
|
|
51
|
+
if (raw.protocol && raw.flow) {
|
|
52
|
+
const { protocol, flow, config, ...rest } = raw;
|
|
53
|
+
return {
|
|
54
|
+
protocol,
|
|
55
|
+
flow,
|
|
56
|
+
config: config ?? rest,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pre-2.0.0 provider config fields that named a JavaScript function
|
|
63
|
+
* (`() => Promise<string>` or similar) rather than a value. JSON cannot
|
|
64
|
+
* carry a function, so a `--config` file can never legitimately supply one —
|
|
65
|
+
* if a key like this shows up, the file is stale in a way no conversion can
|
|
66
|
+
* fix. Refuse rather than silently drop it, and say what to write instead.
|
|
67
|
+
*/
|
|
68
|
+
const UNSUPPORTED_LEGACY_CONFIG_FIELDS = {
|
|
69
|
+
authorizationCodeProvider: "'authorizationCodeProvider' (a function) cannot be expressed in a --config file. Use --code for a value you already hold, or drive the OIDC browser flow interactively without it.",
|
|
70
|
+
assertionProvider: "'assertionProvider' (a function) cannot be expressed in a --config file. Use --assertion for a value you already hold, or --assertion-flow to choose how it is obtained.",
|
|
71
|
+
manualInput: "'manualInput' (a function) cannot be expressed in a --config file. Use --assertion-flow manual to prompt on this CLI's own stdin instead.",
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* `--config` file fields that map 1:1 onto a `McpSsoOptions` field of the
|
|
75
|
+
* same name — every pre-2.0.0 config field the strategies still need, other
|
|
76
|
+
* than `authorizationCode` (see `applyFileConfig`), lands here.
|
|
77
|
+
*/
|
|
78
|
+
const CONFIG_BACKFILL_FIELDS = [
|
|
79
|
+
'browser',
|
|
80
|
+
'redirectPort',
|
|
81
|
+
'redirectUri',
|
|
82
|
+
'issuerUrl',
|
|
83
|
+
'authorizationEndpoint',
|
|
84
|
+
'tokenEndpoint',
|
|
85
|
+
'deviceAuthorizationEndpoint',
|
|
86
|
+
'clientId',
|
|
87
|
+
'clientSecret',
|
|
88
|
+
'scopes',
|
|
89
|
+
'scope',
|
|
90
|
+
'username',
|
|
91
|
+
'password',
|
|
92
|
+
'passcode',
|
|
93
|
+
'subjectToken',
|
|
94
|
+
'subjectTokenType',
|
|
95
|
+
'audience',
|
|
96
|
+
'actorToken',
|
|
97
|
+
'actorTokenType',
|
|
98
|
+
'idpSsoUrl',
|
|
99
|
+
'spEntityId',
|
|
100
|
+
'acsUrl',
|
|
101
|
+
'relayState',
|
|
102
|
+
'assertionFlow',
|
|
103
|
+
'assertion',
|
|
104
|
+
'cookie',
|
|
105
|
+
'uaaUrl',
|
|
106
|
+
];
|
|
107
|
+
/**
|
|
108
|
+
* Backfills `options` in place from a `--config` file's raw `config` object.
|
|
109
|
+
* CLI flags always win — a field already set on `options` is left alone; the
|
|
110
|
+
* file only fills gaps.
|
|
111
|
+
*
|
|
112
|
+
* This is what makes a config-file-only run (no --protocol/--flow on the
|
|
113
|
+
* CLI) reach the same strategy-building code as a CLI-flag run: without it,
|
|
114
|
+
* the file's fields — including a `browser`/`redirectPort` a 2.0.0 provider
|
|
115
|
+
* no longer accepts directly — would reach `SsoProviderFactory.create()`
|
|
116
|
+
* untouched and be silently ignored.
|
|
117
|
+
*
|
|
118
|
+
* Every field a pre-2.0.0 config could carry either lands on its
|
|
119
|
+
* `McpSsoOptions` equivalent here (which the strategy built from `options`
|
|
120
|
+
* then consumes) or the run is refused via `process.exit(1)` naming what to
|
|
121
|
+
* write instead. Nothing may reach the provider without one of those two
|
|
122
|
+
* happening.
|
|
123
|
+
*
|
|
124
|
+
* Also backfills `options.protocol`/`options.flow` from the file when the
|
|
125
|
+
* CLI didn't set them — a `--config`-only run (no `--protocol`/`--flow`
|
|
126
|
+
* flags at all) must still resolve to a real flow, or nothing downstream
|
|
127
|
+
* ever calls a builder at all.
|
|
128
|
+
*
|
|
129
|
+
* A no-op when `fileConfig` is `null` (no `--config` was given), so callers
|
|
130
|
+
* can invoke this unconditionally.
|
|
131
|
+
*/
|
|
132
|
+
function applyFileConfig(options, fileConfig) {
|
|
133
|
+
if (!fileConfig) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
options.protocol = options.protocol ?? fileConfig.protocol;
|
|
137
|
+
options.flow = options.flow ?? fileConfig.flow;
|
|
138
|
+
const fields = (fileConfig.config ?? {});
|
|
139
|
+
for (const [field, guidance] of Object.entries(UNSUPPORTED_LEGACY_CONFIG_FIELDS)) {
|
|
140
|
+
if (fields[field] !== undefined) {
|
|
141
|
+
console.error(`❌ --config: ${guidance}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Pre-2.0.0 field name for the OIDC manual/OOB code paste path; maps onto
|
|
146
|
+
// the same slot --code fills.
|
|
147
|
+
if (options.code === undefined &&
|
|
148
|
+
typeof fields.authorizationCode === 'string') {
|
|
149
|
+
options.code = fields.authorizationCode;
|
|
150
|
+
}
|
|
151
|
+
for (const field of CONFIG_BACKFILL_FIELDS) {
|
|
152
|
+
if (options[field] === undefined && fields[field] !== undefined) {
|
|
153
|
+
options[field] = fields[field];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function requireOption(value, flagName) {
|
|
158
|
+
if (!value) {
|
|
159
|
+
console.error(`❌ ${flagName} is required for this flow.`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function resolveOidcTokenEndpoint(options) {
|
|
165
|
+
return (options.tokenEndpoint ||
|
|
166
|
+
(options.uaaUrl
|
|
167
|
+
? `${options.uaaUrl.replace(/\/+$/, '')}/oauth/token`
|
|
168
|
+
: undefined));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Only the OIDC 'browser' flow opens a browser; routes `--browser`,
|
|
172
|
+
* `--redirect-port` and manual/OOB code paste into the strategy that
|
|
173
|
+
* replaces them.
|
|
174
|
+
*/
|
|
175
|
+
function buildOidcBrowserAuthorization(options) {
|
|
176
|
+
if (options.code) {
|
|
177
|
+
// The consumer already holds the code (manual paste / OOB redirect
|
|
178
|
+
// URI); no callback server is opened at all.
|
|
179
|
+
return (0, auth_providers_1.asOidcResult)((0, auth_providers_1.staticCodeStrategy)({
|
|
180
|
+
redirectUri: options.redirectUri,
|
|
181
|
+
payload: options.code,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
// No fallback: an omitted --redirect-port lets the strategy bind its own
|
|
185
|
+
// default port rather than this CLI pinning a number it doesn't own.
|
|
186
|
+
return (0, auth_providers_1.oidcCallbackStrategy)({
|
|
187
|
+
port: options.redirectPort,
|
|
188
|
+
browser: options.browser,
|
|
189
|
+
timeoutMs: exports.INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function buildOidcCommon(options) {
|
|
193
|
+
return {
|
|
194
|
+
issuerUrl: options.issuerUrl,
|
|
195
|
+
clientId: requireOption(options.clientId, '--client-id'),
|
|
196
|
+
clientSecret: options.clientSecret,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function buildOidcBrowserConfig(options) {
|
|
200
|
+
return {
|
|
201
|
+
...buildOidcCommon(options),
|
|
202
|
+
authorizationEndpoint: options.authorizationEndpoint,
|
|
203
|
+
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
204
|
+
scopes: options.scopes,
|
|
205
|
+
authorization: buildOidcBrowserAuthorization(options),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildOidcDeviceConfig(options) {
|
|
209
|
+
// Device flow never opens a browser from this process; --browser and
|
|
210
|
+
// --redirect-port have nothing to attach to here.
|
|
211
|
+
return {
|
|
212
|
+
...buildOidcCommon(options),
|
|
213
|
+
deviceAuthorizationEndpoint: options.deviceAuthorizationEndpoint,
|
|
214
|
+
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
215
|
+
scopes: options.scopes,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function buildOidcPasswordConfig(options) {
|
|
219
|
+
// Password flow never opens a browser; --browser and --redirect-port are
|
|
220
|
+
// not meaningful here either.
|
|
221
|
+
const passcode = options.passcode;
|
|
222
|
+
const username = options.username || (passcode ? 'passcode' : undefined);
|
|
223
|
+
const password = options.password || passcode;
|
|
224
|
+
return {
|
|
225
|
+
...buildOidcCommon(options),
|
|
226
|
+
username: requireOption(username, '--username (or --passcode)'),
|
|
227
|
+
password: requireOption(password, '--password (or --passcode)'),
|
|
228
|
+
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
229
|
+
scopes: options.scopes,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function buildOidcTokenExchangeConfig(options) {
|
|
233
|
+
// Token exchange never opens a browser; --browser and --redirect-port are
|
|
234
|
+
// not meaningful here either.
|
|
235
|
+
return {
|
|
236
|
+
...buildOidcCommon(options),
|
|
237
|
+
subjectToken: requireOption(options.subjectToken, '--subject-token'),
|
|
238
|
+
subjectTokenType: options.subjectTokenType ||
|
|
239
|
+
'urn:ietf:params:oauth:token-type:access_token',
|
|
240
|
+
scope: options.scope,
|
|
241
|
+
audience: options.audience,
|
|
242
|
+
actorToken: options.actorToken,
|
|
243
|
+
actorTokenType: options.actorTokenType,
|
|
244
|
+
tokenEndpoint: resolveOidcTokenEndpoint(options),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Both SAML flows (bearer, pure) can open a browser; routes `--browser`,
|
|
249
|
+
* `--redirect-port` and the manual/static assertion options into the
|
|
250
|
+
* strategy that replaces them.
|
|
251
|
+
*/
|
|
252
|
+
function buildSamlAuthorization(options) {
|
|
253
|
+
if (options.assertion) {
|
|
254
|
+
// The consumer already holds the assertion; nothing is opened or asked.
|
|
255
|
+
return (0, auth_providers_1.staticCodeStrategy)({
|
|
256
|
+
redirectUri: options.acsUrl,
|
|
257
|
+
payload: options.assertion,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const assertionFlow = options.assertionFlow || 'browser';
|
|
261
|
+
if (assertionFlow !== 'browser') {
|
|
262
|
+
// 'manual', and an 'assertion' flow given no value, both need a human to
|
|
263
|
+
// lift the SAMLResponse out of the POST body by hand.
|
|
264
|
+
return (0, auth_providers_1.manualSamlResponseStrategy)({
|
|
265
|
+
redirectUri: options.acsUrl,
|
|
266
|
+
read: readManualInput,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// No fallback: an omitted --redirect-port lets the strategy bind its own
|
|
270
|
+
// default port rather than this CLI pinning a number it doesn't own.
|
|
271
|
+
return (0, auth_providers_1.samlCallbackStrategy)({
|
|
272
|
+
port: options.redirectPort,
|
|
273
|
+
browser: options.browser,
|
|
274
|
+
timeoutMs: exports.INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function buildSamlCookieProvider(options) {
|
|
278
|
+
const assertionFlow = options.assertionFlow || (options.assertion ? 'assertion' : 'browser');
|
|
279
|
+
return async (samlResponse) => {
|
|
280
|
+
if (options.cookie) {
|
|
281
|
+
return options.cookie;
|
|
282
|
+
}
|
|
283
|
+
if (assertionFlow === 'assertion') {
|
|
284
|
+
return `SAMLResponse=${samlResponse}`;
|
|
285
|
+
}
|
|
286
|
+
return readManualInput('Paste session cookies: ');
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function buildSamlBearerConfig(options) {
|
|
290
|
+
return {
|
|
291
|
+
idpSsoUrl: requireOption(options.idpSsoUrl, '--idp-sso-url'),
|
|
292
|
+
spEntityId: requireOption(options.spEntityId, '--sp-entity-id'),
|
|
293
|
+
acsUrl: options.acsUrl,
|
|
294
|
+
relayState: options.relayState,
|
|
295
|
+
tokenUrl: options.tokenEndpoint,
|
|
296
|
+
uaaUrl: options.uaaUrl,
|
|
297
|
+
clientId: options.clientId,
|
|
298
|
+
clientSecret: options.clientSecret,
|
|
299
|
+
authorization: buildSamlAuthorization(options),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function buildSamlPureConfig(options) {
|
|
303
|
+
return {
|
|
304
|
+
idpSsoUrl: requireOption(options.idpSsoUrl, '--idp-sso-url'),
|
|
305
|
+
spEntityId: requireOption(options.spEntityId, '--sp-entity-id'),
|
|
306
|
+
acsUrl: options.acsUrl,
|
|
307
|
+
relayState: options.relayState,
|
|
308
|
+
authorization: buildSamlAuthorization(options),
|
|
309
|
+
cookieProvider: buildSamlCookieProvider(options),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Builds the effective `SsoProviderConfig` from `options` alone.
|
|
314
|
+
*
|
|
315
|
+
* `options` must already be the *merged* view — `--config` file fields
|
|
316
|
+
* backfilled beneath whatever the CLI flags set, via `applyFileConfig` — so
|
|
317
|
+
* this never needs a separate file/CLI merge step, and required-field
|
|
318
|
+
* validation (`requireOption`, inside each builder above) sees the same
|
|
319
|
+
* merged data regardless of whether a value came from the file or a flag.
|
|
320
|
+
*/
|
|
321
|
+
function buildProviderConfig(options, existingAuth, existingConn) {
|
|
322
|
+
if (!options.protocol || !options.flow) {
|
|
323
|
+
throw new Error('Provider config is missing. Use --config or --protocol/--flow options.');
|
|
324
|
+
}
|
|
325
|
+
let result;
|
|
326
|
+
if (options.protocol === 'oidc') {
|
|
327
|
+
switch (options.flow) {
|
|
328
|
+
case 'browser':
|
|
329
|
+
result = {
|
|
330
|
+
protocol: 'oidc',
|
|
331
|
+
flow: 'browser',
|
|
332
|
+
config: buildOidcBrowserConfig(options),
|
|
333
|
+
};
|
|
334
|
+
break;
|
|
335
|
+
case 'device':
|
|
336
|
+
result = {
|
|
337
|
+
protocol: 'oidc',
|
|
338
|
+
flow: 'device',
|
|
339
|
+
config: buildOidcDeviceConfig(options),
|
|
340
|
+
};
|
|
341
|
+
break;
|
|
342
|
+
case 'password':
|
|
343
|
+
result = {
|
|
344
|
+
protocol: 'oidc',
|
|
345
|
+
flow: 'password',
|
|
346
|
+
config: buildOidcPasswordConfig(options),
|
|
347
|
+
};
|
|
348
|
+
break;
|
|
349
|
+
case 'token_exchange':
|
|
350
|
+
result = {
|
|
351
|
+
protocol: 'oidc',
|
|
352
|
+
flow: 'token_exchange',
|
|
353
|
+
config: buildOidcTokenExchangeConfig(options),
|
|
354
|
+
};
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
throw new Error(`Unsupported OIDC flow: ${options.flow}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (options.protocol === 'saml2') {
|
|
361
|
+
switch (options.flow) {
|
|
362
|
+
case 'bearer':
|
|
363
|
+
result = {
|
|
364
|
+
protocol: 'saml2',
|
|
365
|
+
flow: 'bearer',
|
|
366
|
+
config: buildSamlBearerConfig(options),
|
|
367
|
+
};
|
|
368
|
+
break;
|
|
369
|
+
case 'pure':
|
|
370
|
+
result = {
|
|
371
|
+
protocol: 'saml2',
|
|
372
|
+
flow: 'pure',
|
|
373
|
+
config: buildSamlPureConfig(options),
|
|
374
|
+
};
|
|
375
|
+
break;
|
|
376
|
+
default:
|
|
377
|
+
throw new Error(`Unsupported SAML flow: ${options.flow}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
throw new Error(`Unsupported protocol: ${options.protocol}`);
|
|
382
|
+
}
|
|
383
|
+
const accessToken = existingConn?.authorizationToken;
|
|
384
|
+
const refreshToken = existingAuth?.refreshToken;
|
|
385
|
+
if ((result.protocol === 'oidc' ||
|
|
386
|
+
(result.protocol === 'saml2' && result.flow === 'bearer')) &&
|
|
387
|
+
(accessToken || refreshToken)) {
|
|
388
|
+
result = {
|
|
389
|
+
...result,
|
|
390
|
+
config: {
|
|
391
|
+
...result.config,
|
|
392
|
+
accessToken,
|
|
393
|
+
refreshToken,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/auth-broker",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"node": ">=18.2.0"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@mcp-abap-adt/auth-providers": "^
|
|
65
|
+
"@mcp-abap-adt/auth-providers": "^2.0.0",
|
|
66
66
|
"@mcp-abap-adt/auth-stores": "^1.0.2",
|
|
67
67
|
"@mcp-abap-adt/interfaces": "^2.3.0",
|
|
68
68
|
"axios": "^1.13.5"
|