@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/README.md
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
# @mcp-abap-adt/auth-broker
|
|
2
2
|
[](https://stand-with-ukraine.pp.ua)
|
|
3
3
|
|
|
4
|
-
|
|
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
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
34
|
+
### Basic Usage
|
|
50
35
|
|
|
51
|
-
|
|
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-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
75
|
+
The factory receives:
|
|
90
76
|
|
|
91
|
-
|
|
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/
|
|
96
|
-
|
|
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
|
-
|
|
93
|
+
> `AuthorizationCodeProvider` and the other `@mcp-abap-adt/auth-providers`
|
|
94
|
+
> providers implement `IRefreshableTokenProvider` from auth-providers 4.2.0.
|
|
107
95
|
|
|
108
|
-
|
|
96
|
+
### Headless Processes (No Browser)
|
|
109
97
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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():
|
|
173
|
-
// - refreshToken():
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
**
|
|
233
|
-
|
|
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]
|
|
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
|
-
- **
|
|
374
|
-
- **
|
|
375
|
-
- **
|
|
376
|
-
- **
|
|
377
|
-
- **
|
|
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 `
|
|
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 `
|
|
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`, `
|
|
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
|
|
449
|
+
- `AuthBroker`: Orchestration — resolving, asking, persisting
|
|
426
450
|
- `ISessionStore`: Session data storage and retrieval
|
|
427
|
-
- `
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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` -
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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,
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
605
|
-
|
|
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
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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 --
|
|
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
|
|
757
|
-
|
|
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
|
-
#
|
|
760
|
-
mcp-sso
|
|
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
|
|
764
|
-
|
|
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:
|