@mcp-abap-adt/auth-broker 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +396 -0
- package/README.md +694 -226
- package/dist/AuthBroker.d.ts +178 -29
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +494 -101
- package/dist/DestinationConfigError.d.ts +27 -0
- package/dist/DestinationConfigError.d.ts.map +1 -0
- package/dist/DestinationConfigError.js +36 -0
- package/dist/SessionWriter.d.ts +54 -0
- package/dist/SessionWriter.d.ts.map +1 -0
- package/dist/SessionWriter.js +135 -0
- package/dist/binding.d.ts +95 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +207 -0
- package/dist/bindingOf.d.ts +36 -0
- package/dist/bindingOf.d.ts.map +1 -0
- package/dist/bindingOf.js +53 -0
- package/dist/destinations.d.ts +139 -0
- package/dist/destinations.d.ts.map +1 -0
- package/dist/destinations.js +429 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/providers/ITokenProvider.d.ts +2 -1
- package/dist/providers/ITokenProvider.d.ts.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js +2 -2
- package/dist/stores/index.d.ts +2 -2
- package/dist/stores/index.js +2 -2
- package/dist/stores/interfaces.d.ts +4 -3
- package/dist/stores/interfaces.d.ts.map +1 -1
- package/dist/stores/interfaces.js +2 -2
- package/dist/types.d.ts +5 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -1
- package/package.json +15 -8
package/README.md
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
# @mcp-abap-adt/auth-broker
|
|
2
2
|
[](https://stand-with-ukraine.pp.ua)
|
|
3
3
|
|
|
4
|
-
A per-destination
|
|
5
|
-
`TRIAL` —
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
A per-destination credential broker for SAP BTP and ABAP systems. For a destination — a name,
|
|
5
|
+
such as `TRIAL` — `getProvider` builds the `IAuthProvider` the destination states (basic, SNC, a
|
|
6
|
+
UAA, OIDC or SAML grant, or a credential handed over) from the *means* in the service key store
|
|
7
|
+
and the *secret* in the session store, ready for a `@mcp-abap-adt/connection` 10 connector, and
|
|
8
|
+
stores back every token or set of SAML session cookies that provider obtains or renews. The
|
|
9
|
+
token API (`getToken`, `refreshToken`, `createTokenRefresher`) serves whoever wants a token and
|
|
10
|
+
nothing else. It decides nothing about tokens itself: whether the cached token is still good,
|
|
11
|
+
when to refresh and when to log in is the provider's call (`@mcp-abap-adt/auth-providers`), and
|
|
12
|
+
where means and secrets live is the stores' (`@mcp-abap-adt/auth-stores`, or your own).
|
|
13
|
+
|
|
14
|
+
**Upgrading from 3.x?** See [*Migrating from 3.x*](#migrating-from-3x) — 4.0.0 needs a session
|
|
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.
|
|
17
|
+
|
|
18
|
+
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.0.0, on this
|
|
20
|
+
version), in the same repository. Up to 3.0.4 they shipped in this package; from 3.1.0 this package
|
|
14
21
|
has no `bin`. Install the commands with
|
|
15
22
|
`npm i -g @mcp-abap-adt/auth-broker-cli` — after
|
|
16
23
|
`npm uninstall -g @mcp-abap-adt/auth-broker` if you had installed this one
|
|
@@ -18,8 +25,12 @@ globally for them.
|
|
|
18
25
|
|
|
19
26
|
## Features
|
|
20
27
|
|
|
21
|
-
-
|
|
28
|
+
- 🔌 **A credential for a connector**: `getProvider(destination)` builds the `IAuthProvider` the destination states — basic, SNC, the UAA grants (authorization code, client credentials, passcode), the OIDC grants (authorization code with PKCE, device code, password, token exchange), the SAML grants (session cookies, bearer token), or a credential handed over — from the service key store's means and the session store's secret
|
|
29
|
+
- 💾 **What a provider obtains is stored**: every token or set of SAML session cookies a `getProvider` provider obtains or renews — at `prepare()`, on expiry, or after a 401 in `rejected()` — is written to the session store, the secret alone with what it is bound to; a failed write is retried by the broker, and `flush()` tells you whether everything landed
|
|
30
|
+
- 🔒 **A secret goes only where it was obtained**: a stored token is used only for the resource it was obtained for, from the issuer and client that issued it — otherwise it is discarded and the provider logs in afresh
|
|
31
|
+
- 🎯 **Per destination**: one provider per destination name, shared by `getProvider` and the token API — one token, one refresh token, one renewal in flight
|
|
22
32
|
- 🔄 **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
|
|
33
|
+
- 🪙 **A token API for whoever wants a token and nothing else**: `getToken`, `refreshToken` and `createTokenRefresher` on the destination's own provider, or on one you give the broker, as in 3.x
|
|
23
34
|
- ⚡ **Forced refresh**: `refreshToken()` obtains a new token even when the cached one looks valid — for a caller holding a 401
|
|
24
35
|
- 🧾 **JWT or SAML cookies**: what the provider returns is saved as a token or as session cookies
|
|
25
36
|
- 🔑 **No secrets copied**: The client secret stays in the service key; the session store gets tokens only
|
|
@@ -38,10 +49,13 @@ supported as well.
|
|
|
38
49
|
|
|
39
50
|
### Basic Usage
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
This is the token API with a provider of your own — the 3.x way, which keeps
|
|
53
|
+
working. The broker takes a session store, an optional service key store, and
|
|
54
|
+
a token provider implementing `IRefreshableTokenProvider` (from
|
|
43
55
|
`@mcp-abap-adt/interfaces-auth`) — or a factory that builds one per
|
|
44
|
-
destination
|
|
56
|
+
destination. Without a `provider` the token API serves what the destination
|
|
57
|
+
states, on the provider `getProvider` builds (see *Getting Tokens*); for a
|
|
58
|
+
`@mcp-abap-adt/connection` 10 connector, use `getProvider` itself.
|
|
45
59
|
|
|
46
60
|
```typescript
|
|
47
61
|
import { AuthBroker } from '@mcp-abap-adt/auth-broker';
|
|
@@ -85,18 +99,358 @@ The factory receives:
|
|
|
85
99
|
- `connConfig` — the session's connection config, with `serviceUrl` resolved
|
|
86
100
|
(from the session, else the service key) and the token stored last.
|
|
87
101
|
|
|
102
|
+
These are 3.x's reads, in 3.x's order, kept for this path: a session store
|
|
103
|
+
that still answers a client or a URL is read first. auth-stores 3's session
|
|
104
|
+
stores answer neither, so the key store's are what is found. What the broker
|
|
105
|
+
writes back is the session secret alone (see *Getting Tokens*).
|
|
106
|
+
|
|
88
107
|
A provider instance can be passed instead of a factory; it is then used as
|
|
89
108
|
given, for every destination, and the broker seeds it with nothing:
|
|
90
109
|
|
|
91
110
|
```typescript
|
|
92
111
|
const broker = new AuthBroker({
|
|
93
112
|
sessionStore: new AbapSessionStore('/path/to/sessions'),
|
|
94
|
-
provider: new AuthorizationCodeProvider({
|
|
113
|
+
provider: new AuthorizationCodeProvider({
|
|
114
|
+
uaaUrl, clientId, clientSecret,
|
|
115
|
+
authorization: browserCallbackStrategy({ browser: 'system' }),
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> `AuthorizationCodeProvider` and the other `@mcp-abap-adt/auth-providers` 5
|
|
121
|
+
> providers implement `IRefreshableTokenProvider` (from `@mcp-abap-adt/interfaces-auth`
|
|
122
|
+
> 3). Since auth-providers 5 the interactive ones take their `authorization`
|
|
123
|
+
> strategy explicitly — none is built for you.
|
|
124
|
+
|
|
125
|
+
### A Provider for a Connector: `getProvider`
|
|
126
|
+
|
|
127
|
+
`getProvider(destination)` returns the `IAuthProvider` (from
|
|
128
|
+
`@mcp-abap-adt/interfaces-auth` 3) that a `@mcp-abap-adt/connection` 10
|
|
129
|
+
connector takes as it is. The destination states which provider it gets; the
|
|
130
|
+
broker reads two stores, each for one role:
|
|
131
|
+
|
|
132
|
+
- **the means** — `authType`, `grantType`, basic's user and password, the SNC,
|
|
133
|
+
OIDC and SAML fields, `serviceUrl`, the client — from the **service key store**
|
|
134
|
+
(`getConnectionConfig`), and only from there;
|
|
135
|
+
- **the secret** — the token or session cookies, `expiresAt`, the refresh
|
|
136
|
+
token — from the **session store** (`loadSession`), and only from there.
|
|
137
|
+
|
|
138
|
+
Means a session store happens to answer are not read, and neither is a token
|
|
139
|
+
or cookies a key store happens to answer. Nothing is inferred: the
|
|
140
|
+
destination's `authType` (and `grantType`, for `jwt` and `saml`) decides the
|
|
141
|
+
provider, never which other fields are present.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { AuthBroker } from '@mcp-abap-adt/auth-broker';
|
|
145
|
+
|
|
146
|
+
const broker = new AuthBroker({
|
|
147
|
+
serviceKeyStore: myKeyStore, // the means — required by getProvider
|
|
148
|
+
sessionStore: mySessionStore, // the secret
|
|
149
|
+
// no `provider`: that option is the token API's source, not getProvider's
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const provider = await broker.getProvider('DEV'); // an IAuthProvider
|
|
153
|
+
// const connection = createAbapConnection({ url, provider }, logger) …
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| `authType` / `grantType` | Provider (auth-providers 5.2) | Read from the key store | Read from the session store |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `basic` (no grant read) | `new BasicAuthProvider(username, password)` | `username`, `password` | nothing |
|
|
159
|
+
| `snc` (no grant read) | `SncLogonProvider.forSecureLoginClient({ partnerName, qop, sncLib, myName, logger })` | `sncPartnerName` (required); `sncQop`, `sncLib`, `sncMyName` when set | nothing |
|
|
160
|
+
| `jwt` / `authorization_code` | `AuthorizationCodeProvider` | the client: `uaaUrl`, `uaaClientId`, `uaaClientSecret`; `serviceUrl`, `sapClient` for the binding | the seed: `authorizationToken`, `refreshToken`, `expiresAt` — used only when `issuedFor` and `issuedBy` match |
|
|
161
|
+
| `jwt` / `client_credentials` | `ClientCredentialsProvider` | the client: `uaaUrl`, `uaaClientId`, `uaaClientSecret`; `serviceUrl`, `sapClient` for the binding | nothing (the row takes the client alone) |
|
|
162
|
+
| `jwt` / `passcode` | `UaaPasscodeProvider` | the client: `uaaUrl`, `uaaClientId`, `uaaClientSecret` (`''` = a public client); `serviceUrl`, `sapClient` for the binding | the seed: `authorizationToken`, `refreshToken`, `expiresAt` — used only when `issuedFor` and `issuedBy` match |
|
|
163
|
+
| `jwt` / `oidc_authorization_code` | `OidcBrowserProvider` (PKCE) | `uaaClientId`, `uaaClientSecret` (`''` = a public client); `oidcIssuerUrl`, or `oidcAuthorizationEndpoint` + `oidcTokenEndpoint`; `oidcScopes`; `serviceUrl`, `sapClient` for the binding | the seed — used only when `issuedFor` and `issuedBy` match |
|
|
164
|
+
| `jwt` / `device_code` | `OidcDeviceFlowProvider` | the client as above; `oidcIssuerUrl`, or `oidcDeviceAuthorizationEndpoint` + `oidcTokenEndpoint`; `oidcScopes`; `serviceUrl`, `sapClient` | the seed, as above |
|
|
165
|
+
| `jwt` / `password` | `OidcPasswordProvider` | the client as above; `username`, `password`; `oidcIssuerUrl`, or `oidcTokenEndpoint`; `oidcScopes`; `serviceUrl`, `sapClient` | the seed, as above |
|
|
166
|
+
| `jwt` / `token_exchange` | `OidcTokenExchangeProvider` (RFC 8693) | the client as above; `oidcSubjectToken`, `oidcSubjectTokenType`; `oidcAudience`, `oidcActorToken`, `oidcActorTokenType` when set; `oidcScopes`, joined by one space into its `scope`; `oidcIssuerUrl`, or `oidcTokenEndpoint`; `serviceUrl`, `sapClient` | the seed, as above (it has no refresh grant: a renewal exchanges again) |
|
|
167
|
+
| `saml` / `saml2_pure` | `Saml2PureProvider` | `samlIdpSsoUrl`, `samlSpEntityId`, `samlIdpEntityId` (the expected issuer), `samlIdpCertificates`; `samlAcsUrl`, `samlRelayState`, `samlIdpInitiated`, `samlClockSkewMs` when set; `serviceUrl`, `sapClient` for the binding | the seed: `sessionCookies`, `expiresAt` — used only when `issuedFor` and `issuedBy` match |
|
|
168
|
+
| `saml` / `saml2_bearer` | `Saml2BearerProvider` (RFC 7522) | the same SAML fields; `samlTokenUrl` when set; the client: `uaaUrl`, `uaaClientId`, `uaaClientSecret` (`''` = a public client); `serviceUrl`, `sapClient` | the seed: `authorizationToken`, `refreshToken`, `expiresAt` — used only when `issuedFor` and `issuedBy` match |
|
|
169
|
+
| `jwt` / `none` | `TokenAuthProvider.fixed(authorizationToken)` | `authType`, `grantType`, `serviceUrl` (+ `sapClient`); an issuer, when stated: `oidcIssuerUrl`, or the client's `uaaUrl` + `uaaClientId` | `authorizationToken` (required), `issuedFor` (required to match); `issuedBy` when the means state an issuer |
|
|
170
|
+
| `saml` / `none` | `new SamlAuthProvider(sessionCookies)` | `authType`, `grantType`, `serviceUrl` (+ `sapClient`); `samlAcsUrl` when stated | `sessionCookies` (required), `issuedFor` (required to match); `issuedBy` when `samlAcsUrl` is stated |
|
|
171
|
+
|
|
172
|
+
**No provider reads `serviceUrl`.** The URL of the system is where the
|
|
173
|
+
connector connects, not authorization data: no token provider reads it — a
|
|
174
|
+
UAA grant needs the client and `uaaUrl` — so `getProvider` neither requires it
|
|
175
|
+
nor passes it to a provider. Give the connector its URL from your key store
|
|
176
|
+
(`getConnectionConfig`), as the key store answers it. The broker reads it,
|
|
177
|
+
with `sapClient`, for one thing only: to bind a stored secret to the resource
|
|
178
|
+
it was obtained for (see *A Secret Is Bound to Its Resource and Issuer*). A
|
|
179
|
+
token destination without it still gets its provider, but no stored secret
|
|
180
|
+
is reused for it.
|
|
181
|
+
|
|
182
|
+
`none` is how a handed-over credential is stated: the key store says
|
|
183
|
+
`grantType: 'none'`, and the token or cookies live in the session. The SNC
|
|
184
|
+
row takes the contract's own defaults when a field is absent — the library is
|
|
185
|
+
discovered (`SNC_LIB_64`, `SNC_LIB`, the Secure Login Client's install path)
|
|
186
|
+
when `sncLib` is, the user's SNC name comes from the credential when
|
|
187
|
+
`sncMyName` is, and `qop` is the provider's `'9'` when `sncQop` is.
|
|
188
|
+
|
|
189
|
+
The allowed pairs are `jwt` with `authorization_code`, `client_credentials`,
|
|
190
|
+
`passcode`, `oidc_authorization_code`, `device_code`, `password`,
|
|
191
|
+
`token_exchange` or `none`, and `saml` with `saml2_pure`, `saml2_bearer` or
|
|
192
|
+
`none`. A pair outside them is a `DestinationConfigError` naming `grantType`.
|
|
193
|
+
|
|
194
|
+
**The UAA grants.** The client comes from the key store's
|
|
195
|
+
`getAuthorizationConfig` — never from the session store. A session is the
|
|
196
|
+
seed: the stored token is presented while it is valid (a JWT's own `exp`
|
|
197
|
+
decides; the stored `expiresAt` serves a token that carries none), and the
|
|
198
|
+
stored refresh token renews it. Without a session the provider is built
|
|
199
|
+
unseeded and obtains its first token at `prepare()`. `uaaClientSecret: ''` is
|
|
200
|
+
a public client: `passcode` takes it as no secret; `AuthorizationCodeProvider`
|
|
201
|
+
and `ClientCredentialsProvider` require a secret, so for them `''` is missing.
|
|
202
|
+
|
|
203
|
+
**The `authorization` option** is the interactive half of
|
|
204
|
+
`authorization_code` and `passcode`: a function of the destination and the
|
|
205
|
+
grant, returning the `IAuthorizationStrategy<string>` the provider logs in
|
|
206
|
+
with. The broker calls it once, when it builds that destination's provider,
|
|
207
|
+
and never disposes what it returns — whoever constructs, disposes. For
|
|
208
|
+
`passcode` the strategy is handed `<uaaUrl>/passcode` as the URL to send the
|
|
209
|
+
user to, and returns the code. There is no default: a destination whose grant
|
|
210
|
+
needs it, without it, is a `DestinationConfigError` naming `authorization`.
|
|
211
|
+
A headless process passes one that refuses (see *Headless Processes*).
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { browserCallbackStrategy, manualPasscodeStrategy } from '@mcp-abap-adt/auth-providers';
|
|
215
|
+
|
|
216
|
+
const broker = new AuthBroker({
|
|
217
|
+
serviceKeyStore: myKeyStore,
|
|
218
|
+
sessionStore: mySessionStore,
|
|
219
|
+
authorization: (destination, grant) =>
|
|
220
|
+
grant === 'passcode'
|
|
221
|
+
? manualPasscodeStrategy({ timeoutMs: 300_000 })
|
|
222
|
+
: browserCallbackStrategy({ timeoutMs: 120_000 }),
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**The OIDC grants.** The client is the key store's `getAuthorizationConfig`
|
|
227
|
+
again: `uaaClientId`, and `uaaClientSecret` — `''` is a public client, sent
|
|
228
|
+
with no secret. (A key store answers a client when it holds one: from
|
|
229
|
+
auth-stores 3.2.0, `EnvDestinationStore` answers one whenever `uaaClientId`
|
|
230
|
+
is stated, a missing `uaaUrl` or secret as `''` — so an OIDC destination
|
|
231
|
+
states its issuer in `oidcIssuerUrl` alone, and a public client no secret.)
|
|
232
|
+
The endpoints come from `oidcIssuerUrl`, which the provider discovers them
|
|
233
|
+
from, or — without it — from every explicit endpoint the row reads
|
|
234
|
+
(`oidcAuthorizationEndpoint`, `oidcDeviceAuthorizationEndpoint`,
|
|
235
|
+
`oidcTokenEndpoint`); without either the error names `oidcIssuerUrl` and the
|
|
236
|
+
endpoints missing. `oidcScopes` is passed as given (`token_exchange` takes
|
|
237
|
+
one `scope` string: the scopes joined by a space). The subject and actor
|
|
238
|
+
tokens of `token_exchange` and the user and password of `password` are
|
|
239
|
+
means: sent to the token endpoint, never written to the session.
|
|
240
|
+
|
|
241
|
+
**The SAML grants.** Each provider validates the assertion before anything
|
|
242
|
+
uses it, with a validator the broker composes from the destination's trust
|
|
243
|
+
and your replay store: `createSignedResponseValidator` for `saml2_pure` (the
|
|
244
|
+
Response must be signed — the cookies' system receives it whole) and
|
|
245
|
+
`createSignedAssertionValidator` for `saml2_bearer` (the Assertion must be
|
|
246
|
+
signed — the token endpoint receives it alone), from `samlIdpCertificates`
|
|
247
|
+
(PEM or base64 DER; several during a rotation), `samlClockSkewMs` and
|
|
248
|
+
`assertionReplayStore(destination)`; `samlIdpEntityId` is the issuer every
|
|
249
|
+
assertion must name. A certificate the validator cannot read, or a
|
|
250
|
+
`samlClockSkewMs` that is not a whole, non-negative number of milliseconds,
|
|
251
|
+
is a `DestinationConfigError` naming the field. `samlIdpInitiated: true`
|
|
252
|
+
declares an IdP-initiated login: no AuthnRequest, and an assertion carrying
|
|
253
|
+
no `InResponseTo` — your strategy then hands over the SAMLResponse without
|
|
254
|
+
asking for an authorization URL (a strategy that asks gets Oops from
|
|
255
|
+
`prepare()`). `saml2_pure` is seeded with the stored cookies and their
|
|
256
|
+
`expiresAt` — cookies carry no expiry of their own, so the provider keeps them
|
|
257
|
+
until the assertion's earliest `NotOnOrAfter` (less the provider's one-minute
|
|
258
|
+
margin: an identity provider whose assertions live a minute or less makes
|
|
259
|
+
every request a login); SAML has no refresh token, so a renewal is a new
|
|
260
|
+
login through your strategy. `saml2_bearer` posts the Assertion to
|
|
261
|
+
`samlTokenUrl`, else `<uaaUrl>/oauth/token`, with the client, and renews by
|
|
262
|
+
its refresh token.
|
|
263
|
+
|
|
264
|
+
**The collaborator options** — each a function of the destination, called
|
|
265
|
+
once when that destination's provider is built, never disposed by the
|
|
266
|
+
broker, and required only by the rows that use it; a row whose option is
|
|
267
|
+
missing is a `DestinationConfigError` naming it:
|
|
268
|
+
|
|
269
|
+
| Option | Rows | What it returns |
|
|
270
|
+
|---|---|---|
|
|
271
|
+
| `authorization(destination, grant)` | `authorization_code`, `passcode`, `saml2_pure`, `saml2_bearer` (the grant is passed) | the `IAuthorizationStrategy<string>` that conducts the login — for the SAML grants, the one that returns the SAMLResponse (`samlCallbackStrategy`, `manualSamlResponseStrategy`, …) |
|
|
272
|
+
| `oidcAuthorization(destination)` | `oidc_authorization_code` | an `IAuthorizationStrategy<OidcCallbackResult>` (`oidcCallbackStrategy`, or `asOidcResult(…)` over a string strategy) |
|
|
273
|
+
| `deviceCodePresenter(destination)` | `device_code` | an `IDeviceCodePresenter` that shows the user the verification URL and code (`consoleDeviceCodePresenter(logger)`, or your UI) |
|
|
274
|
+
| `samlCookies(destination)` | `saml2_pure` | `(samlResponse) => Promise<string>`: posts the validated SAMLResponse to the system's ACS and returns the session cookies it sets |
|
|
275
|
+
| `assertionReplayStore(destination)` | `saml2_pure`, `saml2_bearer` | the `IAssertionReplayStore` the validator records each assertion in, refusing one presented twice — `defaultReplayStore` (process-wide, in memory) or a shared one of yours |
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import {
|
|
279
|
+
consoleDeviceCodePresenter,
|
|
280
|
+
defaultReplayStore,
|
|
281
|
+
oidcCallbackStrategy,
|
|
282
|
+
samlCallbackStrategy,
|
|
283
|
+
} from '@mcp-abap-adt/auth-providers';
|
|
284
|
+
|
|
285
|
+
const broker = new AuthBroker(
|
|
286
|
+
{
|
|
287
|
+
serviceKeyStore: myKeyStore,
|
|
288
|
+
sessionStore: mySessionStore,
|
|
289
|
+
authorization: (destination, grant) =>
|
|
290
|
+
grant === 'saml2_pure' || grant === 'saml2_bearer'
|
|
291
|
+
? samlCallbackStrategy({ timeoutMs: 120_000 })
|
|
292
|
+
: browserCallbackStrategy({ timeoutMs: 120_000 }),
|
|
293
|
+
oidcAuthorization: () => oidcCallbackStrategy({ timeoutMs: 120_000 }),
|
|
294
|
+
deviceCodePresenter: () => consoleDeviceCodePresenter(logger),
|
|
295
|
+
samlCookies: (destination) => (samlResponse) =>
|
|
296
|
+
postToAcs(destination, samlResponse), // yours: the system's ACS answers Set-Cookie
|
|
297
|
+
assertionReplayStore: () => defaultReplayStore,
|
|
298
|
+
},
|
|
299
|
+
logger,
|
|
300
|
+
);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### A Secret Is Bound to Its Resource and Issuer
|
|
304
|
+
|
|
305
|
+
A token's audience, cookies' host and path, a system's client: presenting a
|
|
306
|
+
secret to a resource it was not obtained for is a leak, and a secret from
|
|
307
|
+
another authorization server or client must not stand in for this
|
|
308
|
+
destination's. So the session store keeps, beside the secret, two strings
|
|
309
|
+
(`IConnectionConfig`, `@mcp-abap-adt/interfaces-auth-broker` 1.1.0):
|
|
310
|
+
|
|
311
|
+
- **`issuedFor`** — the resource: `serviceUrl` with the SAP client, e.g.
|
|
312
|
+
`https://my-abap.example.com:443/sap/bc/adt?sap-client=100`;
|
|
313
|
+
- **`issuedBy`** — who issued it, to which client: `uaaUrl` with
|
|
314
|
+
`client_id=<uaaClientId>` for the UAA grants and `saml2_bearer`, e.g.
|
|
315
|
+
`https://sub.authentication.us10.hana.ondemand.com:443?client_id=sb-abap-trial`;
|
|
316
|
+
`oidcIssuerUrl` — else `uaaUrl` — with `client_id=<uaaClientId>` for the
|
|
317
|
+
OIDC grants; for `saml2_pure`'s cookies, the ACS of the system that set
|
|
318
|
+
them: `samlAcsUrl`, origin and path.
|
|
319
|
+
|
|
320
|
+
The broker computes both from the destination's means and compares them,
|
|
321
|
+
canonicalised on **both** sides, with what the session holds:
|
|
322
|
+
|
|
323
|
+
- **The canonical form:** scheme and host lower-cased; the port explicit
|
|
324
|
+
(`443` for `https`, `80` for `http`); the path without a trailing `/` (the
|
|
325
|
+
root path is empty); one query parameter at most — `sap-client` (the means'
|
|
326
|
+
`sapClient` wins over a `sap-client` already in `serviceUrl`) or
|
|
327
|
+
`client_id` — parsed and re-encoded, so a value percent-encoded on one side
|
|
328
|
+
and plain on the other compares equal; no user info, no other parameter,
|
|
329
|
+
no fragment. A URL that does not parse binds nothing. A store keeps the
|
|
330
|
+
strings as given and need not canonicalise them.
|
|
331
|
+
- **The grants that obtain a secret** (UAA, OIDC, SAML)**:** a stored secret seeds the provider only when **both**
|
|
332
|
+
stored values equal the computed ones. Otherwise — either different, or
|
|
333
|
+
absent on either side (no `serviceUrl` in the means, an OIDC destination
|
|
334
|
+
stating neither `oidcIssuerUrl` nor `uaaUrl`, a `saml2_pure` one without
|
|
335
|
+
`samlAcsUrl`, a session written without them) — the secret is not used,
|
|
336
|
+
**refresh token included**: the
|
|
337
|
+
provider is built as with no session and logs in afresh by its grant, and
|
|
338
|
+
the log says only `<destination>: secret bound to another resource,
|
|
339
|
+
discarded` — never a URI, never a token. The new secret is written with
|
|
340
|
+
both fields.
|
|
341
|
+
- **The `none` rows** present a credential the broker cannot obtain again, so
|
|
342
|
+
a mismatch is refused, not discarded: a `DestinationConfigError` naming
|
|
343
|
+
`issuedFor` when the stored resource differs or is absent (or the means
|
|
344
|
+
state no `serviceUrl`), and naming `issuedBy` when the means state an issuer
|
|
345
|
+
(`oidcIssuerUrl` or the client for `jwt`, `samlAcsUrl` for `saml` — then
|
|
346
|
+
canonicalised as origin and path) and the stored one differs or is absent.
|
|
347
|
+
Means that state no issuer leave `issuedBy` uncompared.
|
|
348
|
+
- **The binding does not check the token.** Its audience stays the
|
|
349
|
+
resource's to enforce; the broker compares two strings and parses no token.
|
|
350
|
+
- **`bindingOf(means, client?)` — for a consumer that hands over a
|
|
351
|
+
credential.** A `none` destination presents a token or cookies the broker
|
|
352
|
+
cannot obtain; whoever writes them to the session store must write the
|
|
353
|
+
binding beside them. `bindingOf` answers exactly what the broker computes for
|
|
354
|
+
those means — the one function `getProvider` checks against and `persist`
|
|
355
|
+
writes (`issuedFor` from `serviceUrl` + `sapClient`, `issuedBy` from the
|
|
356
|
+
issuer, client or ACS the grant uses) — so a consumer never canonicalises a
|
|
357
|
+
URI itself:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
const means = await keyStore.getConnectionConfig('DEV'); // saml / none
|
|
361
|
+
await sessionStore.saveSession('DEV', {
|
|
362
|
+
sessionCookies: cookies,
|
|
363
|
+
...bindingOf(means ?? {}), // { issuedFor: 'https://dev.example.com:443?sap-client=100' }
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
A destination that states no `jwt` / `saml` type or no grant binds nothing
|
|
368
|
+
(`{}`). `mcp-sso … --cookie` writes its cookies this way.
|
|
369
|
+
|
|
370
|
+
**What you meet:**
|
|
371
|
+
|
|
372
|
+
1. **A custom `ISessionStore`** (a database, a message log, a secret store)
|
|
373
|
+
must persist `issuedFor` and `issuedBy` beside the secret, and answer
|
|
374
|
+
them from `loadSession`. One that drops them still type-checks, but the
|
|
375
|
+
broker then never reuses its sessions: every process start is a fresh
|
|
376
|
+
login — a browser each time for an interactive grant.
|
|
377
|
+
2. **A headless process whose strategy refuses logins** gets Oops ("login
|
|
378
|
+
required") from `prepare()` / `rejected()` on a mismatch, instead of
|
|
379
|
+
presenting a foreign secret; log in again with the CLI.
|
|
380
|
+
3. **Changing a destination's URL, SAP client, UAA or client** costs one
|
|
381
|
+
fresh login — intended.
|
|
382
|
+
4. **Session files written before auth-stores 3.1.0** keep working: its
|
|
383
|
+
session stores answer `issuedFor` from the file's `SAP_URL` (+
|
|
384
|
+
`SAP_CLIENT`) and `issuedBy` from `SAP_UAA_URL` + `SAP_UAA_CLIENT_ID`,
|
|
385
|
+
which the 3.x broker and CLI wrote with the token — so a stored token whose
|
|
386
|
+
URL and client are the destination's is reused. XSUAA sessions written with
|
|
387
|
+
an empty URL have no `issuedFor`, and log in once.
|
|
388
|
+
|
|
389
|
+
**One provider per destination** for the broker's life, shared with the token
|
|
390
|
+
API when the broker has no `provider` option (see *Getting Tokens*):
|
|
391
|
+
concurrent first calls share one build, and a build that threw is tried again
|
|
392
|
+
on the next call. A destination rewritten from outside is picked up by a new
|
|
393
|
+
broker.
|
|
394
|
+
|
|
395
|
+
### Persistence and `flush()`
|
|
396
|
+
|
|
397
|
+
Every token provider `getProvider` builds writes what it obtains back to the
|
|
398
|
+
session store, through its `onTokens` hook, before it answers — whichever
|
|
399
|
+
moment triggered the renewal: `prepare()`, `authorize()` on expiry, or
|
|
400
|
+
`rejected()` after a 401. A renewal inside a connector is stored before the
|
|
401
|
+
connector resends.
|
|
402
|
+
|
|
403
|
+
- **The secret alone, with its binding, in one write**:
|
|
404
|
+
`saveSession(destination, { authorizationToken, expiresAt, refreshToken,
|
|
405
|
+
issuedFor, issuedBy })` — or, for `saml2_pure`'s cookies (a result of
|
|
406
|
+
`tokenType: 'saml'`), `{ sessionCookies, expiresAt, issuedFor, issuedBy }`,
|
|
407
|
+
with no refresh token: SAML has none. `saml2_bearer`'s result is a token
|
|
408
|
+
and is written as one — `issuedFor` / `issuedBy` as computed when the
|
|
409
|
+
provider was built, each left out when the means lack its source (so the
|
|
410
|
+
store clears it). No means is ever written — not `serviceUrl`, not
|
|
411
|
+
`authType`, not the client: the client secret lives in the key store, and
|
|
412
|
+
the broker never writes the key store (its contract has no write method).
|
|
413
|
+
`expiresAt` is the result's, else the `expires_in` it reported counted from
|
|
414
|
+
when it arrived. A result without a refresh token keeps the one the session
|
|
415
|
+
holds — when that one is bound where the new secret is; one bound to
|
|
416
|
+
another resource or issuer is not carried into it.
|
|
417
|
+
- **`basic` and `snc` are never written**: they obtain no session secret, and
|
|
418
|
+
`none` obtains nothing either. A destination the key store states as
|
|
419
|
+
`basic` or `snc` at write time is not written.
|
|
420
|
+
- **The token API writes through the same path** (see *Getting Tokens*):
|
|
421
|
+
with a `provider` of yours, every answer it gets is written as above — the
|
|
422
|
+
stored refresh token carried forward whatever its binding, as your provider
|
|
423
|
+
was seeded with it.
|
|
424
|
+
- **A failed write does not fail the authentication.** The connector goes on
|
|
425
|
+
with the token it holds; the broker keeps the result pending for that
|
|
426
|
+
destination and retries on its own — one second, doubling, capped at one
|
|
427
|
+
minute — on a timer that never keeps the process alive. A newer result
|
|
428
|
+
replaces the pending one; writes for one destination never overlap. Each
|
|
429
|
+
failure is logged by its class name, never its message. The token API's
|
|
430
|
+
caller still gets the failure: `getToken()` / `refreshToken()` throw the
|
|
431
|
+
store's error for the token they received, while the retry goes on.
|
|
432
|
+
- **`flush()`** gives every pending write one more attempt, resolves when all
|
|
433
|
+
have landed, and rejects (an `AggregateError` naming the destinations; each
|
|
434
|
+
of its `errors` is `"<destination>": <error class>` — never the store's own
|
|
435
|
+
message, which may quote what was being written) when the store still
|
|
436
|
+
refuses — the broker keeps retrying.
|
|
437
|
+
Call it on shutdown — on `SIGTERM`, before a stdio transport closes — to
|
|
438
|
+
know whether every token is stored.
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
process.on('SIGTERM', async () => {
|
|
442
|
+
try {
|
|
443
|
+
await broker.flush();
|
|
444
|
+
} catch (error) {
|
|
445
|
+
logger.error(`Tokens not stored: ${(error as Error).message}`);
|
|
446
|
+
}
|
|
447
|
+
process.exit(0);
|
|
95
448
|
});
|
|
96
449
|
```
|
|
97
450
|
|
|
98
|
-
|
|
99
|
-
|
|
451
|
+
**A destination that lacks what its type needs** is a `DestinationConfigError`
|
|
452
|
+
(see *Error Handling*), thrown by `getProvider` before any provider exists —
|
|
453
|
+
never a provider built only to refuse in `prepare()`.
|
|
100
454
|
|
|
101
455
|
### Headless Processes (No Browser)
|
|
102
456
|
|
|
@@ -129,6 +483,34 @@ try {
|
|
|
129
483
|
A cached token that is still valid, or a refresh token the UAA accepts, never
|
|
130
484
|
reaches the strategy.
|
|
131
485
|
|
|
486
|
+
With `getProvider` the same holds for every collaborator option: give the
|
|
487
|
+
broker an `authorization` / `oidcAuthorization` that refuses, and a
|
|
488
|
+
`deviceCodePresenter` that routes the code to wherever a person can see it —
|
|
489
|
+
or refuses. The provider then runs on its stored secret and refresh token;
|
|
490
|
+
when a login is needed, `prepare()` / `rejected()` answer Oops — whose
|
|
491
|
+
refusal carries fixed wording, never the message of what your collaborator
|
|
492
|
+
threw — and nothing is written:
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
const refuseLogin = {
|
|
496
|
+
authorize: async () => {
|
|
497
|
+
throw new LoginRequiredError('Run mcp-auth to log in');
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const broker = new AuthBroker({
|
|
502
|
+
serviceKeyStore: myKeyStore,
|
|
503
|
+
sessionStore: mySessionStore,
|
|
504
|
+
authorization: () => refuseLogin,
|
|
505
|
+
oidcAuthorization: () => refuseLogin,
|
|
506
|
+
deviceCodePresenter: () => ({
|
|
507
|
+
present: async () => {
|
|
508
|
+
throw new LoginRequiredError('No one to show a device code to');
|
|
509
|
+
},
|
|
510
|
+
}),
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
132
514
|
### Custom Browser Auth Port
|
|
133
515
|
|
|
134
516
|
How a login is conducted — including which port the local OAuth2 callback
|
|
@@ -156,41 +538,118 @@ const token = await broker.getToken('TRIAL');
|
|
|
156
538
|
const newToken = await broker.refreshToken('TRIAL');
|
|
157
539
|
```
|
|
158
540
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
541
|
+
**Which provider answers.**
|
|
542
|
+
|
|
543
|
+
- **No `provider` option** — the destination's own: the very provider
|
|
544
|
+
`getProvider('TRIAL')` hands out, from the same per-destination cache. A
|
|
545
|
+
connector and the token API in one process therefore share one token, one
|
|
546
|
+
refresh token and one renewal: a `refreshToken()` while the connector renews
|
|
547
|
+
in `rejected()` joins that renewal, and a token the connector renewed is
|
|
548
|
+
what `getToken()` answers next. The destination must state a grant that
|
|
549
|
+
obtains a token — every grant but `none` (see the table under *A Provider
|
|
550
|
+
for a Connector*); a `none` destination is a `DestinationConfigError`
|
|
551
|
+
naming `provider`, before any provider is built.
|
|
552
|
+
- **A `provider` option** — yours, as in 3.x (see *Basic Usage*): the factory
|
|
553
|
+
is called once per destination (concurrent first calls build once; a call
|
|
554
|
+
after one that threw builds again), an instance serves every destination.
|
|
555
|
+
|
|
556
|
+
**What is written.** The session secret alone, in one `saveSession` — the
|
|
557
|
+
token, or the cookies of a SAML result (`tokenType: 'saml'`), its
|
|
558
|
+
`expiresAt`, the refresh token (the stored one carried forward when the
|
|
559
|
+
result has none), and what the secret is bound to (`issuedFor`, `issuedBy`,
|
|
560
|
+
see *A Secret Is Bound to Its Resource and Issuer*). Never `serviceUrl`,
|
|
561
|
+
`authType` or a client: they are means, and live in the key store.
|
|
562
|
+
|
|
563
|
+
- Without a `provider`, the token API writes nothing itself: the provider's
|
|
564
|
+
`onTokens` writes every token it obtains (see *Persistence and `flush()`*),
|
|
565
|
+
and a cache hit writes nothing.
|
|
566
|
+
- With a `provider`, the token API writes after every answer, cache hits
|
|
567
|
+
included, through the same write path. `issuedFor` is the `serviceUrl` it
|
|
568
|
+
resolved, with the SAP client; `issuedBy` the client a factory was handed —
|
|
569
|
+
an instance is handed none, so what it obtains is bound to no issuer and
|
|
570
|
+
`getProvider` never seeds from it. Both are fixed when your provider is
|
|
571
|
+
built for the destination (an instance: at its first call for it) and kept
|
|
572
|
+
with it: a URL or client changed later never re-labels a token that
|
|
573
|
+
provider obtained — a new broker picks the change up.
|
|
574
|
+
|
|
575
|
+
**A write that fails** reaches the caller: `getToken()` / `refreshToken()`
|
|
576
|
+
throw the store's error, as raised, for the token they received — and the
|
|
577
|
+
broker keeps retrying that write on its own; `flush()` tells you when it
|
|
578
|
+
landed.
|
|
579
|
+
|
|
580
|
+
**A destination stated `basic` or `snc` has no token API.** The token API
|
|
581
|
+
reads the destination's `authType` from the key store first and, for `basic`
|
|
582
|
+
or `snc`, throws `DestinationConfigError` naming `authType` before any provider
|
|
583
|
+
is asked — a token written over such a session would replace a credential
|
|
584
|
+
that is not one. A key store that states no `authType` (a 3.x setup), or no
|
|
585
|
+
key store, is served as before.
|
|
586
|
+
|
|
587
|
+
**Two token sources, with a `provider` option.** `getProvider` never uses
|
|
588
|
+
the `provider` option: the destination states its own provider. A process
|
|
589
|
+
that gives the broker a `provider` and also calls `getProvider` for the same
|
|
590
|
+
destination has two token sources for it — each with its own token and
|
|
591
|
+
renewal, both writing to the same session. Use one of them per destination;
|
|
592
|
+
or, to hand your own provider to a connector, wrap the token API:
|
|
593
|
+
`TokenAuthProvider.from(broker.createTokenRefresher('TRIAL'))`
|
|
594
|
+
(`@mcp-abap-adt/auth-providers`), whose every renewal the token API writes.
|
|
162
595
|
|
|
163
596
|
### Creating Token Refresher for DI
|
|
164
597
|
|
|
165
|
-
|
|
598
|
+
`createTokenRefresher(destination)` returns an `ITokenRefresher` (from
|
|
599
|
+
`@mcp-abap-adt/interfaces-auth`) bound to one destination — `getToken()` is
|
|
600
|
+
`broker.getToken(destination)`, `refreshToken()` is
|
|
601
|
+
`broker.refreshToken(destination)` — for a connection of your own that asks
|
|
602
|
+
for a token per request and for a new one after a 401. It is unchanged since
|
|
603
|
+
3.x and not deprecated: calm-server injects it into its own connection.
|
|
166
604
|
|
|
167
605
|
```typescript
|
|
168
|
-
import { AuthBroker } from '@mcp-abap-adt/auth-broker';
|
|
169
|
-
import { JwtAbapConnection } from '@mcp-abap-adt/connection';
|
|
170
|
-
|
|
171
|
-
// Create broker
|
|
172
|
-
const broker = new AuthBroker({
|
|
173
|
-
sessionStore: mySessionStore,
|
|
174
|
-
serviceKeyStore: myServiceKeyStore,
|
|
175
|
-
provider: myProviderFactory,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// Create token refresher for specific destination
|
|
179
606
|
const tokenRefresher = broker.createTokenRefresher('TRIAL');
|
|
180
607
|
|
|
181
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// - getToken(): the provider's current token (broker.getToken)
|
|
186
|
-
// - refreshToken(): a new token, never the cached one (broker.refreshToken)
|
|
608
|
+
// In your own connection:
|
|
609
|
+
const token = await tokenRefresher.getToken();
|
|
610
|
+
// …the server answered 401:
|
|
611
|
+
const newToken = await tokenRefresher.refreshToken();
|
|
187
612
|
```
|
|
188
613
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
614
|
+
A `@mcp-abap-adt/connection` 10 connector takes an `IAuthProvider` instead:
|
|
615
|
+
give it `await broker.getProvider('TRIAL')`.
|
|
616
|
+
|
|
617
|
+
## Migrating from 3.x
|
|
618
|
+
|
|
619
|
+
4.0.0 is a major: what a 3.x consumer must do, in short (the full list is the
|
|
620
|
+
[CHANGELOG](CHANGELOG.md)'s 4.0.0 entry).
|
|
621
|
+
|
|
622
|
+
1. **Session store: `@mcp-abap-adt/auth-stores` 3 or later** (`^3.1.0` for
|
|
623
|
+
the binding), or one of your own on the
|
|
624
|
+
`@mcp-abap-adt/interfaces-auth-broker` contract. The broker writes the
|
|
625
|
+
secret alone — token or cookies, `expiresAt`, refresh token, `issuedFor`,
|
|
626
|
+
`issuedBy` — and auth-stores 1.x/2.x's session stores refuse such a write
|
|
627
|
+
(their `AbapSessionStore` throws without `serviceUrl`): they are not
|
|
628
|
+
supported. A custom store must keep `issuedFor` and `issuedBy`.
|
|
629
|
+
2. **State the means in a key store**, not in the session: `authType`, and
|
|
630
|
+
for `jwt` / `saml` a `grantType`; the client; the URL. For a SAP service
|
|
631
|
+
key, `new AbapServiceKeyStore(dir, { grantType: 'authorization_code' })`
|
|
632
|
+
(or `XsuaaServiceKeyStore`); otherwise `EnvDestinationStore(dir, {
|
|
633
|
+
fallback })`. A 3.x session file is a readable destination as it is once
|
|
634
|
+
it states `SAP_AUTH_TYPE` (and `SAP_GRANT_TYPE` for `jwt` / `saml`).
|
|
635
|
+
3. **A connector of `@mcp-abap-adt/connection` 10** takes `await
|
|
636
|
+
broker.getProvider(destination)` — no `getToken` before connecting, no
|
|
637
|
+
token refresher. Give the broker the collaborators its grants need
|
|
638
|
+
(`authorization`, `oidcAuthorization`, `deviceCodePresenter`,
|
|
639
|
+
`samlCookies`, `assertionReplayStore`); there is no default.
|
|
640
|
+
4. **The token API** keeps its calls, signatures and 3.x reads with a
|
|
641
|
+
`provider` of yours; it writes the secret alone, refuses a destination
|
|
642
|
+
stated `basic` or `snc`, and throws a failed write while the broker retries
|
|
643
|
+
it.
|
|
644
|
+
5. **`getConnectionConfig` / `getAuthorizationConfig`** compose the key
|
|
645
|
+
store's means with the session's secret; a URL or client kept only in the
|
|
646
|
+
session store is not found any more.
|
|
647
|
+
6. **New:** catch `DestinationConfigError`; call `flush()` on shutdown;
|
|
648
|
+
write `bindingOf(means)` beside a credential you hand over yourself.
|
|
649
|
+
7. **Contracts:** `interfaces-auth` 3, `interfaces-auth-sap` 2,
|
|
650
|
+
`interfaces-auth-broker` 1.1 (the store contracts moved there from
|
|
651
|
+
`interfaces-auth-sap`), auth-providers 5. The commands are
|
|
652
|
+
`@mcp-abap-adt/auth-broker-cli` 2.0.0.
|
|
194
653
|
|
|
195
654
|
## Migrating from 2.2.0
|
|
196
655
|
|
|
@@ -218,111 +677,72 @@ const connection = new JwtAbapConnection(config, tokenRefresher);
|
|
|
218
677
|
|
|
219
678
|
### Environment Variables
|
|
220
679
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
- `DEBUG_BROKER` - Enable debug logging for `auth-broker` package (short name)
|
|
228
|
-
- Set to `true` to enable logging (default: `false`)
|
|
229
|
-
- When enabled, logs authentication steps, token operations, and error details
|
|
230
|
-
- Can be explicitly disabled by setting to `false`
|
|
231
|
-
- Example: `DEBUG_BROKER=true npm test`
|
|
232
|
-
|
|
233
|
-
- `DEBUG_AUTH_BROKER` - Long name (backward compatibility)
|
|
234
|
-
- Same as `DEBUG_BROKER`, but longer name
|
|
235
|
-
- Example: `DEBUG_AUTH_BROKER=true npm test`
|
|
236
|
-
|
|
237
|
-
- `LOG_LEVEL` - Control log verbosity level
|
|
238
|
-
- Values: `debug`, `info`, `warn`, `error` (default: `info`)
|
|
239
|
-
- `debug` - All messages including detailed debug information
|
|
240
|
-
- `info` - Informational messages, warnings, and errors
|
|
241
|
-
- `warn` - Warnings and errors only
|
|
242
|
-
- `error` - Errors only
|
|
243
|
-
- Example: `LOG_LEVEL=debug DEBUG_BROKER=true npm test`
|
|
680
|
+
The library reads no environment variable: the stores take their directories
|
|
681
|
+
from their constructors, and the providers their settings from the
|
|
682
|
+
destination's means and the collaborators you give the broker. (In the
|
|
683
|
+
repository's test suites, `DEBUG_BROKER=true` — or `DEBUG_AUTH_BROKER=true`,
|
|
684
|
+
`DEBUG=broker` — turns on the test logger, its level from `AUTH_LOG_LEVEL`.)
|
|
244
685
|
|
|
245
|
-
|
|
246
|
-
- Set to `true` to enable all debug logging
|
|
247
|
-
- Or set to a string containing `broker` or `auth-broker` to enable only this package
|
|
248
|
-
- Example: `DEBUG=true npm test` or `DEBUG=broker npm test` or `DEBUG=auth-broker npm test`
|
|
686
|
+
### Logging
|
|
249
687
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
- `DEBUG_PROVIDER` (short) or `DEBUG_AUTH_PROVIDERS` (long) - Enable logging for `@mcp-abap-adt/auth-providers` package
|
|
688
|
+
Pass an `ILogger` as the constructor's second argument (for instance
|
|
689
|
+
`DefaultLogger` from `@mcp-abap-adt/logger`); without one, nothing is logged.
|
|
253
690
|
|
|
254
|
-
**
|
|
691
|
+
**What is logged:** the broker's initialization (whether a key store is
|
|
692
|
+
given; the `provider` option: `none`, `factory` or `instance`), each provider build (the destination's `authType` and grant, whether
|
|
693
|
+
it was seeded), each session secret saved (token or cookies, whether a
|
|
694
|
+
refresh token came back, the expiry), a stored secret discarded because it is
|
|
695
|
+
bound elsewhere (the destination only), and each failed write with the error's
|
|
696
|
+
class name.
|
|
255
697
|
|
|
256
|
-
|
|
698
|
+
**What is never logged:** any part of a token, refresh token, password or
|
|
699
|
+
secret — not a prefix, not a suffix — nor a store error's message, which may
|
|
700
|
+
quote what was being written.
|
|
257
701
|
|
|
258
|
-
|
|
702
|
+
With `DefaultLogger` at `info`:
|
|
259
703
|
|
|
260
|
-
**What is logged:** broker initialization (which stores, instance or factory),
|
|
261
|
-
provider builds (whether credentials, a refresh token and a stored token were
|
|
262
|
-
there), and each token saved (token type, grant, whether a refresh token came
|
|
263
|
-
back, expiry). Store read failures are logged as warnings.
|
|
264
|
-
|
|
265
|
-
**What is never logged:** any part of a token, refresh token or secret — not a
|
|
266
|
-
prefix, not a suffix. A log line says whether a token is there, never what it is.
|
|
267
|
-
|
|
268
|
-
Example output with `DEBUG_BROKER=true LOG_LEVEL=info`:
|
|
269
704
|
```
|
|
270
|
-
[INFO] ℹ️ [
|
|
705
|
+
[INFO] ℹ️ [AuthBroker] Session secret saved for TRIAL
|
|
706
|
+
{"credential":"token","hasRefreshToken":true,"expiresAt":1790000000000}
|
|
271
707
|
```
|
|
272
708
|
|
|
273
|
-
**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.
|
|
274
|
-
|
|
275
709
|
### File Structure
|
|
276
710
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
711
|
+
With `@mcp-abap-adt/auth-stores` 3 one `<destination>.env` file can hold a
|
|
712
|
+
destination whole, each store reading and writing only its own keys — the
|
|
713
|
+
means through `EnvDestinationStore`, the secret through `AbapSessionStore`
|
|
714
|
+
(or both in separate directories, as you compose them):
|
|
280
715
|
|
|
281
716
|
```env
|
|
717
|
+
# The means — EnvDestinationStore (ABAP_DESTINATION_VARS); the broker never writes them
|
|
282
718
|
SAP_URL=https://your-system.abap.us10.hana.ondemand.com
|
|
283
719
|
SAP_CLIENT=100
|
|
284
|
-
|
|
285
|
-
|
|
720
|
+
SAP_AUTH_TYPE=jwt
|
|
721
|
+
SAP_GRANT_TYPE=authorization_code
|
|
286
722
|
SAP_UAA_URL=https://your-account.authentication.us10.hana.ondemand.com
|
|
287
723
|
SAP_UAA_CLIENT_ID=client_id
|
|
288
724
|
SAP_UAA_CLIENT_SECRET=client_secret
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
#### Environment File for XSUAA (`{destination}.env`)
|
|
292
|
-
|
|
293
|
-
For XSUAA connections (reduced scope), use `XSUAA_*` environment variables:
|
|
294
|
-
|
|
295
|
-
```env
|
|
296
|
-
XSUAA_MCP_URL=https://your-mcp-server.cfapps.eu10.hana.ondemand.com
|
|
297
|
-
XSUAA_JWT_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
298
|
-
XSUAA_REFRESH_TOKEN=refresh_token_string
|
|
299
|
-
XSUAA_UAA_URL=https://your-account.authentication.eu10.hana.ondemand.com
|
|
300
|
-
XSUAA_UAA_CLIENT_ID=client_id
|
|
301
|
-
XSUAA_UAA_CLIENT_SECRET=client_secret
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
**Note**: `XSUAA_MCP_URL` is optional - it's not part of authentication, only needed for making requests. The token and UAA credentials are sufficient for authentication.
|
|
305
725
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
BTP_JWT_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
313
|
-
BTP_REFRESH_TOKEN=refresh_token_string
|
|
314
|
-
BTP_UAA_URL=https://your-account.authentication.eu10.hana.ondemand.com
|
|
315
|
-
BTP_UAA_CLIENT_ID=client_id
|
|
316
|
-
BTP_UAA_CLIENT_SECRET=client_secret
|
|
317
|
-
BTP_SAP_CLIENT=100
|
|
318
|
-
BTP_LANGUAGE=EN
|
|
726
|
+
# The secret — AbapSessionStore (ABAP_SESSION_VARS); what the broker writes
|
|
727
|
+
SAP_JWT_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
728
|
+
SAP_EXPIRES_AT=1790000000000
|
|
729
|
+
SAP_REFRESH_TOKEN=refresh_token_string
|
|
730
|
+
SAP_ISSUED_FOR=https://your-system.abap.us10.hana.ondemand.com:443?sap-client=100
|
|
731
|
+
SAP_ISSUED_BY=https://your-account.authentication.us10.hana.ondemand.com:443?client_id=client_id
|
|
319
732
|
```
|
|
320
733
|
|
|
321
|
-
|
|
734
|
+
SAML cookies are `SAP_SESSION_COOKIES_B64` in place of `SAP_JWT_TOKEN`. The
|
|
735
|
+
XSUAA stores use the same names with `XSUAA_` (`XSUAA_DESTINATION_VARS`,
|
|
736
|
+
`XSUAA_SESSION_VARS`; the URL is `XSUAA_MCP_URL`). Every means key
|
|
737
|
+
(`SAP_USERNAME`, `SAP_SNC_*`, `SAP_OIDC_*`, `SAP_SAML_*`, …) is listed in
|
|
738
|
+
[USAGE.md](../../docs/using/USAGE.md#environment-variables).
|
|
322
739
|
|
|
323
|
-
#### Service Key File
|
|
740
|
+
#### Service Key File (`{destination}.json`)
|
|
324
741
|
|
|
325
|
-
|
|
742
|
+
A SAP service key, read by `AbapServiceKeyStore` (an ABAP environment key:
|
|
743
|
+
the client and the ABAP URL) or `XsuaaServiceKeyStore` (an XSUAA key: the
|
|
744
|
+
client). A key cannot state which grant the destination uses, so the store is
|
|
745
|
+
told: `new AbapServiceKeyStore(dir, { grantType: 'authorization_code' })`.
|
|
326
746
|
|
|
327
747
|
```json
|
|
328
748
|
{
|
|
@@ -335,40 +755,9 @@ Standard ABAP service key format:
|
|
|
335
755
|
}
|
|
336
756
|
```
|
|
337
757
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
```json
|
|
343
|
-
{
|
|
344
|
-
"url": "https://your-account.authentication.eu10.hana.ondemand.com",
|
|
345
|
-
"apiurl": "https://api.authentication.eu10.hana.ondemand.com",
|
|
346
|
-
"clientid": "your_client_id",
|
|
347
|
-
"clientsecret": "your_client_secret"
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
**Note**: For XSUAA service keys, `apiurl` is prioritized over `url` for UAA authorization if present.
|
|
352
|
-
|
|
353
|
-
## XSUAA vs BTP Authentication
|
|
354
|
-
|
|
355
|
-
This package supports two types of BTP authentication:
|
|
356
|
-
|
|
357
|
-
### XSUAA (Reduced Scope)
|
|
358
|
-
- **Purpose**: Access BTP services with limited scopes
|
|
359
|
-
- **Service Key**: Contains only UAA credentials (no ABAP URL)
|
|
360
|
-
- **Session Store**: `XsuaaSessionStore` (uses `XSUAA_*` environment variables)
|
|
361
|
-
- **Authentication**: Client credentials grant type (no browser required)
|
|
362
|
-
- **MCP URL**: Optional, provided separately (from YAML config `mcp_url`, parameter, or request header)
|
|
363
|
-
- **Use Case**: Accessing BTP services like MCP servers with reduced permissions
|
|
364
|
-
|
|
365
|
-
### BTP (Full Scope for ABAP)
|
|
366
|
-
- **Purpose**: Access ABAP systems with full roles and scopes
|
|
367
|
-
- **Service Key**: Contains UAA credentials and ABAP URL
|
|
368
|
-
- **Session Store**: `BtpSessionStore` (uses `BTP_*` environment variables)
|
|
369
|
-
- **Authentication**: Browser-based OAuth2 (like ABAP) or refresh token
|
|
370
|
-
- **ABAP URL**: Required, from service key or YAML configuration
|
|
371
|
-
- **Use Case**: Accessing ABAP systems in BTP with full permissions
|
|
758
|
+
An XSUAA key carries no URL of the resource it authorizes for: state it as
|
|
759
|
+
means in an `EnvDestinationStore` (`XSUAA_MCP_URL`, with
|
|
760
|
+
`XSUAA_DESTINATION_VARS`) whose `fallback` is the `XsuaaServiceKeyStore`.
|
|
372
761
|
|
|
373
762
|
## Responsibilities and Design Principles
|
|
374
763
|
|
|
@@ -377,7 +766,7 @@ This package supports two types of BTP authentication:
|
|
|
377
766
|
**Interface-Only Communication**: This package follows a fundamental development principle: **all interactions with external dependencies happen ONLY through interfaces**. The code knows **NOTHING beyond what is defined in the interfaces**.
|
|
378
767
|
|
|
379
768
|
This means:
|
|
380
|
-
- Does not know about concrete
|
|
769
|
+
- Does not know about concrete store classes (e.g., `AbapSessionStore`); the provider classes it knows only to construct the one a destination states, and hands out as `IAuthProvider`
|
|
381
770
|
- Does not know about internal data structures or methods not defined in interfaces
|
|
382
771
|
- Does not make assumptions about implementation behavior beyond interface contracts
|
|
383
772
|
- Does not access properties or methods not explicitly defined in interfaces
|
|
@@ -394,11 +783,11 @@ The `@mcp-abap-adt/auth-broker` package defines **interfaces** and provides **or
|
|
|
394
783
|
|
|
395
784
|
#### What AuthBroker Does
|
|
396
785
|
|
|
397
|
-
- **Resolves what the stores hold**: the
|
|
398
|
-
- **Builds or reuses the provider**:
|
|
786
|
+
- **Resolves what the stores hold**: the means from the key store, the secret from the session store (and, for a `provider` of yours, the 3.x reads: the service URL and the UAA credentials, session first)
|
|
787
|
+
- **Builds or reuses the provider**: one per destination — the one the destination states, shared by `getProvider` and the token API; or your factory, called once per destination
|
|
399
788
|
- **Asks the provider once**: `getTokens()` for `getToken()`, `refreshTokens()` for `refreshToken()` — no retries, no fallbacks
|
|
400
|
-
- **Persists the answer**: token or session cookies,
|
|
401
|
-
- **Works with interfaces only**: `IServiceKeyStore`, `ISessionStore`, `IRefreshableTokenProvider`
|
|
789
|
+
- **Persists the answer**: the session secret alone — token or session cookies, `expiresAt`, the refresh token, and what it is bound to — to `sessionStore`, retrying a write that fails
|
|
790
|
+
- **Works with interfaces only**: `IServiceKeyStore`, `ISessionStore`, `IAuthProvider`, `IRefreshableTokenProvider`
|
|
402
791
|
|
|
403
792
|
#### What AuthBroker Does NOT Do
|
|
404
793
|
|
|
@@ -412,19 +801,15 @@ The `@mcp-abap-adt/auth-broker` package defines **interfaces** and provides **or
|
|
|
412
801
|
|
|
413
802
|
The **consumer** (application using `AuthBroker`) is responsible for:
|
|
414
803
|
|
|
415
|
-
1. **
|
|
416
|
-
- **ABAP systems**:
|
|
417
|
-
- **
|
|
418
|
-
- **XSUAA services**:
|
|
804
|
+
1. **Composing the stores and collaborators**: an `IServiceKeyStore` for the means and an `ISessionStore` for the secret — with auth-stores 3:
|
|
805
|
+
- **ABAP systems (a SAP service key)**: `AbapServiceKeyStore(dir, { grantType })`, alone or as the `fallback` of an `EnvDestinationStore`, and `AbapSessionStore` (or `SafeAbapSessionStore`, in memory)
|
|
806
|
+
- **Destinations without a key** (basic, SNC, OIDC, SAML, `none`): `EnvDestinationStore(dir)` and `AbapSessionStore`
|
|
807
|
+
- **XSUAA services**: `EnvDestinationStore(dir, { variables: XSUAA_DESTINATION_VARS, fallback: new XsuaaServiceKeyStore(keys, { grantType }) })` and `XsuaaSessionStore` (or `SafeXsuaaSessionStore`)
|
|
808
|
+
- the collaborator options the destinations' grants need — or, for the token API the 3.x way, an `IRefreshableTokenProvider` of your own
|
|
419
809
|
|
|
420
|
-
2. **
|
|
421
|
-
- The session is created with `serviceUrl` before calling `AuthBroker.getToken()`, OR
|
|
422
|
-
- The session store implementation handles `serviceUrl` retrieval internally (e.g., from `serviceKeyStore`)
|
|
810
|
+
2. **Stating the means in the key store**: `authType`, `grantType` and the fields its row reads, the client, and `serviceUrl` — for the token API with a `provider` of yours, the service URL comes from the session, else the key store.
|
|
423
811
|
|
|
424
|
-
3. **
|
|
425
|
-
- `AbapSessionStore`: Requires `sapUrl` (maps to `serviceUrl` in `IConnectionConfig`)
|
|
426
|
-
- `BtpSessionStore`: Does not require `serviceUrl` (uses `mcpUrl` instead)
|
|
427
|
-
- `XsuaaSessionStore`: Does not require `serviceUrl` (MCP URL is optional)
|
|
812
|
+
3. **A session store that takes the secret alone**: the broker writes the token or cookies, `expiresAt`, the refresh token, `issuedFor` and `issuedBy` in one `saveSession`, and nothing else. auth-stores 3's session stores do; auth-stores 1.x/2.x's `AbapSessionStore` and `SafeAbapSessionStore` refuse a write without `serviceUrl` (measured on 1.2.3, 1.2.4, 2.0.0), so a token API on them throws after every token. A custom store must also keep `issuedFor` and `issuedBy`.
|
|
428
813
|
|
|
429
814
|
### Store Responsibilities
|
|
430
815
|
|
|
@@ -432,10 +817,7 @@ Concrete `ISessionStore` implementations are responsible for:
|
|
|
432
817
|
|
|
433
818
|
- **Handling their own data format**: Each store knows its internal data format (e.g., `AbapSessionData`, `BtpBaseSessionData`)
|
|
434
819
|
- **Converting between formats**: Converting between `IConfig`/`IConnectionConfig` and internal storage format
|
|
435
|
-
- **
|
|
436
|
-
- Retrieve it from `serviceKeyStore` if not provided in `IConnectionConfig`, OR
|
|
437
|
-
- Use existing value from current session if available, OR
|
|
438
|
-
- Throw an error if neither is available (depending on implementation)
|
|
820
|
+
- **Taking the secret alone**: a session store is written the secret and its binding only, and keeps what it held beside them (auth-stores 3 refuses means outright)
|
|
439
821
|
|
|
440
822
|
### Provider Responsibilities
|
|
441
823
|
|
|
@@ -449,7 +831,7 @@ Concrete `IRefreshableTokenProvider` implementations are responsible for:
|
|
|
449
831
|
### Design Principles
|
|
450
832
|
|
|
451
833
|
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)
|
|
452
|
-
2. **Dependency Inversion Principle (DIP)**: `AuthBroker` depends on abstractions (`IServiceKeyStore`, `ISessionStore`, `IRefreshableTokenProvider`), not concrete implementations
|
|
834
|
+
2. **Dependency Inversion Principle (DIP)**: `AuthBroker` depends on abstractions (`IServiceKeyStore`, `ISessionStore`, `IAuthProvider`, `IRefreshableTokenProvider`), not concrete store implementations; the providers it builds it hands out as `IAuthProvider`
|
|
453
835
|
3. **Single Responsibility**: Each component has a single, well-defined responsibility:
|
|
454
836
|
- `AuthBroker`: Orchestration — resolving, asking, persisting
|
|
455
837
|
- `ISessionStore`: Session data storage and retrieval
|
|
@@ -468,54 +850,134 @@ Concrete `IRefreshableTokenProvider` implementations are responsible for:
|
|
|
468
850
|
new AuthBroker(
|
|
469
851
|
config: {
|
|
470
852
|
sessionStore: ISessionStore; // required
|
|
471
|
-
serviceKeyStore?: IServiceKeyStore; //
|
|
472
|
-
provider
|
|
853
|
+
serviceKeyStore?: IServiceKeyStore; // getProvider needs it
|
|
854
|
+
provider?: // the token API's, optional
|
|
473
855
|
| IRefreshableTokenProvider
|
|
474
856
|
| ((
|
|
475
857
|
destination: string,
|
|
476
858
|
authConfig: IAuthorizationConfig | null,
|
|
477
859
|
connConfig: IConnectionConfig,
|
|
478
860
|
) => IRefreshableTokenProvider);
|
|
861
|
+
// Collaborators, each a function of the destination, called once per
|
|
862
|
+
// build, never disposed by the broker:
|
|
863
|
+
authorization?: (destination: string, grant: StrategyGrant) => IAuthorizationStrategy<string>; // authorization_code, passcode, saml2_pure, saml2_bearer
|
|
864
|
+
oidcAuthorization?: (destination: string) => IAuthorizationStrategy<OidcCallbackResult>; // oidc_authorization_code
|
|
865
|
+
deviceCodePresenter?: (destination: string) => IDeviceCodePresenter; // device_code
|
|
866
|
+
samlCookies?: (destination: string) => (samlResponse: string) => Promise<string>; // saml2_pure
|
|
867
|
+
assertionReplayStore?: (destination: string) => IAssertionReplayStore; // saml2_pure, saml2_bearer
|
|
479
868
|
},
|
|
480
869
|
logger?: ILogger,
|
|
481
870
|
)
|
|
482
871
|
```
|
|
483
872
|
|
|
484
873
|
**Parameters:**
|
|
485
|
-
- `config.sessionStore` - **Required** -
|
|
486
|
-
- `config.serviceKeyStore` -
|
|
487
|
-
- `config.provider` -
|
|
874
|
+
- `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
|
+
- `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.
|
|
877
|
+
- `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
|
+
- `config.oidcAuthorization` - The interactive strategy of `jwt` / `oidc_authorization_code`.
|
|
879
|
+
- `config.deviceCodePresenter` - Where `jwt` / `device_code` shows the user the verification URL and code.
|
|
880
|
+
- `config.samlCookies` - `saml2_pure`: turns the validated SAMLResponse into the system's session cookies.
|
|
881
|
+
- `config.assertionReplayStore` - The replay store the SAML validators record each assertion in.
|
|
882
|
+
- 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.
|
|
488
883
|
- `logger` - Optional logger. If not provided, nothing is logged.
|
|
489
884
|
|
|
490
885
|
**Available Implementations:**
|
|
491
|
-
- **
|
|
492
|
-
- **
|
|
886
|
+
- **Means** (auth-stores 3): `EnvDestinationStore(directory, { fallback? })`, `AbapServiceKeyStore(directory, { grantType?, log? })`, `XsuaaServiceKeyStore(directory, { grantType?, log? })`
|
|
887
|
+
- **Secret** (auth-stores 3): `AbapSessionStore(directory, log?)`, `SafeAbapSessionStore(log?)`, `XsuaaSessionStore(directory, log?)`, `SafeXsuaaSessionStore(log?)`, `EnvFileSessionStore(file, log?)`
|
|
888
|
+
- **Providers** (auth-providers 5): `AuthorizationCodeProvider(...)`, `ClientCredentialsProvider(...)` and the rest, for a `provider` of yours
|
|
493
889
|
|
|
494
890
|
#### Methods
|
|
495
891
|
|
|
892
|
+
##### `getProvider(destination: string): Promise<IAuthProvider>`
|
|
893
|
+
|
|
894
|
+
The `IAuthProvider` the destination states, built from the key store's means
|
|
895
|
+
and the session store's secret (see *A Provider for a Connector*). Cached per
|
|
896
|
+
destination; a failed build is retried on the next call. Throws
|
|
897
|
+
`DestinationConfigError` for a destination that lacks what its type needs, and
|
|
898
|
+
passes a store failure other than absence on as the store raised it.
|
|
899
|
+
|
|
900
|
+
##### `flush(): Promise<void>`
|
|
901
|
+
|
|
902
|
+
Every session write still pending gets one more attempt. Resolves when all
|
|
903
|
+
have landed; rejects with an `AggregateError` naming the destinations whose
|
|
904
|
+
store still refuses (its `errors` carry each destination and the store
|
|
905
|
+
error's class only, never its message) — the broker keeps retrying them. See *Persistence and `flush()`*.
|
|
906
|
+
|
|
496
907
|
##### `getToken(destination: string): Promise<string>`
|
|
497
908
|
|
|
498
|
-
1.
|
|
499
|
-
2.
|
|
500
|
-
3.
|
|
501
|
-
4.
|
|
502
|
-
5. Returns the token.
|
|
909
|
+
1. Reads the destination's `authType` from the key store: `basic` or `snc` is a `DestinationConfigError` naming `authType`, before any provider is asked.
|
|
910
|
+
2. **Without a `provider` option:** takes the provider `getProvider(destination)` hands out (the same cache; a `none` destination is a `DestinationConfigError` naming `provider`), calls `getTokens()` once, and writes nothing itself — the provider's `onTokens` wrote anything new.
|
|
911
|
+
3. **With one:** resolves `serviceUrl` (session, else service key; an error if neither has one), builds the provider on first use (factory form) seeded with the credentials, the stored refresh token and the stored token — or uses the instance — calls `getTokens()` once, and writes the result: the secret alone, through the broker's write path.
|
|
912
|
+
4. Throws the store's error if the write of this token failed (the broker keeps retrying it); else returns the token.
|
|
503
913
|
|
|
504
914
|
##### `refreshToken(destination: string): Promise<string>`
|
|
505
915
|
|
|
506
916
|
The same, with `provider.refreshTokens()`: a new token, never the cached one —
|
|
507
|
-
for a caller whose token the server has just refused.
|
|
917
|
+
for a caller whose token the server has just refused. A renewal already in
|
|
918
|
+
flight for the destination (a connector's, in `rejected()`) is joined.
|
|
508
919
|
|
|
509
920
|
##### `getAuthorizationConfig(destination)` / `getConnectionConfig(destination)`
|
|
510
921
|
|
|
511
|
-
The
|
|
922
|
+
The two stores composed, each for its role:
|
|
923
|
+
|
|
924
|
+
- `getConnectionConfig` — the service key store's means with the session's
|
|
925
|
+
secret (`authorizationToken`, `sessionCookies`, `expiresAt`) laid over them;
|
|
926
|
+
`null` when neither store holds anything.
|
|
927
|
+
- `getAuthorizationConfig` — the service key store's client (`uaaUrl`,
|
|
928
|
+
`uaaClientId`, `uaaClientSecret`) with the session's refresh token; `null`
|
|
929
|
+
when the key store has no client.
|
|
930
|
+
|
|
931
|
+
Means a session store answers (a client, a URL, an `authType`) are not read,
|
|
932
|
+
nor a secret a key store answers. Up to 3.1.0 both answered the session's
|
|
933
|
+
configuration whole, and the key's only when the session had none.
|
|
512
934
|
|
|
513
935
|
##### `createTokenRefresher(destination): ITokenRefresher`
|
|
514
936
|
|
|
515
|
-
`getToken()` and `refreshToken()` bound to one destination, for injection into a connection.
|
|
937
|
+
`getToken()` and `refreshToken()` bound to one destination, for injection into a connection of your own. Unchanged since 3.x; not deprecated.
|
|
516
938
|
|
|
517
939
|
##### Error Handling
|
|
518
940
|
|
|
941
|
+
- **`DestinationConfigError`** — a destination that lacks what its type needs.
|
|
942
|
+
`getProvider` throws it before any provider is asked; so does the token
|
|
943
|
+
API, for the same destinations and for those below. It carries `code:
|
|
944
|
+
'DESTINATION_CONFIG'`, `destination`, `missingFields` — field or option
|
|
945
|
+
names only. When a provider's constructor refused (an `sncQop` outside `1`,
|
|
946
|
+
`2`, `3`, `8`, `9`), the store field is named and the provider's own error
|
|
947
|
+
is not kept — its message quotes the value. **No stored value
|
|
948
|
+
reaches it**: not a password, not a token, not an `authType` that is none of
|
|
949
|
+
the four. Thrown for:
|
|
950
|
+
|
|
951
|
+
| Case | `missingFields` |
|
|
952
|
+
|---|---|
|
|
953
|
+
| no `serviceKeyStore` option (`getProvider`) | `serviceKeyStore` |
|
|
954
|
+
| the token API with neither a `provider` nor a `serviceKeyStore` option | `provider`, `serviceKeyStore` |
|
|
955
|
+
| the token API on a destination stated `basic` or `snc` | `authType` |
|
|
956
|
+
| the token API without a `provider` option, on a `jwt` / `none` or `saml` / `none` destination | `provider` |
|
|
957
|
+
| the key store has no means for the destination — whatever the session holds | `authType` |
|
|
958
|
+
| no `authType`, `''`, or one that is not `basic`, `jwt`, `saml`, `snc` | `authType` |
|
|
959
|
+
| `jwt` / `saml` without `grantType`, `''`, or a pair outside the table | `grantType` |
|
|
960
|
+
| `basic` without user or password (`''` counts as missing) | `username`, `password` — each that is missing |
|
|
961
|
+
| `snc` without `sncPartnerName` | `sncPartnerName` |
|
|
962
|
+
| `snc` whose settings the provider refuses | the store field, e.g. `sncQop` |
|
|
963
|
+
| `jwt` / `none` without a token in the session | `authorizationToken` |
|
|
964
|
+
| `saml` / `none` without cookies in the session | `sessionCookies` |
|
|
965
|
+
| `jwt` / `none`, `saml` / `none` whose stored `issuedFor` is not the destination's resource, or is absent, or the means state no `serviceUrl` | `issuedFor` |
|
|
966
|
+
| `jwt` / `none`, `saml` / `none` whose means state an issuer and whose stored `issuedBy` is not it, or is absent | `issuedBy` |
|
|
967
|
+
| a UAA grant without its client in the key store (`''` counts as missing) | `uaaUrl`, `uaaClientId`, and `uaaClientSecret` for `authorization_code` / `client_credentials` — each that is missing |
|
|
968
|
+
| `authorization_code` / `passcode` without the `authorization` option | `authorization` |
|
|
969
|
+
| an OIDC grant without its client's id in the key store | `uaaClientId` |
|
|
970
|
+
| an OIDC grant with neither `oidcIssuerUrl` nor every endpoint its row reads | `oidcIssuerUrl`, and each endpoint missing: `oidcAuthorizationEndpoint` / `oidcDeviceAuthorizationEndpoint`, `oidcTokenEndpoint` |
|
|
971
|
+
| `password` without user or password | `username`, `password` — each that is missing |
|
|
972
|
+
| `token_exchange` without its subject | `oidcSubjectToken`, `oidcSubjectTokenType` — each that is missing |
|
|
973
|
+
| `oidc_authorization_code` without the `oidcAuthorization` option | `oidcAuthorization` |
|
|
974
|
+
| `device_code` without the `deviceCodePresenter` option | `deviceCodePresenter` |
|
|
975
|
+
| a SAML grant without its trust (`''` and an empty certificate list count as missing) | `samlIdpSsoUrl`, `samlSpEntityId`, `samlIdpEntityId`, `samlIdpCertificates` — each that is missing |
|
|
976
|
+
| `saml2_bearer` without its client | `uaaUrl`, `uaaClientId` — each that is missing |
|
|
977
|
+
| a SAML grant without its collaborators | `authorization`, `samlCookies` (`saml2_pure`), `assertionReplayStore` — each that is missing |
|
|
978
|
+
| a certificate the validator cannot read | `samlIdpCertificates` |
|
|
979
|
+
| a `samlClockSkewMs` that is not a whole, non-negative number | `samlClockSkewMs` |
|
|
980
|
+
|
|
519
981
|
- **Provider errors propagate unchanged** — the same object, with its class,
|
|
520
982
|
`code`, `missingFields` and `cause`: auth-providers' `ValidationError`,
|
|
521
983
|
`BrowserAuthError`, `AssertionValidationError`, network errors (`ECONNREFUSED`,
|
|
@@ -532,12 +994,25 @@ The session's configuration, else the service key's, else `null`.
|
|
|
532
994
|
the new token was written. (The session stores of
|
|
533
995
|
auth-stores answer an unreadable session file with `null` themselves, so it
|
|
534
996
|
reads as absent before the broker sees it.)
|
|
535
|
-
- **Store writes
|
|
997
|
+
- **Store writes**: a write that fails reaches the token API's caller — the
|
|
998
|
+
store's error, as raised, for the token it received — and is retried by the
|
|
999
|
+
broker all the same. A `getProvider` provider's write never fails the
|
|
1000
|
+
authentication; it is retried, and `flush()` reports it.
|
|
536
1001
|
- **A provider result without a token** is an error.
|
|
537
1002
|
|
|
538
1003
|
```typescript
|
|
1004
|
+
import { DestinationConfigError } from '@mcp-abap-adt/auth-broker';
|
|
539
1005
|
import { ValidationError } from '@mcp-abap-adt/auth-providers';
|
|
540
1006
|
|
|
1007
|
+
try {
|
|
1008
|
+
const provider = await broker.getProvider('TRIAL');
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
if (error instanceof DestinationConfigError) {
|
|
1011
|
+
logger.error(`${error.destination} lacks: ${error.missingFields.join(', ')}`);
|
|
1012
|
+
}
|
|
1013
|
+
throw error;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
541
1016
|
try {
|
|
542
1017
|
const token = await broker.getToken('TRIAL');
|
|
543
1018
|
} catch (error) {
|
|
@@ -550,38 +1025,32 @@ try {
|
|
|
550
1025
|
|
|
551
1026
|
#### Secrets in the Session Store
|
|
552
1027
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1028
|
+
Everything the broker writes — a `getProvider` provider's token and the
|
|
1029
|
+
token API's alike — is the secret alone: `authorizationToken` (or
|
|
1030
|
+
`sessionCookies`), `expiresAt`, `refreshToken`, with `issuedFor` and
|
|
1031
|
+
`issuedBy`, in one `saveSession`, and nothing else (see *Persistence and
|
|
1032
|
+
`flush()`*). Never the client secret, never `serviceUrl` or `authType`:
|
|
1033
|
+
`setConnectionConfig` and `setAuthorizationConfig` are not called. A store
|
|
1034
|
+
that merges a write into what it holds keeps a client or URL it held beside
|
|
1035
|
+
the secret; auth-stores 3 holds the secret alone.
|
|
558
1036
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
process's provider — the XSUAA stores too, from auth-stores 1.2.3.
|
|
1037
|
+
The stored refresh token comes back through `loadSession()`, which the broker
|
|
1038
|
+
reads to seed the next process's provider.
|
|
562
1039
|
|
|
563
1040
|
### Token Providers
|
|
564
1041
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
- No browser interaction required
|
|
570
|
-
- No refresh token provided
|
|
571
|
-
|
|
572
|
-
- **`AuthorizationCodeProvider`** - For BTP/ABAP authentication (full scope)
|
|
573
|
-
- How the login is conducted is an `authorization?: IAuthorizationStrategy<string>`, not a
|
|
574
|
-
provider field. Omitted, it defaults to a browser callback on port `61001`
|
|
575
|
-
- `browserCallbackStrategy({ browser?, port?, timeoutMs? })` from `@mcp-abap-adt/auth-providers`
|
|
576
|
-
builds the ready-made strategy; pass `port` to avoid conflicts when running alongside other
|
|
577
|
-
services (e.g. a proxy server) or to match a redirect URI registered at the identity provider
|
|
578
|
-
- The callback port is held only for the duration of a login and released when it ends —
|
|
579
|
-
by success, failure, timeout, or cancellation
|
|
580
|
-
- Uses browser-based OAuth2 flow (if no refresh token)
|
|
581
|
-
- Uses refresh token if available
|
|
582
|
-
- Provides refresh token for future use
|
|
1042
|
+
Provider implementations live in `@mcp-abap-adt/auth-providers` 5. `getProvider`
|
|
1043
|
+
builds the one the destination states (see *A Provider for a Connector*); for
|
|
1044
|
+
the token API the 3.x way you pass one yourself — an `IRefreshableTokenProvider`
|
|
1045
|
+
or a factory:
|
|
583
1046
|
|
|
584
|
-
|
|
1047
|
+
- **`ClientCredentialsProvider`** — `client_credentials`: no user, no browser,
|
|
1048
|
+
no refresh token.
|
|
1049
|
+
- **`AuthorizationCodeProvider`** — `authorization_code`: refreshes by the
|
|
1050
|
+
stored refresh token, and logs in through the `authorization` strategy you
|
|
1051
|
+
give it — there is no default. `browserCallbackStrategy({ browser?, port?,
|
|
1052
|
+
timeoutMs? })` builds the usual one (callback port `61001` unless `port` is
|
|
1053
|
+
given; held only for the duration of a login).
|
|
585
1054
|
|
|
586
1055
|
```typescript
|
|
587
1056
|
import { AuthBroker } from '@mcp-abap-adt/auth-broker';
|
|
@@ -593,7 +1062,7 @@ import { ClientCredentialsProvider } from '@mcp-abap-adt/auth-providers';
|
|
|
593
1062
|
|
|
594
1063
|
// XSUAA, client_credentials: credentials from the service key
|
|
595
1064
|
const xsuaaBroker = new AuthBroker({
|
|
596
|
-
sessionStore: new XsuaaSessionStore('/path/to/sessions'
|
|
1065
|
+
sessionStore: new XsuaaSessionStore('/path/to/sessions'),
|
|
597
1066
|
serviceKeyStore: new XsuaaServiceKeyStore('/path/to/keys'),
|
|
598
1067
|
provider: (destination, authConfig) => {
|
|
599
1068
|
if (!authConfig) throw new Error(`No UAA credentials for ${destination}`);
|
|
@@ -647,8 +1116,7 @@ integration case, and the run reaches no system.
|
|
|
647
1116
|
Complete documentation is available in the repository's [`docs/`](../../docs/) directory:
|
|
648
1117
|
|
|
649
1118
|
- **[Architecture](../../docs/architecture/ARCHITECTURE.md)** - System architecture and design decisions
|
|
650
|
-
- **[
|
|
651
|
-
- **[Development Roadmap](../../docs/development/DEVELOPMENT_ROADMAP.md)** - Development roadmap and future plans
|
|
1119
|
+
- **[Testing](../../docs/development/TESTING.md)** - Where the suites live, what they need, the release checks
|
|
652
1120
|
- **[Installation](../../docs/installing/INSTALLATION.md)** - Installation and setup guide
|
|
653
1121
|
- **[Usage](../../docs/using/USAGE.md)** - API reference and usage examples
|
|
654
1122
|
|