@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/README.md CHANGED
@@ -1,16 +1,24 @@
1
1
  # @mcp-abap-adt/auth-broker
2
2
  [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
3
3
 
4
- JWT authentication broker for MCP ABAP ADT server. Manages authentication tokens based on destination headers, automatically loading tokens from `.env` files and refreshing them using service keys when needed.
4
+ A per-destination token broker for SAP BTP and ABAP systems. For a destination — a name, such as
5
+ `TRIAL` — it reads the session and the service key from the stores it is given, hands them to
6
+ a token provider, and saves what the provider returns back to the session: a JWT or, for SAML,
7
+ session cookies, with the refresh token. It decides nothing about tokens itself: whether the
8
+ cached token is still good, when to refresh and when to log in is the provider's call
9
+ (`@mcp-abap-adt/auth-providers`), and where sessions live is the stores' (`@mcp-abap-adt/auth-stores`).
10
+
11
+ It also ships two CLIs that write session files: `mcp-auth` (service key → session,
12
+ authorization code or client credentials) and `mcp-sso` (OIDC and SAML single sign-on).
5
13
 
6
14
  ## Features
7
15
 
8
- - 🔐 **Destination-based Authentication**: Load tokens based on `x-mcp-destination` header
9
- - 📁 **Environment File Support**: Automatically loads tokens from `{destination}.env` files
10
- - 🔄 **Automatic Token Refresh**: Refreshes expired tokens using service keys from `{destination}.json` files
11
- - ✅ **Token Validation**: Validates tokens via provider (if `validateToken` is implemented)
12
- - 💾 **Token Caching**: In-memory caching for improved performance
13
- - 🔧 **Configurable Base Path**: Customize where `.env` and `.json` files are stored
16
+ - 🎯 **Per destination**: one provider per destination name, built by a factory or given once
17
+ - 🔄 **Provider-driven token lifecycle**: The provider decides whether its cached token is still good, refreshes it, or logs in; the broker persists what it returns
18
+ - ⚡ **Forced refresh**: `refreshToken()` obtains a new token even when the cached one looks valid — for a caller holding a 401
19
+ - 🧾 **JWT or SAML cookies**: what the provider returns is saved as a token or as session cookies
20
+ - 🔑 **No secrets copied**: The client secret stays in the service key; the session store gets tokens only
21
+ - 🧰 **CLIs**: `mcp-auth` and `mcp-sso` produce `.env`/JSON session files, SAML trust read from metadata
14
22
 
15
23
  ## Installation
16
24
 
@@ -18,103 +26,104 @@ JWT authentication broker for MCP ABAP ADT server. Manages authentication tokens
18
26
  npm install @mcp-abap-adt/auth-broker
19
27
  ```
20
28
 
21
- ## Usage
22
-
23
- ### Basic Usage (Provider Required)
24
-
25
- AuthBroker requires a token provider configured for the destination:
26
-
27
- ```typescript
28
- import { AuthBroker, AbapSessionStore } from '@mcp-abap-adt/auth-broker';
29
- import {
30
- AuthorizationCodeProvider,
31
- browserCallbackStrategy,
32
- } from '@mcp-abap-adt/auth-providers';
29
+ Requires Node.js 22 or 24 (`engines: "^22 || ^24"`), the versions SAP BTP's Cloud Foundry
30
+ Node.js buildpack offers.
33
31
 
34
- const tokenProvider = new AuthorizationCodeProvider({
35
- uaaUrl: 'https://...authentication...hana.ondemand.com',
36
- clientId: '...',
37
- clientSecret: '...',
38
- authorization: browserCallbackStrategy({ browser: 'system' }),
39
- });
40
-
41
- const broker = new AuthBroker({
42
- sessionStore: new AbapSessionStore('/path/to/destinations'),
43
- tokenProvider,
44
- });
45
-
46
- const token = await broker.getToken('TRIAL');
47
- ```
32
+ ## Usage
48
33
 
49
- ### Full Configuration (All Dependencies)
34
+ ### Basic Usage
50
35
 
51
- For maximum flexibility, provide all three dependencies:
36
+ The broker takes a session store, an optional service key store, and a token
37
+ provider implementing `IRefreshableTokenProvider` (from
38
+ `@mcp-abap-adt/interfaces-auth`) — or a factory that builds one per
39
+ destination:
52
40
 
53
41
  ```typescript
42
+ import { AuthBroker } from '@mcp-abap-adt/auth-broker';
54
43
  import {
55
- AuthBroker,
56
44
  AbapServiceKeyStore,
57
45
  AbapSessionStore,
58
- } from '@mcp-abap-adt/auth-broker';
46
+ } from '@mcp-abap-adt/auth-stores';
59
47
  import {
60
48
  AuthorizationCodeProvider,
61
49
  browserCallbackStrategy,
62
50
  } from '@mcp-abap-adt/auth-providers';
63
51
 
64
- const broker = new AuthBroker({
65
- sessionStore: new AbapSessionStore('/path/to/destinations'),
66
- serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'), // optional
67
- tokenProvider: new AuthorizationCodeProvider({
68
- uaaUrl: 'https://...authentication...hana.ondemand.com',
69
- clientId: '...',
70
- clientSecret: '...',
71
- authorization: browserCallbackStrategy({ browser: 'system' }),
72
- }),
73
- }, 'chrome', logger);
74
-
75
- // Disable browser authentication for headless/stdio environments (e.g., MCP with Cline)
76
- const brokerNoBrowser = new AuthBroker({
77
- sessionStore: new AbapSessionStore('/path/to/destinations'),
78
- serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'),
79
- tokenProvider: new AuthorizationCodeProvider({
80
- uaaUrl: 'https://...authentication...hana.ondemand.com',
81
- clientId: '...',
82
- clientSecret: '...',
83
- authorization: browserCallbackStrategy({ browser: 'none' }),
84
- }),
85
- allowBrowserAuth: false, // Throws BROWSER_AUTH_REQUIRED if browser auth needed
86
- }, 'chrome', logger);
52
+ const broker = new AuthBroker(
53
+ {
54
+ sessionStore: new AbapSessionStore('/path/to/sessions'),
55
+ serviceKeyStore: new AbapServiceKeyStore('/path/to/keys'), // optional
56
+ // Called once per destination, seeded with what the stores hold.
57
+ provider: (destination, authConfig, connConfig) => {
58
+ if (!authConfig) throw new Error(`No UAA credentials for ${destination}`);
59
+ return new AuthorizationCodeProvider({
60
+ uaaUrl: authConfig.uaaUrl,
61
+ clientId: authConfig.uaaClientId,
62
+ clientSecret: authConfig.uaaClientSecret,
63
+ refreshToken: authConfig.refreshToken, // stored by an earlier login
64
+ accessToken: connConfig.authorizationToken, // reused while valid
65
+ authorization: browserCallbackStrategy({ browser: 'system' }),
66
+ });
67
+ },
68
+ },
69
+ logger, // optional ILogger
70
+ );
71
+
72
+ const token = await broker.getToken('TRIAL');
87
73
  ```
88
74
 
89
- ### Session + Service Key (For Initialization)
75
+ The factory receives:
90
76
 
91
- If you need to initialize sessions from service keys, create the provider from service key auth config:
77
+ - `authConfig` — the UAA credentials: the session's own when it holds them,
78
+ else the service key's; carrying the refresh token the session stored.
79
+ `null` when no store has credentials (a SAML flow needs none).
80
+ - `connConfig` — the session's connection config, with `serviceUrl` resolved
81
+ (from the session, else the service key) and the token stored last.
82
+
83
+ A provider instance can be passed instead of a factory; it is then used as
84
+ given, for every destination, and the broker seeds it with nothing:
92
85
 
93
86
  ```typescript
