@mcp-abap-adt/auth-broker 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +135 -0
- package/README.md +379 -5
- package/dist/AuthBroker.d.ts +78 -8
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +204 -31
- package/dist/binding.d.ts +28 -6
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +20 -4
- package/dist/bindingOf.d.ts +2 -1
- package/dist/bindingOf.d.ts.map +1 -1
- package/dist/clientAuthentication.d.ts +109 -0
- package/dist/clientAuthentication.d.ts.map +1 -0
- package/dist/clientAuthentication.js +218 -0
- package/dist/destinations.d.ts +35 -10
- package/dist/destinations.d.ts.map +1 -1
- package/dist/destinations.js +61 -17
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,141 @@ Thank you to all contributors! See [CONTRIBUTORS.md](../../CONTRIBUTORS.md) for
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [4.1.0] - 2026-10-05
|
|
15
|
+
|
|
16
|
+
**x509 service keys.** A client that authenticates to the authorization
|
|
17
|
+
server with a certificate instead of a secret — an XSUAA service key created
|
|
18
|
+
with `{"credential-type": "x509"}` — when the consumer says so, through a
|
|
19
|
+
strategy. Additive: without the new option every destination is served
|
|
20
|
+
exactly as by 4.0.0, and nothing certificate-related is read. Released with
|
|
21
|
+
`@mcp-abap-adt/auth-broker-cli` 2.1.0.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`AuthBrokerConfig.clientAuthentication`** — a `ClientAuthenticationStrategy`
|
|
26
|
+
`(context) => Promise<IClientAuthentication>`: how the client of every grant
|
|
27
|
+
that authenticates one (`authorization_code`, `client_credentials`,
|
|
28
|
+
`passcode`, `oidc_authorization_code`, `device_code`, `password`,
|
|
29
|
+
`token_exchange`, `saml2_bearer` — `ClientAuthenticationGrant`)
|
|
30
|
+
authenticates. Called once per build with a `ClientAuthenticationContext`
|
|
31
|
+
(`destination`, `grant`, the key store's secret `client` — `uaaUrl`,
|
|
32
|
+
`uaaClientId`, `uaaClientSecret` only, never a refresh token — or `null`, and a
|
|
33
|
+
lazy, memoised `readCertificate()`); its answer goes to the provider as
|
|
34
|
+
`clientAuthentication`, with no client secret. No default and no "nothing"
|
|
35
|
+
answer: a stated choice never falls back to the secret.
|
|
36
|
+
- **`fromServiceKeyCertificate()`** — auth-providers' `tlsClientCertificate`
|
|
37
|
+
from the key store's certificate client, against `<certUrl>/oauth/token`
|
|
38
|
+
(a trailing `/` of `certUrl` dropped), the material checked before it
|
|
39
|
+
answers. **`fromServiceKeySecret({ encoding: 'raw' | 'form' })`** —
|
|
40
|
+
`clientSecretBasic` with the secret client; `encoding` required (`raw` for
|
|
41
|
+
XSUAA, measured). Each fails closed; a fallback is the consumer's
|
|
42
|
+
composition, decided by what the key holds (`readCertificate()`), never by
|
|
43
|
+
catching a refusal.
|
|
44
|
+
- **The certificate client** is read through the optional
|
|
45
|
+
`IServiceKeyStore.getClientCertificate` (`@mcp-abap-adt/interfaces-auth-broker`
|
|
46
|
+
1.2.0) — auth-stores 3.3.0 answers it for an x509 XSUAA key and for an
|
|
47
|
+
`EnvDestinationStore` stating `SAP_UAA_CLIENT_CERT_PATH`,
|
|
48
|
+
`SAP_UAA_CLIENT_KEY_PATH` and `SAP_UAA_CERT_URL` (`XSUAA_UAA_*`) — only
|
|
49
|
+
when the strategy asks, at most once per build. The row's client is then
|
|
50
|
+
its identity: the secret client's `uaaUrl` / `uaaClientId`, else the
|
|
51
|
+
certificate client's.
|
|
52
|
+
- **The token API factory's fourth argument, `TokenProviderClient`** —
|
|
53
|
+
beside a strategy, for a grant that authenticates a client: the strategy's
|
|
54
|
+
`clientAuthentication`, the client identity (`uaaUrl`, `clientId`) and the
|
|
55
|
+
stored `refreshToken` when the session is bound here — taken only from the
|
|
56
|
+
session read whose binding was checked, never from a client's own read.
|
|
57
|
+
Never a certificate, key or secret. Additive: a 4.0.0 factory keeps working.
|
|
58
|
+
- **Fixed-words refusals**: whatever a strategy, a store it reads or the
|
|
59
|
+
certificate check throws — and an answer that is no `IClientAuthentication`
|
|
60
|
+
— is a `DestinationConfigError` naming `clientAuthentication` before any
|
|
61
|
+
provider exists, its words chosen by the error's class (no certificate, no
|
|
62
|
+
secret, incomplete / expired / unusable certificate, no client
|
|
63
|
+
authentication, the certificate could not be read, the strategy failed); a
|
|
64
|
+
factory that throws beside a strategy is one naming `provider`,
|
|
65
|
+
`clientAuthentication`. No `cause`, nothing of the thrown message, no PEM
|
|
66
|
+
anywhere — log, refusal, error, session store.
|
|
67
|
+
- Exports: `fromServiceKeyCertificate`, `fromServiceKeySecret`, and the
|
|
68
|
+
types `ClientAuthenticationStrategy`, `ClientAuthenticationContext`,
|
|
69
|
+
`ClientAuthenticationGrant`, `FromServiceKeySecretOptions`,
|
|
70
|
+
`TokenProviderClient`, `IClientCertificate`, `IClientAuthentication`.
|
|
71
|
+
- `npm run test:live:x509`: the live check against a BTP subaccount (exact
|
|
72
|
+
`cf target` guard, a GUID ledger, teardown also on failure). Not in CI.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- **A client row with no client and no strategy names the way out.** The
|
|
77
|
+
`DestinationConfigError` of a UAA, OIDC or `saml2_bearer` destination whose
|
|
78
|
+
key store answers no client (or one without a client id) keeps its
|
|
79
|
+
`missingFields`; its message adds `; a certificate client needs a
|
|
80
|
+
clientAuthentication strategy`.
|
|
81
|
+
|
|
82
|
+
### On the strategy path (only with `clientAuthentication`)
|
|
83
|
+
|
|
84
|
+
- **The binding is the client's identity**: `issuedBy` from the identity's
|
|
85
|
+
`uaaUrl` and `client_id`, so a certificate destination's tokens are reused
|
|
86
|
+
after the broker is recreated and dropped when the issuer or client id
|
|
87
|
+
changes — and a secret key and an x509 key of the same client id share one
|
|
88
|
+
session (switching keeps its token and refresh token).
|
|
89
|
+
- **A resource neither side states matches**: means with no `serviceUrl`, or
|
|
90
|
+
one that does not parse (the CLI's XSUAA placeholder), and a session stored
|
|
91
|
+
without `issuedFor`; a resource stated on one side only never matches.
|
|
92
|
+
Without a strategy an absent resource still matches nothing.
|
|
93
|
+
- **The token API with a factory**: the destination must state `authType`
|
|
94
|
+
(`jwt` / `saml`) and `grantType` (else `DestinationConfigError` naming
|
|
95
|
+
both); `saml2_pure` and `none` get three arguments; every stored secret —
|
|
96
|
+
the refresh token in `authConfig` and the fourth argument, the token,
|
|
97
|
+
cookies and expiry in `connConfig` — reaches the factory only when the
|
|
98
|
+
session is bound here, and a result without a refresh token carries forward
|
|
99
|
+
only a bound one. (Without a strategy, 4.0.0's carry-over stands.) An
|
|
100
|
+
instance `provider` gets no strategy. What the strategy and the factory
|
|
101
|
+
are handed is built from allowlists, never a store's answer passed whole:
|
|
102
|
+
the client (`uaaUrl`, `uaaClientId`, `uaaClientSecret`), the certificate
|
|
103
|
+
client (its five declared fields), and the seed (`serviceUrl`, `sapClient`,
|
|
104
|
+
`language`, `authType`, `grantType`, and the session secret's fields only
|
|
105
|
+
from a bound read).
|
|
106
|
+
|
|
107
|
+
### Dependencies
|
|
108
|
+
|
|
109
|
+
`@mcp-abap-adt/interfaces-auth-broker` `^1.2.0` (was `^1.1.0`),
|
|
110
|
+
`@mcp-abap-adt/interfaces-auth` `^3.2.0` (was `^3.0.0`),
|
|
111
|
+
`@mcp-abap-adt/auth-providers` `^5.3.0` (was `^5.2.1`); dev:
|
|
112
|
+
`@mcp-abap-adt/auth-stores` `^3.3.0`.
|
|
113
|
+
|
|
114
|
+
### Migrating from 4.0.0
|
|
115
|
+
|
|
116
|
+
Nothing is required. Code that matched a `DestinationConfigError`'s message
|
|
117
|
+
for a destination without a client should match `missingFields` or the class
|
|
118
|
+
instead. To use an x509 key: auth-stores `^3.3.0` and
|
|
119
|
+
`clientAuthentication: fromServiceKeyCertificate()` — then read the strategy
|
|
120
|
+
path's rules above (identity binding, bound secrets only, the token API's
|
|
121
|
+
grant refusal, the factory's fourth argument). See *Migrating from 4.0.0* in
|
|
122
|
+
the README.
|
|
123
|
+
|
|
124
|
+
### Measured, and not
|
|
125
|
+
|
|
126
|
+
Measured: `client_credentials` with an x509 XSUAA key on a BTP trial,
|
|
127
|
+
2026-10-05 (`credential-types: ["binding-secret", "x509"]`) — the broker
|
|
128
|
+
(`getProvider` and the token API), `mcp-auth --client-auth certificate` and
|
|
129
|
+
`generate-env-from-service-key --client-auth certificate`, each followed by a
|
|
130
|
+
fresh broker over the written destination; all five live cases passed. Not
|
|
131
|
+
measured: `authorization_code` and `passcode` over x509, the OIDC grants and
|
|
132
|
+
`saml2_bearer` with a strategy, ABAP environment service keys with x509.
|
|
133
|
+
|
|
134
|
+
### Known limitations
|
|
135
|
+
|
|
136
|
+
- **The certificate is pinned for the broker's lifetime.** It is read when a
|
|
137
|
+
destination's provider is built, and the provider is cached per
|
|
138
|
+
destination for as long as the `AuthBroker` lives. An XSUAA x509
|
|
139
|
+
certificate lives about seven days: a long-running process then gets "the
|
|
140
|
+
client certificate has expired" until it builds a new `AuthBroker`, and
|
|
141
|
+
replacing the key or the PEM files changes nothing before that. Rotation is
|
|
142
|
+
out of scope.
|
|
143
|
+
- The certificate refusals use the broker's own three phrases (incomplete /
|
|
144
|
+
expired / could not be used), classified by auth-providers'
|
|
145
|
+
`CertificateMaterialError` flags; the provider's own hint for each is not
|
|
146
|
+
relayed. auth-providers' exported refusal words (a later minor) would
|
|
147
|
+
replace them.
|
|
148
|
+
|
|
14
149
|
## [4.0.0] - 2026-10-02
|
|
15
150
|
|
|
16
151
|
**What 4.0.0 is.** The broker hands a process **the credential for a
|
package/README.md
CHANGED
|
@@ -13,10 +13,13 @@ where means and secrets live is the stores' (`@mcp-abap-adt/auth-stores`, or you
|
|
|
13
13
|
|
|
14
14
|
**Upgrading from 3.x?** See [*Migrating from 3.x*](#migrating-from-3x) — 4.0.0 needs a session
|
|
15
15
|
store that takes the secret alone (auth-stores 3 or later), means stated in a key store, and
|
|
16
|
-
the contracts of `interfaces-auth` 3.
|
|
16
|
+
the contracts of `interfaces-auth` 3. **On 4.0.0?** 4.1.0 is additive — a client that
|
|
17
|
+
authenticates with an x509 certificate, through the `clientAuthentication` strategy (see
|
|
18
|
+
[*How the Client Authenticates*](#how-the-client-authenticates-clientauthentication)); what a
|
|
19
|
+
4.0.0 consumer may notice is in [*Migrating from 4.0.0*](#migrating-from-400).
|
|
17
20
|
|
|
18
21
|
The `mcp-auth` and `mcp-sso` commands that write destination files are in
|
|
19
|
-
[`@mcp-abap-adt/auth-broker-cli`](../auth-broker-cli/README.md) (2.
|
|
22
|
+
[`@mcp-abap-adt/auth-broker-cli`](../auth-broker-cli/README.md) (2.1.0, on this
|
|
20
23
|
version), in the same repository. Up to 3.0.4 they shipped in this package; from 3.1.0 this package
|
|
21
24
|
has no `bin`. Install the commands with
|
|
22
25
|
`npm i -g @mcp-abap-adt/auth-broker-cli` — after
|
|
@@ -34,6 +37,7 @@ globally for them.
|
|
|
34
37
|
- ⚡ **Forced refresh**: `refreshToken()` obtains a new token even when the cached one looks valid — for a caller holding a 401
|
|
35
38
|
- 🧾 **JWT or SAML cookies**: what the provider returns is saved as a token or as session cookies
|
|
36
39
|
- 🔑 **No secrets copied**: The client secret stays in the service key; the session store gets tokens only
|
|
40
|
+
- 📜 **x509 service keys**: a client that authenticates with a certificate instead of a secret (`tls_client_auth`), when you say so — `clientAuthentication: fromServiceKeyCertificate()`; the certificate and key are read from the key store, and never reach the session store, a log line or an error
|
|
37
41
|
|
|
38
42
|
## Installation
|
|
39
43
|
|
|
@@ -153,7 +157,7 @@ const provider = await broker.getProvider('DEV'); // an IAuthProvider
|
|
|
153
157
|
// const connection = createAbapConnection({ url, provider }, logger) …
|
|
154
158
|
```
|
|
155
159
|
|
|
156
|
-
| `authType` / `grantType` | Provider (auth-providers 5.
|
|
160
|
+
| `authType` / `grantType` | Provider (auth-providers 5.3) | Read from the key store | Read from the session store |
|
|
157
161
|
|---|---|---|---|
|
|
158
162
|
| `basic` (no grant read) | `new BasicAuthProvider(username, password)` | `username`, `password` | nothing |
|
|
159
163
|
| `snc` (no grant read) | `SncLogonProvider.forSecureLoginClient({ partnerName, qop, sncLib, myName, logger })` | `sncPartnerName` (required); `sncQop`, `sncLib`, `sncMyName` when set | nothing |
|
|
@@ -199,6 +203,9 @@ stored refresh token renews it. Without a session the provider is built
|
|
|
199
203
|
unseeded and obtains its first token at `prepare()`. `uaaClientSecret: ''` is
|
|
200
204
|
a public client: `passcode` takes it as no secret; `AuthorizationCodeProvider`
|
|
201
205
|
and `ClientCredentialsProvider` require a secret, so for them `''` is missing.
|
|
206
|
+
With a `clientAuthentication` strategy no secret is read or required: the
|
|
207
|
+
client authenticates with the strategy's answer — a certificate, say (see *How
|
|
208
|
+
the Client Authenticates*).
|
|
202
209
|
|
|
203
210
|
**The `authorization` option** is the interactive half of
|
|
204
211
|
`authorization_code` and `passcode`: a function of the destination and the
|
|
@@ -392,6 +399,284 @@ concurrent first calls share one build, and a build that threw is tried again
|
|
|
392
399
|
on the next call. A destination rewritten from outside is picked up by a new
|
|
393
400
|
broker.
|
|
394
401
|
|
|
402
|
+
### How the Client Authenticates: `clientAuthentication`
|
|
403
|
+
|
|
404
|
+
Every grant whose client authenticates to the authorization server — the UAA
|
|
405
|
+
grants (`authorization_code`, `client_credentials`, `passcode`), the OIDC
|
|
406
|
+
grants (`oidc_authorization_code`, `device_code`, `password`,
|
|
407
|
+
`token_exchange`) and `saml2_bearer` — sends the client's secret, as in 4.0.0,
|
|
408
|
+
unless you tell the broker otherwise. An XSUAA service key created with
|
|
409
|
+
`{"credential-type": "x509"}` holds no secret: it holds a client certificate,
|
|
410
|
+
its private key and the mTLS host the certificate is presented to (`certurl`).
|
|
411
|
+
How the client authenticates is your choice, stated as a strategy — the broker
|
|
412
|
+
never infers it from a key's shape and has no default:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import {
|
|
416
|
+
AuthBroker,
|
|
417
|
+
fromServiceKeyCertificate,
|
|
418
|
+
} from '@mcp-abap-adt/auth-broker';
|
|
419
|
+
import {
|
|
420
|
+
XsuaaServiceKeyStore,
|
|
421
|
+
XsuaaSessionStore,
|
|
422
|
+
} from '@mcp-abap-adt/auth-stores';
|
|
423
|
+
|
|
424
|
+
const broker = new AuthBroker({
|
|
425
|
+
serviceKeyStore: new XsuaaServiceKeyStore('/path/to/keys', {
|
|
426
|
+
grantType: 'client_credentials',
|
|
427
|
+
}),
|
|
428
|
+
sessionStore: new XsuaaSessionStore('/path/to/sessions'),
|
|
429
|
+
// The key's certificate, presented at <certurl>/oauth/token.
|
|
430
|
+
clientAuthentication: fromServiceKeyCertificate(),
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const provider = await broker.getProvider('mcp'); // its client: the certificate
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**The strategy** is `(context) => Promise<IClientAuthentication>`
|
|
437
|
+
(`ClientAuthenticationStrategy`). The broker calls it once when it builds a
|
|
438
|
+
destination's provider, with a `ClientAuthenticationContext`:
|
|
439
|
+
|
|
440
|
+
- `destination` and `grant` (a `ClientAuthenticationGrant`, one of the eight
|
|
441
|
+
above);
|
|
442
|
+
- `client` — the secret client the key store's `getAuthorizationConfig`
|
|
443
|
+
answered, as `uaaUrl`, `uaaClientId` and `uaaClientSecret` only — never a
|
|
444
|
+
refresh token or any other field the store answered with it — or `null`
|
|
445
|
+
(an x509 key answers `null` there, never a client with an empty secret);
|
|
446
|
+
- `readCertificate()` — the key store's certificate client
|
|
447
|
+
(`IClientCertificate`: `uaaUrl`, `clientId`, `certificate`, `key`,
|
|
448
|
+
`certUrl`), read **only when called**, at most once per build; `null` when
|
|
449
|
+
the store holds none or implements no `getClientCertificate`.
|
|
450
|
+
|
|
451
|
+
Its answer goes to the provider as `clientAuthentication`, and **no client
|
|
452
|
+
secret goes with it** (auth-providers 5.3 refuses both). A given strategy
|
|
453
|
+
always answers or throws: there is no "nothing" answer, so an explicit choice
|
|
454
|
+
never falls back to the secret. `saml2_pure`, the `none` rows, `basic` and
|
|
455
|
+
`snc` authenticate no client and never call it.
|
|
456
|
+
|
|
457
|
+
**The two shipped factories** each fail closed:
|
|
458
|
+
|
|
459
|
+
| Factory | Answers | Throws (→ the refusal below) |
|
|
460
|
+
|---|---|---|
|
|
461
|
+
| `fromServiceKeyCertificate()` | auth-providers' `tlsClientCertificate` with the certificate and key `readCertificate()` answers, against `<certUrl>/oauth/token` (a trailing `/` of `certUrl` dropped). The material is checked before the factory answers, so a malformed, incomplete or expired certificate is refused when the provider is built, not at its first token request | the store answers no certificate client: "the destination has no client certificate" |
|
|
462
|
+
| `fromServiceKeySecret({ encoding })` | auth-providers' `clientSecretBasic` with the secret client's `uaaClientSecret`, in an `Authorization: Basic` header. `encoding` is required: `'raw'` for XSUAA (measured — it does not form-decode), `'form'` for UAA and Keycloak (RFC 6749 §2.3.1); anything else is a `TypeError` when the factory is made | no secret client, or an empty secret: "the destination has no client secret" |
|
|
463
|
+
|
|
464
|
+
**Composing them is yours.** A fallback, and its order, is your statement,
|
|
465
|
+
never the broker's. Branch on what the key holds (`readCertificate()` is
|
|
466
|
+
memoised, so the factory reads the same answer); do not `catch` a factory's
|
|
467
|
+
refusal and fall back — that would also swallow an expired, incomplete or
|
|
468
|
+
unreadable certificate and send the secret instead:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import {
|
|
472
|
+
type ClientAuthenticationStrategy,
|
|
473
|
+
fromServiceKeyCertificate,
|
|
474
|
+
fromServiceKeySecret,
|
|
475
|
+
} from '@mcp-abap-adt/auth-broker';
|
|
476
|
+
|
|
477
|
+
// The certificate when the key holds one, else the secret in a Basic header.
|
|
478
|
+
// Decided by what the key holds, never by a failure: an expired, incomplete or
|
|
479
|
+
// unreadable certificate is refused, not replaced by the secret.
|
|
480
|
+
const certificateElseSecret: ClientAuthenticationStrategy = async (context) =>
|
|
481
|
+
(await context.readCertificate())
|
|
482
|
+
? fromServiceKeyCertificate()(context)
|
|
483
|
+
: fromServiceKeySecret({ encoding: 'raw' })(context);
|
|
484
|
+
|
|
485
|
+
// The certificate for client_credentials only; every other grant its secret.
|
|
486
|
+
const byGrant: ClientAuthenticationStrategy = (context) =>
|
|
487
|
+
context.grant === 'client_credentials'
|
|
488
|
+
? fromServiceKeyCertificate()(context)
|
|
489
|
+
: fromServiceKeySecret({ encoding: 'raw' })(context);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Where the certificate comes from** — `IServiceKeyStore.getClientCertificate?`
|
|
493
|
+
(`@mcp-abap-adt/interfaces-auth-broker` 1.2.0, optional), which auth-stores
|
|
494
|
+
3.3.0 implements:
|
|
495
|
+
|
|
496
|
+
- **`XsuaaServiceKeyStore`** — a key (bare, or wrapped in `credentials`)
|
|
497
|
+
carrying `url`, `clientid`, `certificate`, `key` and `certurl` and no
|
|
498
|
+
`clientsecret` is an x509 key: `getAuthorizationConfig` answers `null`,
|
|
499
|
+
`getClientCertificate` the certificate client. A key carrying both a secret
|
|
500
|
+
and a complete certificate offers both — the secret client as before, the
|
|
501
|
+
certificate client beside it — and your strategy picks; the store does not
|
|
502
|
+
read `credential-type` to choose. A key carrying only part of a
|
|
503
|
+
certificate client is refused by `getClientCertificate` (`incomplete`),
|
|
504
|
+
naming the missing fields, never a value. `AbapServiceKeyStore` holds no
|
|
505
|
+
certificate client.
|
|
506
|
+
- **`EnvDestinationStore`** — three means variables, each a path or a URL,
|
|
507
|
+
never PEM: `SAP_UAA_CLIENT_CERT_PATH`, `SAP_UAA_CLIENT_KEY_PATH`,
|
|
508
|
+
`SAP_UAA_CERT_URL` (`XSUAA_UAA_CLIENT_CERT_PATH`, … with
|
|
509
|
+
`XSUAA_DESTINATION_VARS`), beside `SAP_UAA_URL` / `SAP_UAA_CLIENT_ID` and
|
|
510
|
+
**without** `SAP_UAA_CLIENT_SECRET`. All three and no secret is a
|
|
511
|
+
certificate client (the two files read when `getClientCertificate` is
|
|
512
|
+
called); none of them is 4.0.0's answer; some but not all, or any beside the
|
|
513
|
+
secret, is the store's `ClientCertificateError` from both methods, naming
|
|
514
|
+
the variables. A file that states neither a client id, a secret nor any of
|
|
515
|
+
the three asks its `fallback` — so an `EnvDestinationStore` over an `XsuaaServiceKeyStore`
|
|
516
|
+
answers the x509 key's certificate client. See auth-stores' README for the
|
|
517
|
+
details.
|
|
518
|
+
|
|
519
|
+
```env
|
|
520
|
+
# A certificate destination — EnvDestinationStore (ABAP_DESTINATION_VARS)
|
|
521
|
+
SAP_AUTH_TYPE=jwt
|
|
522
|
+
SAP_GRANT_TYPE=client_credentials
|
|
523
|
+
SAP_URL=https://your-system.abap.us10.hana.ondemand.com
|
|
524
|
+
SAP_UAA_URL=https://your-account.authentication.us10.hana.ondemand.com
|
|
525
|
+
SAP_UAA_CLIENT_ID=sb-your-app!t12345
|
|
526
|
+
SAP_UAA_CLIENT_CERT_PATH=/home/you/keys/client.crt
|
|
527
|
+
SAP_UAA_CLIENT_KEY_PATH=/home/you/keys/client.key
|
|
528
|
+
SAP_UAA_CERT_URL=https://your-account.authentication.cert.us10.hana.ondemand.com
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Without a strategy nothing changes:** the client secret, exactly as 4.0.0,
|
|
532
|
+
and nothing certificate-related is read — no `getClientCertificate`, no file.
|
|
533
|
+
A client row whose key store answers no client, or one without a client id,
|
|
534
|
+
now says, after 4.0.0's words, `a certificate client needs a clientAuthentication strategy` (see
|
|
535
|
+
*Migrating from 4.0.0*).
|
|
536
|
+
|
|
537
|
+
**With a strategy, what the row requires** is its client's identity —
|
|
538
|
+
`uaaUrl` and `uaaClientId` (the OIDC rows: `uaaClientId`) — taken from the
|
|
539
|
+
secret client when the key store has one, else from the certificate client
|
|
540
|
+
(`uaaUrl`, `clientId`), read through the same memoised `readCertificate`; the
|
|
541
|
+
secret is no longer required.
|
|
542
|
+
|
|
543
|
+
**The binding on the strategy path.** A secret is bound to the client's
|
|
544
|
+
identity (`issuedBy` = `uaaUrl` + `client_id`), never to how the client
|
|
545
|
+
authenticates and never to PEM:
|
|
546
|
+
|
|
547
|
+
- a certificate destination's tokens are stored with `issuedBy` from the
|
|
548
|
+
certificate client's `uaaUrl` and `clientId`, reused after the broker is
|
|
549
|
+
recreated, and dropped after the issuer or the client id changes;
|
|
550
|
+
- a secret key and an x509 key of the **same client id** at the same
|
|
551
|
+
`uaaUrl` (as an XSUAA instance's keys are) share one session: switching the
|
|
552
|
+
destination from one to the other keeps its token and refresh token;
|
|
553
|
+
- the resource (`issuedFor`) matches when both sides state it and it is
|
|
554
|
+
equal — **or when neither does**: means with no `serviceUrl`, or one that
|
|
555
|
+
does not parse (the CLI's XSUAA placeholder is such a URL), and a session
|
|
556
|
+
stored without `issuedFor`. A resource stated on one side only never
|
|
557
|
+
matches. So an XSUAA destination without a service URL reuses its own
|
|
558
|
+
session instead of logging in on every run; the client identity still
|
|
559
|
+
decides. (Without a strategy, 4.0.0's rule stands: an absent resource
|
|
560
|
+
matches nothing.)
|
|
561
|
+
|
|
562
|
+
**The token API with a factory of yours** gets the same strategy. For a
|
|
563
|
+
destination whose grant authenticates a client, the broker resolves the
|
|
564
|
+
strategy first and calls your factory with a **fourth argument**,
|
|
565
|
+
`TokenProviderClient` — never a certificate, a key or a secret:
|
|
566
|
+
|
|
567
|
+
| Field | What it is |
|
|
568
|
+
|---|---|
|
|
569
|
+
| `clientAuthentication` | the strategy's answer, to hand to your provider |
|
|
570
|
+
| `uaaUrl`, `clientId` | the client's identity: the secret client's when the stores hold one, else the certificate client's; absent when there is neither |
|
|
571
|
+
| `refreshToken` | the refresh token the session stored — only when the session is bound to this resource and this client identity, and only from that session read (a refresh token a store's `getAuthorizationConfig` carries is dropped on this path; `authConfig` carries the same bound one) |
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
import {
|
|
575
|
+
AuthBroker,
|
|
576
|
+
fromServiceKeyCertificate,
|
|
577
|
+
type TokenProviderFactory,
|
|
578
|
+
} from '@mcp-abap-adt/auth-broker';
|
|
579
|
+
import { ClientCredentialsProvider } from '@mcp-abap-adt/auth-providers';
|
|
580
|
+
import {
|
|
581
|
+
XsuaaServiceKeyStore,
|
|
582
|
+
XsuaaSessionStore,
|
|
583
|
+
} from '@mcp-abap-adt/auth-stores';
|
|
584
|
+
|
|
585
|
+
const certificateClient: TokenProviderFactory = (
|
|
586
|
+
destination,
|
|
587
|
+
_authConfig, // null for an x509 key
|
|
588
|
+
_connConfig,
|
|
589
|
+
client,
|
|
590
|
+
) => {
|
|
591
|
+
if (!client?.clientAuthentication || !client.uaaUrl || !client.clientId) {
|
|
592
|
+
throw new Error(`No client for ${destination}`);
|
|
593
|
+
}
|
|
594
|
+
return new ClientCredentialsProvider({
|
|
595
|
+
uaaUrl: client.uaaUrl,
|
|
596
|
+
clientId: client.clientId,
|
|
597
|
+
clientAuthentication: client.clientAuthentication,
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const tokenBroker = new AuthBroker({
|
|
602
|
+
serviceKeyStore: new XsuaaServiceKeyStore('/path/to/keys', {
|
|
603
|
+
grantType: 'client_credentials',
|
|
604
|
+
}),
|
|
605
|
+
sessionStore: new XsuaaSessionStore('/path/to/sessions'),
|
|
606
|
+
clientAuthentication: fromServiceKeyCertificate(),
|
|
607
|
+
provider: certificateClient,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const token = await tokenBroker.getToken('mcp');
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
On this path:
|
|
614
|
+
|
|
615
|
+
- the destination must state its `authType` (`jwt` or `saml`) and
|
|
616
|
+
`grantType` — the strategy is told the grant — else a
|
|
617
|
+
`DestinationConfigError` naming `authType`, `grantType`; a `saml2_pure` or
|
|
618
|
+
`none` destination authenticates no client, and its factory gets three
|
|
619
|
+
arguments, as in 4.0.0;
|
|
620
|
+
- every stored secret your factory is seeded with passes only when the
|
|
621
|
+
session is bound to this resource and this client identity: the refresh
|
|
622
|
+
token (in the fourth argument, and in `authConfig` for a secret client), and
|
|
623
|
+
the stored token, cookies and expiry in `connConfig` — so a refresh token
|
|
624
|
+
can never reach another authorization server. A result without a refresh
|
|
625
|
+
token carries forward only a bound one. (Without a strategy, 4.0.0's
|
|
626
|
+
carry-over stands: the stored refresh token whatever its binding.)
|
|
627
|
+
- what the factory is handed is built from allowlists, never a store's
|
|
628
|
+
answer passed whole: `authConfig` is `uaaUrl`, `uaaClientId`,
|
|
629
|
+
`uaaClientSecret` and the bound refresh token; `connConfig` is
|
|
630
|
+
`serviceUrl`, `sapClient`, `language`, `authType`, `grantType`, plus — when
|
|
631
|
+
that same connection read is bound — `authorizationToken`,
|
|
632
|
+
`sessionCookies`, `expiresAt`, `issuedFor`, `issuedBy`. Anything else a
|
|
633
|
+
store answers (a password, grant data, a field outside the type) stays
|
|
634
|
+
behind; without a strategy the reads go through as in 4.0.0;
|
|
635
|
+
- a factory that throws is a `DestinationConfigError` naming `provider`,
|
|
636
|
+
`clientAuthentication`, in fixed words — what it threw is not kept;
|
|
637
|
+
- a provider **instance** given as `provider` is your own composition: the
|
|
638
|
+
token API uses it as given and calls no strategy for it.
|
|
639
|
+
|
|
640
|
+
**A strategy that fails carries nothing out.** Whatever the strategy, a store
|
|
641
|
+
it reads or the certificate check throws — and an answer that is no
|
|
642
|
+
`IClientAuthentication` — becomes a `DestinationConfigError` with
|
|
643
|
+
`missingFields: ['clientAuthentication']`, before any provider exists, in
|
|
644
|
+
fixed words chosen by the error's class only — no `cause`, nothing of the
|
|
645
|
+
thrown message:
|
|
646
|
+
|
|
647
|
+
| Words (after `Destination "<name>": `) | When |
|
|
648
|
+
|---|---|
|
|
649
|
+
| `the clientAuthentication strategy refused: the destination has no client certificate` | `fromServiceKeyCertificate()`, no certificate client |
|
|
650
|
+
| `the clientAuthentication strategy refused: the destination has no client secret` | `fromServiceKeySecret()`, no secret client or an empty secret |
|
|
651
|
+
| `the clientAuthentication strategy refused: the client certificate is incomplete` / `has expired` / `could not be used` | the certificate check refused the material |
|
|
652
|
+
| `the clientAuthentication strategy answered no client authentication` | the answer has no `authenticate` function |
|
|
653
|
+
| `the client certificate could not be read` | the store failed reading the certificate client for the row's identity |
|
|
654
|
+
| `the clientAuthentication strategy failed` | anything else — your strategy's own error, a store's error (an incomplete `.env`), an unreadable file |
|
|
655
|
+
|
|
656
|
+
A certificate, a key or a file's content never reaches a log line, a refusal,
|
|
657
|
+
a `DestinationConfigError`, a thrown message or the session store.
|
|
658
|
+
|
|
659
|
+
**The certificate is pinned for the broker's lifetime.** It is read when the
|
|
660
|
+
destination's provider is built, and that provider is cached per destination
|
|
661
|
+
for as long as the `AuthBroker` lives. An XSUAA x509 key's certificate lives
|
|
662
|
+
about seven days: a long-running process then gets `the client certificate
|
|
663
|
+
has expired` from its provider until it builds a new `AuthBroker`, and
|
|
664
|
+
replacing the key or the PEM files changes nothing before that. Rotating
|
|
665
|
+
certificates is out of scope: create a new key, then a new broker (restart).
|
|
666
|
+
|
|
667
|
+
**What is measured.** `client_credentials` with an x509 XSUAA service key,
|
|
668
|
+
against XSUAA on a BTP trial (2026-10-05; the instance's `credential-types`
|
|
669
|
+
`["binding-secret", "x509"]`, its key created with
|
|
670
|
+
`{"credential-type": "x509"}`): through the broker
|
|
671
|
+
(`fromServiceKeyCertificate()` + `XsuaaServiceKeyStore`, `getProvider` and the
|
|
672
|
+
token API), through `mcp-auth --client-auth certificate` and through
|
|
673
|
+
`generate-env-from-service-key --client-auth certificate`, each followed by a
|
|
674
|
+
fresh broker over the written destination — see *Testing*. **Not measured:**
|
|
675
|
+
`authorization_code` and `passcode` over x509 (the broker builds them, nothing
|
|
676
|
+
has run them against XSUAA), the OIDC grants and `saml2_bearer` with a
|
|
677
|
+
strategy, and ABAP environment service keys with x509 (`AbapServiceKeyStore`
|
|
678
|
+
holds no certificate client).
|
|
679
|
+
|
|
395
680
|
### Persistence and `flush()`
|
|
396
681
|
|
|
397
682
|
Every token provider `getProvider` builds writes what it obtains back to the
|
|
@@ -614,6 +899,44 @@ const newToken = await tokenRefresher.refreshToken();
|
|
|
614
899
|
A `@mcp-abap-adt/connection` 10 connector takes an `IAuthProvider` instead:
|
|
615
900
|
give it `await broker.getProvider('TRIAL')`.
|
|
616
901
|
|
|
902
|
+
## Migrating from 4.0.0
|
|
903
|
+
|
|
904
|
+
4.1.0 is a minor: nothing to change to keep 4.0.0's behaviour. Without a
|
|
905
|
+
`clientAuthentication` strategy every destination is served as before and
|
|
906
|
+
nothing certificate-related is read. What a 4.0.0 consumer may still notice:
|
|
907
|
+
|
|
908
|
+
1. **A refusal's message gains a hint.** A UAA, OIDC or `saml2_bearer`
|
|
909
|
+
destination whose key store answers no client (or one without a client
|
|
910
|
+
id) is still a `DestinationConfigError` with the same `missingFields`; its
|
|
911
|
+
message now ends with `; a certificate client needs a clientAuthentication
|
|
912
|
+
strategy`. Match on `missingFields` or the class, never the message.
|
|
913
|
+
2. **Dependencies.** `@mcp-abap-adt/interfaces-auth-broker` `^1.2.0` (the
|
|
914
|
+
optional `IServiceKeyStore.getClientCertificate` and `IClientCertificate`),
|
|
915
|
+
`@mcp-abap-adt/interfaces-auth` `^3.2.0` and `@mcp-abap-adt/auth-providers`
|
|
916
|
+
`^5.3.0` (`IClientAuthentication`). A key store that reads x509 keys is
|
|
917
|
+
auth-stores `^3.3.0`; an older store simply has no certificate client.
|
|
918
|
+
3. **New exports:** `fromServiceKeyCertificate`, `fromServiceKeySecret`, and
|
|
919
|
+
the types `ClientAuthenticationStrategy`, `ClientAuthenticationContext`,
|
|
920
|
+
`ClientAuthenticationGrant`, `FromServiceKeySecretOptions`,
|
|
921
|
+
`TokenProviderClient`, `IClientCertificate`, `IClientAuthentication`.
|
|
922
|
+
|
|
923
|
+
**Adopting a strategy** (`clientAuthentication`, see *How the Client
|
|
924
|
+
Authenticates*) changes, for the destinations it applies to:
|
|
925
|
+
|
|
926
|
+
- **No secret goes to the provider** — the strategy's answer does; a
|
|
927
|
+
provider's constructor refuses both.
|
|
928
|
+
- **A secret is bound to the client's identity**: a secret key and an x509
|
|
929
|
+
key of the same client id at the same `uaaUrl` share one session, so
|
|
930
|
+
switching between them keeps the token and the refresh token; a destination
|
|
931
|
+
stating no parseable `serviceUrl` reuses a session stored without
|
|
932
|
+
`issuedFor`, which 4.0.0 never did.
|
|
933
|
+
- **The token API with a factory of yours** passes the fourth argument; seeds
|
|
934
|
+
your factory with a stored refresh token, token or cookies only when the
|
|
935
|
+
session is bound here (4.0.0 carried the stored refresh token over whatever
|
|
936
|
+
its binding); refuses a destination that states no `authType` / `grantType`;
|
|
937
|
+
and turns a throwing factory into a `DestinationConfigError` naming
|
|
938
|
+
`provider`, `clientAuthentication`.
|
|
939
|
+
|
|
617
940
|
## Migrating from 3.x
|
|
618
941
|
|
|
619
942
|
4.0.0 is a major: what a 3.x consumer must do, in short (the full list is the
|
|
@@ -731,6 +1054,9 @@ SAP_ISSUED_FOR=https://your-system.abap.us10.hana.ondemand.com:443?sap-client=10
|
|
|
731
1054
|
SAP_ISSUED_BY=https://your-account.authentication.us10.hana.ondemand.com:443?client_id=client_id
|
|
732
1055
|
```
|
|
733
1056
|
|
|
1057
|
+
A client that authenticates with a certificate states `SAP_UAA_CLIENT_CERT_PATH`,
|
|
1058
|
+
`SAP_UAA_CLIENT_KEY_PATH` and `SAP_UAA_CERT_URL` — paths and a URL, never PEM —
|
|
1059
|
+
in place of `SAP_UAA_CLIENT_SECRET` (see *How the Client Authenticates*).
|
|
734
1060
|
SAML cookies are `SAP_SESSION_COOKIES_B64` in place of `SAP_JWT_TOKEN`. The
|
|
735
1061
|
XSUAA stores use the same names with `XSUAA_` (`XSUAA_DESTINATION_VARS`,
|
|
736
1062
|
`XSUAA_SESSION_VARS`; the URL is `XSUAA_MCP_URL`). Every means key
|
|
@@ -759,6 +1085,21 @@ An XSUAA key carries no URL of the resource it authorizes for: state it as
|
|
|
759
1085
|
means in an `EnvDestinationStore` (`XSUAA_MCP_URL`, with
|
|
760
1086
|
`XSUAA_DESTINATION_VARS`) whose `fallback` is the `XsuaaServiceKeyStore`.
|
|
761
1087
|
|
|
1088
|
+
An x509 XSUAA key (created with `{"credential-type": "x509"}`) holds a
|
|
1089
|
+
certificate client instead of a secret — the broker uses it only beside a
|
|
1090
|
+
`clientAuthentication` strategy (see *How the Client Authenticates*):
|
|
1091
|
+
|
|
1092
|
+
```json
|
|
1093
|
+
{
|
|
1094
|
+
"url": "https://your-account.authentication.us10.hana.ondemand.com",
|
|
1095
|
+
"clientid": "sb-your-app!t12345",
|
|
1096
|
+
"certificate": "-----BEGIN CERTIFICATE-----\n...",
|
|
1097
|
+
"key": "-----BEGIN RSA PRIVATE KEY-----\n...",
|
|
1098
|
+
"certurl": "https://your-account.authentication.cert.us10.hana.ondemand.com",
|
|
1099
|
+
"credential-type": "x509"
|
|
1100
|
+
}
|
|
1101
|
+
```
|
|
1102
|
+
|
|
762
1103
|
## Responsibilities and Design Principles
|
|
763
1104
|
|
|
764
1105
|
### Core Development Principle
|
|
@@ -857,6 +1198,7 @@ new AuthBroker(
|
|
|
857
1198
|
destination: string,
|
|
858
1199
|
authConfig: IAuthorizationConfig | null,
|
|
859
1200
|
connConfig: IConnectionConfig,
|
|
1201
|
+
client?: TokenProviderClient, // only beside clientAuthentication (4.1.0)
|
|
860
1202
|
) => IRefreshableTokenProvider);
|
|
861
1203
|
// Collaborators, each a function of the destination, called once per
|
|
862
1204
|
// build, never disposed by the broker:
|
|
@@ -865,6 +1207,8 @@ new AuthBroker(
|
|
|
865
1207
|
deviceCodePresenter?: (destination: string) => IDeviceCodePresenter; // device_code
|
|
866
1208
|
samlCookies?: (destination: string) => (samlResponse: string) => Promise<string>; // saml2_pure
|
|
867
1209
|
assertionReplayStore?: (destination: string) => IAssertionReplayStore; // saml2_pure, saml2_bearer
|
|
1210
|
+
// How a client authenticates (4.1.0): every grant that authenticates one
|
|
1211
|
+
clientAuthentication?: ClientAuthenticationStrategy;
|
|
868
1212
|
},
|
|
869
1213
|
logger?: ILogger,
|
|
870
1214
|
)
|
|
@@ -873,12 +1217,13 @@ new AuthBroker(
|
|
|
873
1217
|
**Parameters:**
|
|
874
1218
|
- `config.sessionStore` - **Required** - The session secret: the token or cookies, `expiresAt`, the refresh token, and what it is bound to (`issuedFor`, `issuedBy`). For the token API with a `provider`, its `serviceUrl`, or the service key's, is required (3.x's reads).
|
|
875
1219
|
- `config.serviceKeyStore` - The means: `authType`, `grantType`, the client, basic's user and password, the SNC, OIDC and SAML fields, `serviceUrl`. **Required by `getProvider`**, which has no other source of means.
|
|
876
|
-
- `config.provider` - The token API's source (`getToken`, `refreshToken`, `createTokenRefresher`): 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*). Not used by `getProvider`. Without it, the token API asks the provider `getProvider` builds for the destination (see *Getting Tokens*); with neither it nor a `serviceKeyStore`, the token API throws `DestinationConfigError` naming both.
|
|
1220
|
+
- `config.provider` - The token API's source (`getToken`, `refreshToken`, `createTokenRefresher`): 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*); beside a `clientAuthentication` strategy, with a fourth argument (`TokenProviderClient`, see *How the Client Authenticates*). Not used by `getProvider`. Without it, the token API asks the provider `getProvider` builds for the destination (see *Getting Tokens*); with neither it nor a `serviceKeyStore`, the token API throws `DestinationConfigError` naming both.
|
|
877
1221
|
- `config.authorization` - The interactive strategy of `jwt` / `authorization_code`, `jwt` / `passcode`, `saml` / `saml2_pure` and `saml` / `saml2_bearer`, as a function of the destination and the grant (see *A Provider for a Connector*).
|
|
878
1222
|
- `config.oidcAuthorization` - The interactive strategy of `jwt` / `oidc_authorization_code`.
|
|
879
1223
|
- `config.deviceCodePresenter` - Where `jwt` / `device_code` shows the user the verification URL and code.
|
|
880
1224
|
- `config.samlCookies` - `saml2_pure`: turns the validated SAMLResponse into the system's session cookies.
|
|
881
1225
|
- `config.assertionReplayStore` - The replay store the SAML validators record each assertion in.
|
|
1226
|
+
- `config.clientAuthentication` - How the client of every grant that authenticates one (the UAA and OIDC grants, `saml2_bearer`) authenticates to the authorization server: a `ClientAuthenticationStrategy` — `fromServiceKeyCertificate()`, `fromServiceKeySecret({ encoding })`, or your own composition. Called once per build; absent, the client secret as in 4.0.0 and nothing certificate-related is read (see *How the Client Authenticates*).
|
|
882
1227
|
- Each collaborator option is required only by the rows that use it (missing → `DestinationConfigError` naming it); the broker supplies no default, calls it once per build, and disposes nothing it returns.
|
|
883
1228
|
- `logger` - Optional logger. If not provided, nothing is logged.
|
|
884
1229
|
|
|
@@ -926,7 +1271,8 @@ The two stores composed, each for its role:
|
|
|
926
1271
|
`null` when neither store holds anything.
|
|
927
1272
|
- `getAuthorizationConfig` — the service key store's client (`uaaUrl`,
|
|
928
1273
|
`uaaClientId`, `uaaClientSecret`) with the session's refresh token; `null`
|
|
929
|
-
when the key store has no client
|
|
1274
|
+
when the key store has no client — an x509 key's included: it never answers
|
|
1275
|
+
a certificate or a key.
|
|
930
1276
|
|
|
931
1277
|
Means a session store answers (a client, a URL, an `authType`) are not read,
|
|
932
1278
|
nor a secret a key store answers. Up to 3.1.0 both answered the session's
|
|
@@ -974,6 +1320,10 @@ configuration whole, and the key's only when the session had none.
|
|
|
974
1320
|
| `device_code` without the `deviceCodePresenter` option | `deviceCodePresenter` |
|
|
975
1321
|
| a SAML grant without its trust (`''` and an empty certificate list count as missing) | `samlIdpSsoUrl`, `samlSpEntityId`, `samlIdpEntityId`, `samlIdpCertificates` — each that is missing |
|
|
976
1322
|
| `saml2_bearer` without its client | `uaaUrl`, `uaaClientId` — each that is missing |
|
|
1323
|
+
| a client row (UAA, OIDC, `saml2_bearer`) with no client id and no `clientAuthentication` strategy | as above; the message adds `a certificate client needs a clientAuthentication strategy` |
|
|
1324
|
+
| the `clientAuthentication` strategy threw, refused, or answered no client authentication; the certificate client could not be read | `clientAuthentication` (fixed words, see *How the Client Authenticates*) |
|
|
1325
|
+
| the token API with a `clientAuthentication` strategy and a factory, on a destination that states no `jwt` / `saml` type | `authType`, `grantType` |
|
|
1326
|
+
| the token API's factory threw beside a `clientAuthentication` strategy | `provider`, `clientAuthentication` |
|
|
977
1327
|
| a SAML grant without its collaborators | `authorization`, `samlCookies` (`saml2_pure`), `assertionReplayStore` — each that is missing |
|
|
978
1328
|
| a certificate the validator cannot read | `samlIdpCertificates` |
|
|
979
1329
|
| a `samlClockSkewMs` that is not a whole, non-negative number | `samlClockSkewMs` |
|
|
@@ -1111,6 +1461,30 @@ destination.
|
|
|
1111
1461
|
Without `test-config.yaml` the template is read, its placeholders disable every
|
|
1112
1462
|
integration case, and the run reaches no system.
|
|
1113
1463
|
|
|
1464
|
+
### The x509 Live Check
|
|
1465
|
+
|
|
1466
|
+
`npm run test:live:x509` (from the repository root or this package) runs
|
|
1467
|
+
`client_credentials` with an x509 XSUAA service key against a real BTP
|
|
1468
|
+
subaccount: it builds, creates an `xsuaa` / `application` instance
|
|
1469
|
+
(`credential-types: ["binding-secret", "x509"]`) and a key made with
|
|
1470
|
+
`{"credential-type": "x509"}`, runs `src/__tests__/live/x509.live.test.ts`
|
|
1471
|
+
— the broker with `fromServiceKeyCertificate()`, `mcp-auth --client-auth
|
|
1472
|
+
certificate`, `generate-env-from-service-key --client-auth certificate`, a
|
|
1473
|
+
fresh broker over each written destination, and a failing run that leaves the
|
|
1474
|
+
previous destination untouched and prints no PEM — and removes everything,
|
|
1475
|
+
also when a test fails. Not in `npm test`, not in CI.
|
|
1476
|
+
|
|
1477
|
+
It refuses to run unless `XSUAA_CF_API`, `XSUAA_CF_ORG` and `XSUAA_CF_SPACE`
|
|
1478
|
+
equal exactly what `cf target` shows (`cf login -a <api> --sso -o <org> -s
|
|
1479
|
+
<space>` first). It touches only what it created: each resource is recorded
|
|
1480
|
+
with its GUID in a ledger, `tests/live/x509/.local/owned` (gitignored, with
|
|
1481
|
+
the key and its PEM files, owner-only), bound to that API, org and space, and
|
|
1482
|
+
re-checked before every reuse or delete; a name held by anything else is
|
|
1483
|
+
refused. A failed teardown exits non-zero and keeps `.local/` — run
|
|
1484
|
+
`tests/live/x509/teardown.sh` with the same three variables. `X509_KEEP=1`
|
|
1485
|
+
keeps the environment for another run; `-- -t "(b)"` runs one case. Last run:
|
|
1486
|
+
BTP trial, 2026-10-05, all five cases passed.
|
|
1487
|
+
|
|
1114
1488
|
## Documentation
|
|
1115
1489
|
|
|
1116
1490
|
Complete documentation is available in the repository's [`docs/`](../../docs/) directory:
|