@mcp-abap-adt/auth-broker 2.2.0 → 3.0.1
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 +235 -0
- package/README.md +324 -284
- package/dist/AuthBroker.d.ts +81 -106
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +209 -567
- package/dist/bin/mcp-auth.js +54 -31
- package/dist/bin/mcp-sso.js +52 -44
- package/dist/bin/mcpSsoConfig.js +169 -1
- package/dist/bin/samlMetadata.js +175 -0
- package/dist/bin/workDir.js +79 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/providers/ITokenProvider.d.ts +2 -2
- package/dist/providers/ITokenProvider.d.ts.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/package.json +12 -8
- package/dist/utils/formatting.d.ts +0 -16
- package/dist/utils/formatting.d.ts.map +0 -1
- package/dist/utils/formatting.js +0 -34
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,241 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the complete list.
|
|
11
11
|
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
## [3.0.1] - 2026-09-26
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A store failure other than a missing file reaches the caller as raised.**
|
|
19
|
+
The broker answered every store error as absence — logged, then `null` — so
|
|
20
|
+
a service key that is there but not valid JSON, or a file the process may
|
|
21
|
+
not read, surfaced only as its consequence: "Session for destination … is
|
|
22
|
+
missing required field 'serviceUrl'", which sends users to create a file
|
|
23
|
+
that already exists. Absence is still `null` or `FILE_NOT_FOUND`, and the
|
|
24
|
+
flow goes on to the next source; anything else is thrown unchanged — before
|
|
25
|
+
the provider is asked when it comes from the reads that precede the token,
|
|
26
|
+
and after the provider answered and the token was written when it comes
|
|
27
|
+
from the reads that save the refresh token.
|
|
28
|
+
|
|
29
|
+
Measured on auth-stores 1.2.3: the service key stores throw on an invalid or
|
|
30
|
+
unreadable file (`Invalid JSON in file …`, `EACCES`), so this change is what
|
|
31
|
+
a broker user sees. The session stores answer an unreadable or unparsable
|
|
32
|
+
file with `null` themselves; that is theirs to change, and this release does
|
|
33
|
+
not see it.
|
|
34
|
+
|
|
35
|
+
## [3.0.0] - 2026-09-26
|
|
36
|
+
|
|
37
|
+
### Security
|
|
38
|
+
|
|
39
|
+
- **No token reaches a log line or the terminal.** `formatToken` returned a
|
|
40
|
+
token of 50 characters or fewer whole, and a longer one's first and last 25
|
|
41
|
+
characters. UAA and XSUAA refresh tokens are opaque and about 34 characters,
|
|
42
|
+
so `AuthBroker` logged every refresh token in full, at `info`, on each token
|
|
43
|
+
request, persist and session check; `mcp-auth` printed the first 50
|
|
44
|
+
characters of the access and refresh tokens it wrote, which is the whole of
|
|
45
|
+
a refresh token. The broker now logs no token at all (only whether there is
|
|
46
|
+
one), and `mcp-auth` says `<redacted, N chars>`. auth-providers 4.1.2
|
|
47
|
+
closed the same leak in its own `formatToken`, and the range now requires
|
|
48
|
+
it. **Anyone who shipped broker logs at `info` or `debug`, or kept
|
|
49
|
+
`mcp-auth` output, should treat the refresh tokens in them as exposed and
|
|
50
|
+
revoke them.**
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- **BREAKING: `AuthBroker` orchestrates, the provider decides.** The broker
|
|
55
|
+
resolves what the stores hold for a destination, hands it to the provider,
|
|
56
|
+
calls it once and writes the answer back. The Step 0 / session / service-key
|
|
57
|
+
branches that repeated the provider's own refresh-then-login logic — each
|
|
58
|
+
calling the same `getTokens()` — are gone, and so is the retry of a failed
|
|
59
|
+
call.
|
|
60
|
+
- **Constructor:** `new AuthBroker({ sessionStore, serviceKeyStore?, provider }, logger?)`.
|
|
61
|
+
`tokenProvider` is now `provider`, typed `IRefreshableTokenProvider` (from
|
|
62
|
+
`@mcp-abap-adt/interfaces-auth` 2.1.0), or a factory
|
|
63
|
+
`(destination, authConfig, connConfig) => IRefreshableTokenProvider`. The
|
|
64
|
+
factory is called once per destination and seeded with what the stores
|
|
65
|
+
hold: the UAA credentials (the session's, else the service key's) with the
|
|
66
|
+
stored refresh token, and the connection config with `serviceUrl` and the
|
|
67
|
+
stored token. An instance is used as given, for every destination.
|
|
68
|
+
- **`allowBrowserAuth` and the `browser` argument are removed**, and with them
|
|
69
|
+
the `BROWSER_AUTH_REQUIRED` error. `browser` was never read, and how a
|
|
70
|
+
login is conducted is the provider's authorization strategy.
|
|
71
|
+
- **`refreshToken(destination)` forces a new token.** It called `getToken()`,
|
|
72
|
+
which returned the provider's cached token — the one the server had just
|
|
73
|
+
refused — breaking `ITokenRefresher`'s "always obtains new token" and
|
|
74
|
+
risking a 401 loop. It now calls the provider's `refreshTokens()`, and so
|
|
75
|
+
does `createTokenRefresher(destination).refreshToken()`.
|
|
76
|
+
- **Provider errors propagate unchanged.** They were rewrapped into a plain
|
|
77
|
+
`Error`, losing `code`, `missingFields`, `cause` and the class itself
|
|
78
|
+
(`instanceof ValidationError` was false).
|
|
79
|
+
- **The client secret is no longer copied into the session store.** After a
|
|
80
|
+
login from a service key the broker wrote the service key's
|
|
81
|
+
`uaaClientSecret` into the session (`SAP_UAA_CLIENT_SECRET` /
|
|
82
|
+
`XSUAA_UAA_CLIENT_SECRET` in the `.env`). It now writes the token and the
|
|
83
|
+
refresh token only; credentials a session already holds are left as they
|
|
84
|
+
are. `mcp-auth`, `mcp-sso` and `generate-env` still write the secret into
|
|
85
|
+
the file they produce, deliberately: that file is a self-contained session
|
|
86
|
+
read with no service key beside it.
|
|
87
|
+
- **The session store's `loadSession` and `saveSession` are used**, both part
|
|
88
|
+
of `ISessionStore`: `loadSession` to read a refresh token stored without
|
|
89
|
+
credentials, `saveSession` to store one. The constructor checks for them.
|
|
90
|
+
- **Removed:** `src/utils/formatting` (`formatToken`,
|
|
91
|
+
`formatExpirationDate`): the broker logs no token, so there is nothing to
|
|
92
|
+
format.
|
|
93
|
+
|
|
94
|
+
**Migrating from 2.2.0:**
|
|
95
|
+
1. Rename `tokenProvider` to `provider` and drop the second constructor
|
|
96
|
+
argument: `new AuthBroker(config, 'system', logger)` becomes
|
|
97
|
+
`new AuthBroker(config, logger)`.
|
|
98
|
+
2. The provider must implement `IRefreshableTokenProvider`, i.e. have
|
|
99
|
+
`refreshTokens()`. `@mcp-abap-adt/auth-providers` 4.2.0 providers do; a
|
|
100
|
+
provider of your own must add it (a new token, never the cached one).
|
|
101
|
+
3. Instead of `allowBrowserAuth: false`, give the provider an authorization
|
|
102
|
+
strategy that refuses: `authorization: { authorize: async () => { throw new LoginRequiredError(); } }`,
|
|
103
|
+
and catch your own error — the broker hands it back unchanged. A browser
|
|
104
|
+
login that fails (timeout, the identity provider's refusal, a busy
|
|
105
|
+
callback port, a browser that would not open) is auth-providers'
|
|
106
|
+
`BrowserAuthError` from 4.2.0; before that it was a plain `Error`.
|
|
107
|
+
4. Catch provider errors by class or `code` (`ValidationError`,
|
|
108
|
+
`BrowserAuthError`, `AssertionValidationError`, network `ECONNREFUSED` …),
|
|
109
|
+
not by the old `Token provider … error for <destination>` messages.
|
|
110
|
+
5. If a consumer read the client secret back from a session the broker had
|
|
111
|
+
written, read it from the service key store instead — or put it into the
|
|
112
|
+
session yourself.
|
|
113
|
+
6. To have the broker seed a provider from the stores, pass a factory rather
|
|
114
|
+
than an instance.
|
|
115
|
+
Node.js 22 or 24 and the SAML trust options (below) are required too.
|
|
116
|
+
|
|
117
|
+
- **`@mcp-abap-adt/interfaces-auth@^2.1.0`** (was `^2.0.1` on this branch,
|
|
118
|
+
`^1.2.0` in 2.2.0), for `IRefreshableTokenProvider`; **`@mcp-abap-adt/auth-stores@^1.2.3`** (was
|
|
119
|
+
`^1.2.0` in 2.2.0): 1.2.2 stops logging token characters — the same leak as
|
|
120
|
+
under *Security* — and 1.2.3 lets an XSUAA session without a client secret
|
|
121
|
+
keep its refresh token, which is what a session this broker writes now is.
|
|
122
|
+
No store API changed.
|
|
123
|
+
|
|
124
|
+
- **BREAKING: Node.js 22 or 24** — `engines: "^22 || ^24"` (was `>=18.2.0`),
|
|
125
|
+
following `@mcp-abap-adt/auth-providers` 3.0.0, which requires it: the
|
|
126
|
+
versions SAP BTP's Cloud Foundry Node.js buildpack offers.
|
|
127
|
+
|
|
128
|
+
- **BREAKING for `mcp-sso` SAML users: `@mcp-abap-adt/auth-providers@^4.2.0`**
|
|
129
|
+
(was `^2.2.0`). From 4.0.0 both SAML providers validate every assertion —
|
|
130
|
+
signature, issuer, audience, recipient, time window, request ID, replay —
|
|
131
|
+
and refuse to construct without the trust to check it against. Every
|
|
132
|
+
`mcp-sso bearer`, `mcp-sso saml2 --flow pure`, `mcp-auth saml2-pure` and
|
|
133
|
+
`mcp-auth saml2-bearer` run that worked with 2.2.0 now fails before login
|
|
134
|
+
with auth-providers' `ValidationError`
|
|
135
|
+
(`missing idpCertificates, idpEntityId`), and nothing in TypeScript said so,
|
|
136
|
+
since the new fields are optional.
|
|
137
|
+
|
|
138
|
+
**Migrating:** add `--idp-cert <path>` and `--idp-entity-id <id>` (or
|
|
139
|
+
`idpCertificates`/`idpEntityId` in `--config`); give the real
|
|
140
|
+
`--sp-entity-id`, now the `Audience`, and the `--acs-url` the assertion names
|
|
141
|
+
as `Recipient`; for `bearer` against UAA or XSUAA add `--idp-initiated`,
|
|
142
|
+
since both refuse an assertion carrying `InResponseTo`; for any other
|
|
143
|
+
`--assertion` from a request `mcp-sso` did not send, add
|
|
144
|
+
`--authn-request-id`. The README's *Migrating `mcp-sso` SAML runs from
|
|
145
|
+
2.2.0* has the details.
|
|
146
|
+
|
|
147
|
+
Also from auth-providers 3.0.0: `Saml2BearerProvider` sends one
|
|
148
|
+
base64url-encoded Assertion, as RFC 7522 requires, instead of the whole
|
|
149
|
+
`SAMLResponse`, which UAA answered with 401. The rest of what 3.x and 4.x
|
|
150
|
+
changed does not reach this package: `DeviceFlowProvider` was never used
|
|
151
|
+
(`mcp-sso oidc --flow device` builds `OidcDeviceFlowProvider`), and
|
|
152
|
+
`buildSamlAuthorizationUrl`, `getSamlAssertion` and `parseSamlNotOnOrAfter`
|
|
153
|
+
were never imported. `AuthorizationCodeProvider` and
|
|
154
|
+
`ClientCredentialsProvider`, which `mcp-auth` builds, are unchanged.
|
|
155
|
+
|
|
156
|
+
- **`@mcp-abap-adt/interfaces-auth` 2.x** (was `^1.2.0`). Its one break,
|
|
157
|
+
`AssertionContext.expectedInResponseTo` becoming optional, concerns
|
|
158
|
+
`IAssertionValidator` implementers; nothing here implements one.
|
|
159
|
+
- **`@mcp-abap-adt/interfaces-auth-sap@^1.0.1`** (was `^1.0.0`), the release
|
|
160
|
+
that accepts `interfaces-auth` 2, so an install carries one copy of it.
|
|
161
|
+
|
|
162
|
+
### Added
|
|
163
|
+
|
|
164
|
+
- **SAML metadata instead of hand-copied values.**
|
|
165
|
+
- `--idp-metadata <url|path>` / `idpMetadata`: the identity provider's SAML
|
|
166
|
+
metadata — for SAP Cloud Identity Services
|
|
167
|
+
`https://<tenant>.accounts.ondemand.com/saml2/metadata` — fills
|
|
168
|
+
`--idp-cert`, `--idp-entity-id` and `--idp-sso-url` where they were not
|
|
169
|
+
given. Signing keys and keys without `use` are trusted, encryption keys
|
|
170
|
+
never.
|
|
171
|
+
- `saml2-bearer` with `--service-key` reads XSUAA's own
|
|
172
|
+
`<uaa.url>/saml/metadata` for the `Audience` (`--sp-entity-id`), the
|
|
173
|
+
`Recipient` (`--acs-url`) and the token endpoint — all three the
|
|
174
|
+
`/oauth/token/alias/<alias>` endpoint and the entityID it publishes, none
|
|
175
|
+
of them in the service key. `--saml-metadata <file>` still works, and now
|
|
176
|
+
fills the same three.
|
|
177
|
+
- An explicit option always wins, and trust is replaced, never widened: a
|
|
178
|
+
`--idp-cert` means no certificate from the metadata is trusted beside it.
|
|
179
|
+
- Federation metadata — an `EntitiesDescriptor` holding several entities —
|
|
180
|
+
is read per entity: the entityID, keys and SSO URL all come from the one
|
|
181
|
+
identity provider. With several, `--idp-entity-id` names it, and without
|
|
182
|
+
it the run stops listing them; a named entity the metadata does not
|
|
183
|
+
describe is refused rather than filled from another one. The same holds
|
|
184
|
+
for the service provider's metadata and `--sp-entity-id`.
|
|
185
|
+
Metadata carries the certificates assertions are verified against, so a
|
|
186
|
+
URL must be https (plain http only for loopback, i.e. a local test IdP).
|
|
187
|
+
|
|
188
|
+
A bearer run against XSUAA is now
|
|
189
|
+
`mcp-sso bearer --service-key ./key.json --idp-metadata https://<tenant>.accounts.ondemand.com/saml2/metadata --idp-initiated …`.
|
|
190
|
+
|
|
191
|
+
- **`mcp-sso` SAML trust options**, passed into both the `bearer` and the
|
|
192
|
+
`pure` provider config:
|
|
193
|
+
- `--idp-cert <path>`, repeatable for key rotation: a PEM file (one
|
|
194
|
+
certificate or several) or a binary DER file. `idpCertificates` in
|
|
195
|
+
`--config` carries them inline (a string or a list, PEM or base64 DER); a
|
|
196
|
+
`--idp-cert` replaces the file's list rather than adding to it.
|
|
197
|
+
- `--idp-entity-id <id>` / `idpEntityId`: the `Issuer` the assertion must
|
|
198
|
+
name.
|
|
199
|
+
- `--idp-initiated` / `idpInitiated`: no AuthnRequest is sent. With it,
|
|
200
|
+
`mcp-sso` never asks auth-providers for an authorization URL, which it
|
|
201
|
+
refuses for an IdP-initiated login: the default assertion flow becomes
|
|
202
|
+
`manual` (start the login at the identity provider, paste the
|
|
203
|
+
`SAMLResponse`), `--assertion` works as before, and
|
|
204
|
+
`--assertion-flow browser` is refused, having no URL to open.
|
|
205
|
+
- `--authn-request-id <id>` / `authnRequestId`: the request an
|
|
206
|
+
`--assertion` answers, when `mcp-sso` did not send it.
|
|
207
|
+
|
|
208
|
+
Missing trust material is passed on as missing: auth-providers' own
|
|
209
|
+
`ValidationError` names it. `mcp-sso --help` says what a SAML run requires.
|
|
210
|
+
|
|
211
|
+
### Fixed
|
|
212
|
+
|
|
213
|
+
- **The CLIs' temporary session no longer outlives a failed login.** `mcp-auth`
|
|
214
|
+
and `mcp-sso` kept their temporary session store — which holds the client
|
|
215
|
+
secret — in `.tmp` beside the output file, the user's sessions folder, and
|
|
216
|
+
removed it only on success: a failed or interrupted login left the secret
|
|
217
|
+
there, and two runs at once shared one directory that each removed on exit.
|
|
218
|
+
Each run now gets a private directory under the OS temp dir (mode 0700),
|
|
219
|
+
removed on any exit, error and `SIGINT`/`SIGTERM`/`SIGHUP` included.
|
|
220
|
+
Measured: after a refused login (exit 1) and after `SIGTERM` (exit 143) the
|
|
221
|
+
directory is gone, and no `.tmp` appears beside the output.
|
|
222
|
+
- **`mcp-auth` writes the refresh token the login obtained.** It wrote the
|
|
223
|
+
output from the authorization config it read *before* the login — the
|
|
224
|
+
service key's, which holds no refresh token — while the refresh token XSUAA
|
|
225
|
+
returned went into a temporary session store that was then deleted. Every
|
|
226
|
+
`mcp-auth` session file came out without `SAP_REFRESH_TOKEN`, so the next
|
|
227
|
+
expiry meant another browser login. Measured on the trial: the same login
|
|
228
|
+
now writes a 34-character refresh token.
|
|
229
|
+
- **A closed stdin is a failure, not a success.** A prompt for a pasted
|
|
230
|
+
`SAMLResponse`, passcode or cookie waited on a promise that never settled
|
|
231
|
+
when stdin was closed; the event loop drained and `mcp-sso` exited 0 having
|
|
232
|
+
written nothing, which a script reads as success. It now fails, naming the
|
|
233
|
+
prompt.
|
|
234
|
+
- **`generate-env` constructs its XSUAA session store with a service URL.**
|
|
235
|
+
`XsuaaSessionStore` requires one since auth-stores 1.x and the script passed
|
|
236
|
+
none; it now takes the service key's, or the placeholder `mcp-auth` uses.
|
|
237
|
+
|
|
238
|
+
### Tests
|
|
239
|
+
|
|
240
|
+
- The Keycloak fixtures run the SAML pure flow end to end with validation:
|
|
241
|
+
the realm signs its responses and names `http://localhost:3002/acs` as the
|
|
242
|
+
IdP-initiated ACS, the `demo` user has the profile Keycloak 24 requires, and
|
|
243
|
+
`run-tests.sh` / `run-saml.sh` pass `--idp-metadata` (and
|
|
244
|
+
`--authn-request-id` for the SP-initiated login, whose ID `saml-sp.js` now
|
|
245
|
+
records).
|
|
246
|
+
|
|
12
247
|
## [2.2.0] - 2026-09-24
|
|
13
248
|
|
|
14
249
|
### Changed
|