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