94
87
  const broker = new AuthBroker({
95
- sessionStore: new AbapSessionStore('/path/to/destinations'),
96
- serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'),
97
- tokenProvider: new AuthorizationCodeProvider({
98
- uaaUrl: 'https://...authentication...hana.ondemand.com',
99
- clientId: '...',
100
- clientSecret: '...',
101
- authorization: browserCallbackStrategy({ browser: 'system' }),
102
- }),
88
+ sessionStore: new AbapSessionStore('/path/to/sessions'),
89
+ provider: new AuthorizationCodeProvider({ uaaUrl, clientId, clientSecret }),
103
90
  });
104
91
  ```
105
92
 
106
- ### In-Memory Session Store
93
+ > `AuthorizationCodeProvider` and the other `@mcp-abap-adt/auth-providers`
94
+ > providers implement `IRefreshableTokenProvider` from auth-providers 4.2.0.
107
95
 
108
- For testing or temporary sessions:
96
+ ### Headless Processes (No Browser)
109
97
 
110
- ```typescript
111
- import { AuthBroker, SafeAbapSessionStore } from '@mcp-abap-adt/auth-broker';
98
+ Whether a login may open a browser is the provider's authorization strategy,
99
+ not a broker option. A process nobody is watching (an MCP server on stdio, a
100
+ CI job) gives the provider a strategy that refuses, and catches its own error —
101
+ the broker hands it back unchanged:
112
102
 
