@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 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