113
- const broker = new AuthBroker({
114
- sessionStore: new SafeAbapSessionStore(), // In-memory, data lost after restart
103
+ ```typescript
104
+ class LoginRequiredError extends Error {}
105
+
106
+ const provider = new AuthorizationCodeProvider({
107
+ uaaUrl, clientId, clientSecret, refreshToken,
108
+ authorization: {
109
+ authorize: async () => {
110
+ throw new LoginRequiredError('Run mcp-auth to log in');
111
+ },
112
+ },
115
113
  });
114
+
115
+ try {
116
+ await broker.getToken('TRIAL');
117
+ } catch (error) {
118
+ if (error instanceof LoginRequiredError) {
119
+ // No usable token or refresh token: a person has to log in.
120
+ }
121
+ }
116
122
  ```
117
123
 
124
+ A cached token that is still valid, or a refresh token the UAA accepts, never
125
+ reaches the strategy.
126
+
118
127
  ### Custom Browser Auth Port
119
128
 
120
129
  How a login is conducted — including which port the local OAuth2 callback
@@ -126,27 +135,26 @@ proxies typically use (e.g. `3001`/`3333`). Pass `port` to avoid conflicts
126
135
  with a specific redirect URI registered at the identity provider:
127
136
 
128
137
  ```typescript
129
- const broker = new AuthBroker({
130
- sessionStore: new AbapSessionStore('/path/to/destinations'),
131
- serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'),
132
- tokenProvider: new AuthorizationCodeProvider({
133
- uaaUrl: 'https://...authentication...hana.ondemand.com',
134
- clientId: '...',
135
- clientSecret: '...',
136
- authorization: browserCallbackStrategy({ browser: 'system', port: 4001 }),
137
- }),
138
- }, 'chrome');
138
+ new AuthorizationCodeProvider({
139
+ uaaUrl, clientId, clientSecret,
140
+ authorization: browserCallbackStrategy({ browser: 'system', port: 4001 }),
141
+ });
139
142
  ```
140
143
 
141
144
  ### Getting Tokens
142
145
 
143
146
  ```typescript
147
+ // The provider's current token: cached while valid, else refreshed or logged in.
144
148
  const token = await broker.getToken('TRIAL');
145
149
 
146
- // Force refresh token
150
+ // A new token, never the cached one — after the server refused the token (401).
147
151
  const newToken = await broker.refreshToken('TRIAL');
148
152
  ```
149
153
 
154
+ Both write the result to the session store: a SAML result as session cookies,
155
+ anything else as the bearer token, and the refresh token when the result has
156
+ one.
157
+
150
158
  ### Creating Token Refresher for DI
151
159
 
152
160
  The `createTokenRefresher()` method creates an `ITokenRefresher` implementation that can be injected into connections. This enables connections to handle token refresh transparently without knowing about authentication internals.
@@ -159,7 +167,7 @@ import { JwtAbapConnection } from '@mcp-abap-adt/connection';
159
167
  const broker = new AuthBroker({
160
168
  sessionStore: mySessionStore,
161
169
  serviceKeyStore: myServiceKeyStore,
162
- tokenProvider: myTokenProvider,
170
+ provider: myProviderFactory,
163
171
  });
164
172
 
165
173
  // Create token refresher for specific destination
@@ -169,8 +177,8 @@ const tokenRefresher = broker.createTokenRefresher('TRIAL');
169
177
  const connection = new JwtAbapConnection(config, tokenRefresher);
170
178
 
171
179
  // Token refresher methods:
172
- // - getToken(): Returns cached token if valid, otherwise refreshes
173
- // - refreshToken(): Forces token refresh and saves to session store
180
+ // - getToken(): the provider's current token (broker.getToken)
181
+ // - refreshToken(): a new token, never the cached one (broker.refreshToken)
174
182
  ```
175
183
 
176
184
  **Benefits of Token Refresher:**
@@ -179,6 +187,28 @@ const connection = new JwtAbapConnection(config, tokenRefresher);
179
187
  - 💾 **Automatic Persistence**: Tokens saved to session store after refresh
180
188
  - 🎯 **Destination-Scoped**: Each refresher is bound to specific destination
181
189
 
190
+ ## Migrating from 2.2.0
191
+
192
+ 1. `tokenProvider` is now `provider`, and the `browser` argument is gone:
193
+ `new AuthBroker({ sessionStore, serviceKeyStore, tokenProvider }, 'system', logger)`
194
+ becomes `new AuthBroker({ sessionStore, serviceKeyStore, provider }, logger)`.
195
+ 2. The provider must implement `IRefreshableTokenProvider` (`refreshTokens()`,
196
+ a new token, never the cached one) — `@mcp-abap-adt/auth-providers` 4.2.0
197
+ providers do. Pass a factory instead of an instance to have the broker seed
198
+ it with the stored refresh token and token.
199
+ 3. `allowBrowserAuth: false` and `BROWSER_AUTH_REQUIRED` are gone: give the
200
+ provider an authorization strategy that refuses and catch your own error
201
+ (see *Headless Processes*). A browser login that fails — timeout, the
202
+ identity provider's refusal, a busy callback port — is auth-providers'
203
+ `BrowserAuthError` (from 4.2.0).
204
+ 4. Provider errors arrive unchanged: match on the class or `code`, not on the
205
+ old `Token provider … error for <destination>` messages.
206
+ 5. The broker no longer writes the client secret into the session store. Read
207
+ it from the service key store if you relied on finding it in the session.
208
+ 6. `refreshToken()` now forces a new token; it used to return `getToken()`'s.
209
+ 7. Node.js 22 or 24; SAML runs need the IdP trust (see *Migrating `mcp-sso`
210
+ SAML runs from 2.2.0*).
211
+
182
212
  ## Configuration
183
213
 
184
214
  ### Environment Variables
@@ -222,28 +252,17 @@ const connection = new JwtAbapConnection(config, tokenRefresher);
222
252
 
223
253
  When logging is enabled (via `DEBUG_BROKER=true` or `DEBUG_AUTH_BROKER=true`), the broker provides detailed structured logging:
224
254
 
225
- **What is logged:**
226
- - **Broker initialization**: Configuration details, stores, token provider, browser settings
227
- - **Token retrieval**: Session state checks, token presence, refresh token availability
228
- - **Token operations**: Token requests via provider, received tokens with expiration information
229
- - **Token persistence**: Saving tokens to session with formatted token values and expiration dates
230
- - **Error context**: Detailed error information with file paths, error codes, missing fields
255
+ **What is logged:** broker initialization (which stores, instance or factory),
256
+ provider builds (whether credentials, a refresh token and a stored token were
257
+ there), and each token saved (token type, grant, whether a refresh token came
258
+ back, expiry). Store read failures are logged as warnings.
231
259
 
232
- **Logging Features:**
233
- - **Token Formatting**: Tokens are logged in truncated format (first 25 and last 25 characters, skipping middle) for security and readability
234
- - **Date Formatting**: Expiration dates are logged in readable format (e.g., "2025-12-25 19:21:27 UTC") instead of raw timestamps
235
- - **Structured Logging**: Uses `DefaultLogger` from `@mcp-abap-adt/logger` for proper formatting with icons and level prefixes
236
- - **Log Levels**: Controlled via `LOG_LEVEL` or `AUTH_LOG_LEVEL` environment variable (error, warn, info, debug)
260
+ **What is never logged:** any part of a token, refresh token or secret — not a
261
+ prefix, not a suffix. A log line says whether a token is there, never what it is.
237
262
 
238
263
  Example output with `DEBUG_BROKER=true LOG_LEVEL=info`:
239
264
  ```
240
- [INFO] ℹ️ [AUTH-BROKER] Broker initialized: hasServiceKeyStore(true), hasSessionStore(true), hasTokenProvider(true), browser(system), allowBrowserAuth(true)
241
- [INFO] ℹ️ [AUTH-BROKER] Getting token for destination: TRIAL
242
- [INFO] ℹ️ [AUTH-BROKER] Session check for TRIAL: hasToken(true), hasAuthConfig(true), hasServiceUrl(true), serviceUrl(https://...abap...), authorizationToken(eyJ0eXAiOiJKV1QiLCJqaWQiO...Q5ti7aYmEzItIDuLp7axNYo6w), hasRefreshToken(true)
243
- [INFO] ℹ️ [AUTH-BROKER] Requesting tokens for TRIAL via session
244
- [INFO] ℹ️ [AUTH-BROKER] Tokens received for TRIAL: authorizationToken(eyJ0eXAiOiJKV1QiLCJqaWQiO...Q5ti7aYmEzItIDuLp7axNYo6w), hasRefreshToken(true), authType(authorization_code), expiresIn(43199), expiresAt(2025-12-26 20:15:30 UTC)
245
- [INFO] ℹ️ [AUTH-BROKER] Saving tokens to session for TRIAL: serviceUrl(https://...abap...), authorizationToken(eyJ0eXAiOiJKV1QiLCJqaWQiO...Q5ti7aYmEzItIDuLp7axNYo6w), hasRefreshToken(true), expiresAt(2025-12-26 20:15:30 UTC)
246
- [INFO] ℹ️ [AUTH-BROKER] Token retrieved for TRIAL (via session): authorizationToken(eyJ0eXAiOiJKV1QiLCJqaWQiO...Q5ti7aYmEzItIDuLp7axNYo6w)
265
+ [INFO] ℹ️ [AUTH-BROKER] [AuthBroker] Token saved for TRIAL: tokenType(jwt), authType(authorization_code), hasRefreshToken(true), expiresAt(2026-09-26T20:15:30.000Z)
247
266
  ```
248
267
 
249
268
  **Note**: Logging only works when a logger is explicitly provided to the broker constructor. The broker will not output anything to console if no logger is passed.
@@ -370,22 +389,25 @@ The `@mcp-abap-adt/auth-broker` package defines **interfaces** and provides **or
370
389
 
371
390
  #### What AuthBroker Does
372
391
 
373
- - **Orchestrates authentication flows**: Coordinates token retrieval, validation, and refresh using provided stores and providers
374
- - **Manages token lifecycle**: Handles token caching, validation, and automatic refresh
375
- - **Works with interfaces only**: Uses `IServiceKeyStore`, `ISessionStore`, and `ITokenProvider` interfaces without knowing concrete implementations
376
- - **Delegates to providers**: Calls `tokenProvider.getTokens()` to obtain tokens
377
- - **Delegates to stores**: Saves tokens and connection configuration to `sessionStore`
392
+ - **Resolves what the stores hold**: the service URL, the UAA credentials, the stored token and refresh token
393
+ - **Builds or reuses the provider**: a factory is called once per destination, seeded with the above
394
+ - **Asks the provider once**: `getTokens()` for `getToken()`, `refreshTokens()` for `refreshToken()` — no retries, no fallbacks
395
+ - **Persists the answer**: token or session cookies, and the refresh token, to `sessionStore`
396
+ - **Works with interfaces only**: `IServiceKeyStore`, `ISessionStore`, `IRefreshableTokenProvider`
378
397
 
379
398
  #### What AuthBroker Does NOT Do
380
399
 
381
400
  - **Does NOT implement storage**: File I/O, parsing, and storage logic are handled by concrete store implementations from `@mcp-abap-adt/auth-stores`
382
401
  - **Does NOT implement token acquisition**: OAuth2 flows, refresh token logic, and client credentials are handled by concrete provider implementations from `@mcp-abap-adt/auth-providers`
402
+ - **Does NOT judge the token**: whether a cached token is still valid, and whether to refresh or log in, is the provider's decision
403
+ - **Does NOT decide how a login is conducted**: browser, headless or pasted code is the provider's authorization strategy
404
+ - **Does NOT copy secrets**: the client secret stays in the service key store
383
405
 
384
406
  ### Consumer Responsibilities
385
407
 
386
408
  The **consumer** (application using `AuthBroker`) is responsible for:
387
409
 
388
- 1. **Selecting appropriate implementations**: Choose the correct `IServiceKeyStore`, `ISessionStore`, and `ITokenProvider` implementations based on the use case:
410
+ 1. **Selecting appropriate implementations**: Choose the correct `IServiceKeyStore`, `ISessionStore`, and `IRefreshableTokenProvider` implementations based on the use case:
389
411
  - **ABAP systems**: Use `AbapServiceKeyStore`, `AbapSessionStore` (or `SafeAbapSessionStore`), and `AuthorizationCodeProvider`
390
412
  - **BTP systems**: Use `AbapServiceKeyStore`, `BtpSessionStore` (or `SafeBtpSessionStore`), and `AuthorizationCodeProvider`
391
413
  - **XSUAA services**: Use `XsuaaServiceKeyStore`, `XsuaaSessionStore` (or `SafeXsuaaSessionStore`), and `ClientCredentialsProvider`
@@ -412,19 +434,21 @@ Concrete `ISessionStore` implementations are responsible for:
412
434
 
413
435
  ### Provider Responsibilities
414
436
 
415
- Concrete `ITokenProvider` implementations are responsible for:
437
+ Concrete `IRefreshableTokenProvider` implementations are responsible for:
416
438
 
417
439
  - **Obtaining tokens**: Using OAuth2 flows, refresh tokens, or client credentials to obtain JWT tokens
418
- - **Managing token lifecycle**: Caching, validating, refreshing, and re-authenticating as needed
440
+ - **Managing token lifecycle**: Caching, validating, refreshing, and re-authenticating as needed (`getTokens()`)
441
+ - **Forcing a new token**: `refreshTokens()` — never the cached one
442
+ - **Reporting failures with typed errors**, which the broker passes on unchanged
419
443
 
420
444
  ### Design Principles
421
445
 
422
446
  1. **Interface-Only Communication** (Core Principle): All interactions with external dependencies happen **ONLY through interfaces**. The code knows **NOTHING beyond what is defined in the interfaces** (see [Core Development Principle](#core-development-principle) above)
423
- 2. **Dependency Inversion Principle (DIP)**: `AuthBroker` depends on abstractions (`IServiceKeyStore`, `ISessionStore`, `ITokenProvider`), not concrete implementations
447
+ 2. **Dependency Inversion Principle (DIP)**: `AuthBroker` depends on abstractions (`IServiceKeyStore`, `ISessionStore`, `IRefreshableTokenProvider`), not concrete implementations
424
448
  3. **Single Responsibility**: Each component has a single, well-defined responsibility:
425
- - `AuthBroker`: Orchestration and token lifecycle management
449
+ - `AuthBroker`: Orchestration — resolving, asking, persisting
426
450
  - `ISessionStore`: Session data storage and retrieval
427
- - `ITokenProvider`: Token acquisition
451
+ - `IRefreshableTokenProvider`: Token acquisition and lifecycle
428
452
  - `IServiceKeyStore`: Service key storage and retrieval
429
453
  4. **Interface Segregation**: Interfaces are focused and minimal, containing only what's necessary for their specific purpose
430
454
  5. **Open/Closed Principle**: New store and provider implementations can be added without modifying `AuthBroker`
@@ -440,138 +464,96 @@ new AuthBroker(
440
464
  config: {
441
465
  sessionStore: ISessionStore; // required
442
466
  serviceKeyStore?: IServiceKeyStore; // optional
443
- tokenProvider: ITokenProvider; // required
444
- allowBrowserAuth?: boolean; // optional
445
- },
446
- browser?: string,
447
- logger?: ILogger
467
+ provider: // required
468
+ | IRefreshableTokenProvider
469
+ | ((
470
+ destination: string,
471
+ authConfig: IAuthorizationConfig | null,
472
+ connConfig: IConnectionConfig,
473
+ ) => IRefreshableTokenProvider);
474
+ },
475
+ logger?: ILogger,
448
476
  )
449
477
  ```
450
478
 
451
479
  **Parameters:**
452
- - `config` - Configuration object:
453
- - `sessionStore` - **Required** - Store for session data. Must contain initial session with `serviceUrl`
454
- - `serviceKeyStore` - **Optional** - Store for service keys. Only needed for initializing sessions from service keys
455
- - `tokenProvider` - **Required** - Token provider for token acquisition and refresh
456
- - `allowBrowserAuth` - **Optional** - When `false`, throws `BROWSER_AUTH_REQUIRED` instead of launching browser auth
457
- - `browser` - Optional browser name for authentication (`chrome`, `edge`, `firefox`, `system`, `headless`, `none`). Default: `system`
458
- - Use `'headless'` for SSH/remote sessions - logs URL and waits for manual callback
459
- - Use `'none'` for automated tests - logs URL and rejects immediately
460
- - For XSUAA, browser is not used (client_credentials grant type) - use `'none'`
461
- - `logger` - Optional logger instance. If not provided, uses no-op logger
462
-
463
- **When to Provide Each Dependency:**
464
-
465
- - **`sessionStore` (required)**: Always required. Must contain initial session with `serviceUrl`
466
- - **`serviceKeyStore` (optional)**:
467
- - Required if you need to initialize sessions from service keys (Step 0)
468
- - Not needed if session already contains authorization config and tokens
469
- - **`tokenProvider` (required)**:
470
- - Used for all token acquisition and refresh flows
471
- - Must be configured with the destination's auth parameters (e.g., UAA credentials)
480
+ - `config.sessionStore` - **Required** - Where tokens and the refresh token are kept. Its `serviceUrl`, or the service key's, is required.
481
+ - `config.serviceKeyStore` - **Optional** - UAA credentials and the service URL.
482
+ - `config.provider` - **Required** - A provider instance, used for every destination, or a factory (`TokenProviderFactory`), called once per destination and seeded with what the stores hold (see *Basic Usage*).
483
+ - `logger` - Optional logger. If not provided, nothing is logged.
472
484
 
473
485
  **Available Implementations:**
474
- - **ABAP**: `AbapServiceKeyStore(directory, defaultServiceUrl?, logger?)`, `AbapSessionStore(directory, defaultServiceUrl?, logger?)`, `SafeAbapSessionStore(defaultServiceUrl?, logger?)`, `AuthorizationCodeProvider(...)`
486
+ - **ABAP**: `AbapServiceKeyStore(directory, logger?)`, `AbapSessionStore(directory, logger?, defaultServiceUrl?)`, `SafeAbapSessionStore(logger?, defaultServiceUrl?)`, `AuthorizationCodeProvider(...)`
475
487
  - **XSUAA** (reduced scope): `XsuaaServiceKeyStore(directory, logger?)`, `XsuaaSessionStore(directory, defaultServiceUrl, logger?)`, `SafeXsuaaSessionStore(defaultServiceUrl, logger?)`, `ClientCredentialsProvider(...)`
476
- - **BTP** (full scope for ABAP): `AbapServiceKeyStore(directory, defaultServiceUrl?, logger?)`, `BtpSessionStore(directory, defaultServiceUrl, logger?)`, `SafeBtpSessionStore(defaultServiceUrl, logger?)`, `AuthorizationCodeProvider(...)`
477
488
 
478
489
  #### Methods
479
490
 
480
491
  ##### `getToken(destination: string): Promise<string>`
481
492
 
482
- Gets authentication token for destination. Implements a three-step flow:
483
-
484
- **Step 0: Initialize Session with Token (if needed)**
485
- - Checks if session has `authorizationToken` and authorization config
486
- - If both are missing and `serviceKeyStore` is available:
487
- - Loads authorization config from service key
488
- - Uses `tokenProvider.getTokens()` to obtain tokens
489
- - Persists tokens to session
490
- - Otherwise → proceeds to Step 1
491
-
492
- **Step 1: Token Refresh / Re-Auth**
493
- - If session has authorization config:
494
- - Uses `tokenProvider.getTokens()` to refresh or re-authenticate
495
- - Persists tokens to session
496
- - Returns new token
497
- - If that fails (or no session auth config) and `serviceKeyStore` is available:
498
- - Loads authorization config from service key
499
- - Uses `tokenProvider.getTokens()` to obtain tokens
500
- - Persists tokens to session
501
- - If all failed → throws error
502
-
503
- **Important Notes:**
504
- - All authentication is handled by the injected provider (authorization_code or client_credentials).
505
- - `tokenProvider` is required for all token acquisition and refresh flows.
506
- - **Broker always calls `provider.getTokens()`** - provider handles token lifecycle internally (validation, refresh, login). Consumer doesn't need to know about token issues.
507
- - Provider decides whether to return cached token, refresh, or perform login based on token state.
508
- - **Store errors are handled gracefully**: If service key files are missing or malformed, the broker logs the error and continues with fallback mechanisms (session store data or provider-based auth)
493
+ 1. Resolves the destination's `serviceUrl` (session, else service key; an error if neither has one).
494
+ 2. Builds the provider on first use (factory form), seeded with the credentials, the stored refresh token and the stored token — or uses the instance.
495
+ 3. Calls `provider.getTokens()` once. The provider answers from its cache, refreshes, or logs in.
496
+ 4. Persists the result: `sessionCookies` for `tokenType: 'saml'`, else `authorizationToken`; the refresh token when the result has one.
497
+ 5. Returns the token.
498
+
499
+ ##### `refreshToken(destination: string): Promise<string>`
500
+
501
+ The same, with `provider.refreshTokens()`: a new token, never the cached one —
502
+ for a caller whose token the server has just refused.
503
+
504
+ ##### `getAuthorizationConfig(destination)` / `getConnectionConfig(destination)`
505
+
506
+ The session's configuration, else the service key's, else `null`.
507
+
508
+ ##### `createTokenRefresher(destination): ITokenRefresher`
509
+
510
+ `getToken()` and `refreshToken()` bound to one destination, for injection into a connection.
509
511
 
510
512
  ##### Error Handling
511
513
 
512
- The broker implements comprehensive error handling for all external operations, treating all injected dependencies as untrusted:
514
+ - **Provider errors propagate unchanged** — the same object, with its class,
515
+ `code`, `missingFields` and `cause`: auth-providers' `ValidationError`,
516
+ `BrowserAuthError`, `AssertionValidationError`, network errors (`ECONNREFUSED`,
517
+ `ETIMEDOUT`, `ENOTFOUND`), and whatever your authorization strategy throws.
518
+ The broker does not retry a failed call.
519
+ - **Store reads: absence is an answer, a failure is not.** A store that answers
520
+ `null`, or fails with `FILE_NOT_FOUND` (logged at debug), means "nothing
521
+ here", and the broker goes on to the next source. Any other store failure —
522
+ a service key that is not valid JSON, a file the process may not read — is
523
+ thrown unchanged. A failure in the reads that come before the token (the
524
+ session's connection config, the service key) stops the call before the
525
+ provider is asked; one in the reads that save it (the session's
526
+ authorization config, the session) arrives after the provider answered and
527
+ the new token was written. (The session stores of
528
+ auth-stores answer an unreadable session file with `null` themselves, so it
529
+ reads as absent before the broker sees it.)
530
+ - **Store writes propagate**: a token that cannot be saved is an error.
531
+ - **A provider result without a token** is an error.
513
532
 
514
533
  ```typescript
515
- import { STORE_ERROR_CODES } from '@mcp-abap-adt/interfaces-auth';
534
+ import { ValidationError } from '@mcp-abap-adt/auth-providers';
516
535
 
517
536
  try {
518
537
  const token = await broker.getToken('TRIAL');
519
- } catch (error: any) {
520
- // Broker handles errors internally where possible, but critical errors propagate
521
- console.error('Failed to get token:', error.message);
538
+ } catch (error) {
539
+ if (error instanceof ValidationError) {
540
+ logger.error(`Missing: ${error.missingFields?.join(', ')}`);
541
+ }
542
+ throw error;
522
543
  }
523
544
  ```
524
545
 
525
- **Error Categories** (handled by broker with graceful degradation):
526
-
527
- **1. SessionStore Errors** (reading session files):
528
- - `STORE_ERROR_CODES.FILE_NOT_FOUND` - Session file missing (logged, tries serviceKeyStore fallback)
529
- - `STORE_ERROR_CODES.PARSE_ERROR` - Corrupted session file (logged with file path, tries fallback)
530
- - Write failures when saving tokens (logged and thrown - critical)
531
-
532
- **2. ServiceKeyStore Errors** (reading service key files):
533
- - `STORE_ERROR_CODES.FILE_NOT_FOUND` - Service key file missing (logged, continues with session data)
534
- - `STORE_ERROR_CODES.PARSE_ERROR` - Invalid JSON in service key (logged with file path and cause)
535
- - `STORE_ERROR_CODES.INVALID_CONFIG` - Missing required fields (logged with missing field names)
536
- - `STORE_ERROR_CODES.STORAGE_ERROR` - Permission/write errors (logged)
537
-
538
- **3. TokenProvider Errors** (network operations):
539
- - Network errors: `ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND` (logged, throws with descriptive message)
540
- - `VALIDATION_ERROR` - Missing required auth fields (logged with field names, throws)
541
- - `BROWSER_AUTH_ERROR` - Browser authentication failed or cancelled (logged, throws)
542
- - `REFRESH_ERROR` - Token refresh failed at UAA server (logged, throws)
543
-
544
- **4. Browser Auth Disabled Errors** (when `allowBrowserAuth: false`):
545
- - `BROWSER_AUTH_REQUIRED` - Browser authentication is required but disabled. Thrown when:
546
- - **Step 0**: No token and no UAA credentials in session, service key exists but browser auth needed
547
- - **Step 2b**: Refresh token expired/invalid and browser auth needed for new token
548
- - Error includes `destination` property for context
549
- - Use case: Non-interactive environments (MCP stdio, Cline) where browser cannot open
550
-
551
- **Defensive Design Principles:**
552
- - **All external operations wrapped in try-catch**: Files may be missing/corrupted, network may fail
553
- - **Graceful degradation**: Store errors trigger fallback mechanisms (serviceKey → session → provider)
554
- - **Detailed error context**: Logs include file paths, error codes, missing fields for debugging
555
- - **Fail-fast for critical errors**: Write failures and provider errors throw immediately (cannot recover)
556
- - **No assumptions about injected dependencies**: All stores/providers treated as potentially unreliable
557
-
558
- Example error scenarios handled:
559
- - Session file deleted mid-operation → uses service key
560
- - Service key has invalid JSON → logs parse error, uses session data
561
- - Network timeout during token refresh → logs timeout, throws descriptive error
562
- - File permission denied → logs error with file path, throws
563
-
564
- ##### `refreshToken(destination: string): Promise<string>`
565
-
566
- Force refresh token for destination. Calls `getToken()` to run the full refresh flow and persist updated tokens.
546
+ #### Secrets in the Session Store
567
547
 
568
- ##### `clearCache(destination: string): void`
548
+ The broker writes the token (or session cookies) and the refresh token to the
549
+ session store — never the client secret. When the credentials come from the
550
+ service key they stay there. A session that already holds its own credentials
551
+ (written by you, or by `mcp-auth`/`mcp-sso`, whose output is a self-contained
552
+ session file) keeps them, and only its refresh token is updated.
569
553
 
570
- Clear cached token for specific destination.
571
-
572
- ##### `clearAllCache(): void`
573
-
574
- Clear all cached tokens.
554
+ With credentials in the service key, the stores return the stored refresh
555
+ token through `loadSession()`, which the broker reads to seed the next
556
+ process's provider — the XSUAA stores too, from auth-stores 1.2.3.
575
557
 
576
558
  ### Token Providers
577
559
 
@@ -597,61 +579,25 @@ The package uses the `ITokenProvider` interface for token acquisition. Provider
597
579
  **Example Usage:**
598
580
 
599
581
  ```typescript
582
+ import { AuthBroker } from '@mcp-abap-adt/auth-broker';
600
583
  import {
601
- AuthBroker,
602
584
  XsuaaServiceKeyStore,
603
585
  XsuaaSessionStore,
604
- AbapServiceKeyStore,
605
- BtpSessionStore
606
- } from '@mcp-abap-adt/auth-broker';
607
- import {
608
- ClientCredentialsProvider,
609
- AuthorizationCodeProvider,
610
- browserCallbackStrategy,
611
- } from '@mcp-abap-adt/auth-providers';
586
+ } from '@mcp-abap-adt/auth-stores';
587
+ import { ClientCredentialsProvider } from '@mcp-abap-adt/auth-providers';
612
588
 
613
- // XSUAA authentication
589
+ // XSUAA, client_credentials: credentials from the service key
614
590
  const xsuaaBroker = new AuthBroker({
615
- sessionStore: new XsuaaSessionStore('/path/to/sessions', 'https://mcp.example.com'),
616
- tokenProvider: new ClientCredentialsProvider({
617
- uaaUrl: 'https://auth.example.com',
618
- clientId: '...',
619
- clientSecret: '...',
620
- }),
621
- });
622
-
623
- // XSUAA authentication - with service key initialization
624
- const xsuaaBrokerWithServiceKey = new AuthBroker({
625
591
  sessionStore: new XsuaaSessionStore('/path/to/sessions', 'https://mcp.example.com'),
626
592
  serviceKeyStore: new XsuaaServiceKeyStore('/path/to/keys'),
627
- tokenProvider: new ClientCredentialsProvider({
628
- uaaUrl: 'https://auth.example.com',
629
- clientId: '...',
630
- clientSecret: '...',
631
- }),
632
- }, 'none');
633
-
634
- // BTP authentication
635
- const btpBroker = new AuthBroker({
636
- sessionStore: new BtpSessionStore('/path/to/sessions', 'https://abap.example.com'),
637
- tokenProvider: new AuthorizationCodeProvider({
638
- uaaUrl: 'https://auth.example.com',
639
- clientId: '...',
640
- clientSecret: '...',
641
- authorization: browserCallbackStrategy({ browser: 'system' }),
642
- }),
643
- });
644
-
645
- // BTP authentication - with service key and provider (for browser auth)
646
- const btpBrokerFull = new AuthBroker({
647
- sessionStore: new BtpSessionStore('/path/to/sessions', 'https://abap.example.com'),
648
- serviceKeyStore: new AbapServiceKeyStore('/path/to/keys'),
649
- tokenProvider: new AuthorizationCodeProvider({
650
- uaaUrl: 'https://auth.example.com',
651
- clientId: '...',
652
- clientSecret: '...',
653
- authorization: browserCallbackStrategy({ browser: 'system' }),
654
- }),
593
+ provider: (destination, authConfig) => {
594
+ if (!authConfig) throw new Error(`No UAA credentials for ${destination}`);
595
+ return new ClientCredentialsProvider({
596
+ uaaUrl: authConfig.uaaUrl,
597
+ clientId: authConfig.uaaClientId,
598
+ clientSecret: authConfig.uaaClientSecret,
599
+ });
600
+ },
655
601
  });
656
602
  ```
657
603
 
@@ -683,6 +629,22 @@ redirect URI with a specific port at your identity provider, pass `--redirect-po
683
629
  A login is given 5 minutes to complete (this is a person switching to a browser and signing in
684
630
  by hand, not an unattended caller).
685
631
 
632
+ **SAML options (`saml2-pure`, `saml2-bearer`):**
633
+ `mcp-auth` hands these subcommands to `mcp-sso` with every argument unchanged, so the SAML options
634
+ are `mcp-sso`'s (see *CLI: mcp-sso* and *SAML assertion validation* below) and its exit code is
635
+ `mcp-auth`'s. The ones a run needs:
636
+
637
+ | Option | What it is |
638
+ |---|---|
639
+ | `--idp-metadata <url\|path>` | The identity provider's SAML metadata; fills `--idp-cert`, `--idp-entity-id` and `--idp-sso-url`. For SAP Cloud Identity Services: `https://<tenant>.accounts.ondemand.com/saml2/metadata`. |
640
+ | `--idp-cert <path>`, `--idp-entity-id <id>` | The same trust, stated instead of read. |
641
+ | `--idp-initiated` | The identity provider starts the login. `saml2-bearer` against XSUAA needs it. |
642
+ | `--sp-entity-id`, `--acs-url` | The `Audience` and `Recipient`. For `saml2-bearer` with `--service-key`, read from `<uaa.url>/saml/metadata`. |
643
+ | `--assertion <base64>`, `--assertion-flow <flow>` | A `SAMLResponse` obtained elsewhere, or how to obtain one. |
644
+ | `--authn-request-id <id>` | The request an `--assertion` answers, when `mcp-sso` did not send it. |
645
+
646
+ `saml2-bearer` still requires `--dev`: it has not been run against a live XSUAA with a SAML trust.
647
+
686
648
  **Examples:**
687
649
  ```bash
688
650
  # Auth code (default via service key)
@@ -691,11 +653,11 @@ mcp-auth auth-code --service-key ./abap.json --output ./abap.env --type abap
691
653
  # OIDC SSO (device flow example)
692
654
  mcp-auth oidc --flow device --issuer https://issuer --client-id my-client --output ./sso.env --type xsuaa
693
655
 
694
- # SAML2 pure (cookie)
695
- mcp-auth saml2-pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --output ./saml.env --type abap
656
+ # SAML2 pure (cookie); the SAML flags are mcp-sso's, see "SAML assertion validation" below
657
+ mcp-auth saml2-pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --idp-cert ./idp-signing.pem --idp-entity-id https://idp.example/metadata --output ./saml.env --type abap
696
658
 
697
659
  # SAML2 bearer (in progress, requires --dev)
698
- mcp-auth saml2-bearer --dev --service-key ./mcp.json --assertion <base64> --output ./sso.env --type xsuaa
660
+ mcp-auth saml2-bearer --dev --service-key ./mcp.json --idp-metadata https://<ias-tenant>.accounts.ondemand.com/saml2/metadata --idp-initiated --output ./sso.env --type xsuaa
699
661
 
700
662
  # ABAP: authorization_code (default, opens browser)
701
663
  mcp-auth --service-key ./abap.json --output ./abap.env --type abap
@@ -753,18 +715,62 @@ mcp-sso oidc --flow password --token-endpoint https://issuer/oauth/token --clien
753
715
  # OIDC token exchange
754
716
  mcp-sso oidc --flow token_exchange --issuer https://issuer --client-id my-client --subject-token <token> --output ./sso.env --type xsuaa
755
717
 
756
- # SAML bearer flow (assertion -> token)
757
- mcp-sso bearer --idp-sso-url https://idp/sso --sp-entity-id my-sp --token-endpoint https://uaa.example/oauth/token --assertion <base64> --output ./sso.env --type xsuaa
718
+ # SAML bearer flow against XSUAA with a service key: the Audience, Recipient and token alias
719
+ # come from <uaa.url>/saml/metadata, the IdP's trust from its own metadata
720
+ mcp-sso bearer --service-key ./service-key.json --idp-metadata https://<ias-tenant>.accounts.ondemand.com/saml2/metadata --idp-initiated --output ./sso.env --type xsuaa
758
721
 
759
- # SAML pure flow (cookie)
760
- mcp-sso saml2 --flow pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --assertion <base64> --cookie "SAP_SESSION=..." --output ./sso.env --type abap
722
+ # The same, every value stated (IdP-initiated assertion -> token)
723
+ mcp-sso bearer --idp-sso-url https://idp/sso --sp-entity-id <uaa-entity-id> --acs-url <uaa-bearer-acs> --idp-cert ./idp-signing.pem --idp-entity-id https://idp.example/metadata --idp-initiated --token-endpoint https://uaa.example/oauth/token --assertion <base64> --output ./sso.env --type xsuaa
724
+
725
+ # SAML pure flow (cookie; SP-initiated browser login, the request is sent by mcp-sso)
726
+ mcp-sso saml2 --flow pure --idp-sso-url https://idp/sso --sp-entity-id my-sp --idp-cert ./idp-signing.pem --idp-entity-id https://idp.example/metadata --cookie "SAP_SESSION=..." --output ./sso.env --type abap
761
727
  ```
762
728
 
763
- **SAML token alias (XSUAA):**
764
- If your IdP requires the token alias endpoint, pass SAML metadata XML:
729
+ **SAML assertion validation:**
730
+ Both SAML flows validate every assertion before using it — signature, issuer, audience,
731
+ recipient, time window, request ID and replay (done by `@mcp-abap-adt/auth-providers` 4; see its
732
+ README, *SAML assertion validation*). The provider will not even be constructed without the
733
+ trust it checks against, and `mcp-sso` invents none of it — it is stated, or read from SAML
734
+ metadata:
735
+
736
+ | Option | `--config` field | What it is |
737
+ |---|---|---|
738
+ | `--idp-cert <path>` (repeatable) | `idpCertificates` (string or list, inline PEM or base64 DER) | The identity provider's signing certificate(s). A file may be PEM (one or several certificates) or binary DER. Repeat the flag, or list several, to trust both keys during a rotation. |
739
+ | `--idp-entity-id <id>` | `idpEntityId` | The identity provider's `entityID` — the `Issuer` its assertions carry. |
740
+ | `--idp-metadata <url\|path>` | `idpMetadata` | The identity provider's SAML metadata (for SAP Cloud Identity Services `https://<tenant>.accounts.ondemand.com/saml2/metadata`). Fills the two rows above and `--idp-sso-url` where not given: signing keys and keys without `use`, never encryption keys. An https URL or a file; plain http only for loopback. Federation metadata (an `EntitiesDescriptor` of several entities) works too: entity ID, keys and SSO URL all come from the same identity provider, which `--idp-entity-id` names when there are several — without it such a run stops and lists them. |
741
+ | `--sp-entity-id <id>` | `spEntityId` | Already required; it is now also the `Audience` the assertion must name. For bearer against UAA/XSUAA, the `entityID` in their SAML metadata. |
742
+ | `--acs-url <url>` | `acsUrl` | The `Recipient` the assertion must name. For bearer against UAA/XSUAA, the token endpoint's bearer ACS; the default `http://localhost:<port>/callback` fits only a login delivered to this CLI. |
743
+ | `--idp-initiated` | `idpInitiated` (`true`/`false`) | The identity provider starts the login and no AuthnRequest is sent, so the assertion must carry no `InResponseTo`. |
744
+ | `--authn-request-id <id>` | `authnRequestId` | The AuthnRequest ID an `--assertion` answers, when the request was sent by something other than `mcp-sso`. |
745
+
746
+ A `--idp-cert` on the command line replaces the file's `idpCertificates` rather than adding to
747
+ them, so a certificate retired on the command line is not still trusted from the file.
748
+
749
+ Which request setting a run needs:
750
+
751
+ - **Browser or manual login, SP-initiated** (`--assertion-flow browser`, the default, or
752
+ `manual`): nothing — `mcp-sso` builds the AuthnRequest and knows its ID.
753
+ - **`--assertion <base64>`** from an SP-initiated login sent elsewhere: `--authn-request-id`.
754
+ - **IdP-initiated** — required for `bearer` against UAA or XSUAA, whose saml2-bearer grant refuses
755
+ an assertion carrying `InResponseTo`: `--idp-initiated`, with `--assertion`, or with
756
+ `--assertion-flow manual` (the default under `--idp-initiated`), which asks you to start the
757
+ login at the identity provider and paste the `SAMLResponse` it posts. `--idp-initiated` with
758
+ `--assertion-flow browser` is refused, since there is no request URL to open.
759
+
760
+ `--idp-initiated` together with `--authn-request-id`, a missing certificate or entity ID, or an
761
+ assertion that fails a check is reported by `auth-providers` itself (`ValidationError` or
762
+ `AssertionValidationError`), with the field or the check it refused.
763
+
764
+ **XSUAA's side of a bearer run:**
765
+ None of `--sp-entity-id`, `--acs-url` and the bearer token endpoint is in an XSUAA service key, but
766
+ XSUAA publishes all three in its SAML metadata: its `entityID` is the `Audience`, and its
767
+ `/oauth/token/alias/<alias>` endpoint is both the `Recipient` and where the assertion is exchanged.
768
+ With `--service-key`, `bearer` reads `<uaa.url>/saml/metadata` and fills whichever of them was not
769
+ given. Without network access to it, pass the file (from *Security > Trust Configuration >
770
+ Download SAML Metadata* in the subaccount):
765
771
 
766
772
  ```bash
767
- mcp-sso bearer --saml-metadata ./saml-sp.xml --assertion <base64> --service-key ./service-key.json --output ./sso.env --type xsuaa
773
+ mcp-sso bearer --saml-metadata ./saml-sp.xml --idp-sso-url https://idp/sso --sp-entity-id <uaa-entity-id> --acs-url <uaa-bearer-acs> --idp-cert ./idp-signing.pem --idp-entity-id https://idp.example/metadata --idp-initiated --assertion <base64> --service-key ./service-key.json --output ./sso.env --type xsuaa
768
774
  ```
769
775
 
770
776
  ### Local Keycloak (OIDC + SAML Tests)
@@ -819,6 +825,40 @@ are. A file that sets `authorizationCodeProvider`, `assertionProvider`, or `manu
819
825
  functions, which JSON cannot express — is refused with an error naming the CLI flag to use
820
826
  instead, rather than having the field silently dropped.
821
827
 
828
+ A SAML config file carries the trust inline:
829
+
830
+ ```json
831
+ {
832
+ "protocol": "saml2",
833
+ "flow": "bearer",
834
+ "idpSsoUrl": "https://idp.example/sso",
835
+ "spEntityId": "https://uaa.example/entity",
836
+ "acsUrl": "https://uaa.example/oauth/token/alias/example",
837
+ "idpEntityId": "https://idp.example/metadata",
838
+ "idpCertificates": ["MIIC...base64 DER from the IdP metadata's <X509Certificate>..."],
839
+ "idpInitiated": true,
840
+ "assertionFlow": "manual"
841
+ }
842
+ ```
843
+
844
+ #### Migrating `mcp-sso` SAML runs from 2.2.0
845
+
846
+ 2.2.0 used `@mcp-abap-adt/auth-providers` 2.x, which trusted any SAML payload it was handed.
847
+ With 4.x every `mcp-sso` SAML run (`bearer`, `saml2 --flow pure`, and `mcp-auth saml2-pure` /
848
+ `saml2-bearer`, which call it) fails before login until you add:
849
+
850
+ 1. `--idp-metadata <url|path>`, or `--idp-cert <path>` and `--idp-entity-id <id>` (or
851
+ `idpCertificates` and `idpEntityId` in `--config`) — without them the provider refuses to
852
+ construct.
853
+ 2. The real `--sp-entity-id` (the `Audience`) and, unless the assertion is delivered to this CLI's
854
+ own callback, the `--acs-url` it names as `Recipient`. For `bearer` with `--service-key` both
855
+ are read from XSUAA's metadata.
856
+ 3. For `bearer` against UAA or XSUAA: `--idp-initiated`, with `--assertion` or
857
+ `--assertion-flow manual`. For any other `--assertion` from an SP-initiated login:
858
+ `--authn-request-id`.
859
+
860
+ Node.js 22 or 24 is required.
861
+
822
862
  ### Utility Script
823
863
 
824
864
  Generate `.env` files from service keys: