@mcp-abap-adt/auth-broker 4.1.0 → 5.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 +138 -1
- package/README.md +960 -1214
- package/dist/AuthBroker.d.ts +294 -110
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +726 -351
- package/dist/DestinationConfigError.d.ts +35 -6
- package/dist/DestinationConfigError.d.ts.map +1 -1
- package/dist/DestinationConfigError.js +51 -9
- package/dist/SessionWriter.d.ts +70 -33
- package/dist/SessionWriter.d.ts.map +1 -1
- package/dist/SessionWriter.js +118 -94
- package/dist/binding.d.ts +125 -74
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +320 -137
- package/dist/bindingOf.d.ts +33 -15
- package/dist/bindingOf.d.ts.map +1 -1
- package/dist/bindingOf.js +50 -20
- package/dist/buildIdentity.d.ts +137 -0
- package/dist/buildIdentity.d.ts.map +1 -0
- package/dist/buildIdentity.js +393 -0
- package/dist/clientAuthentication.d.ts +21 -6
- package/dist/clientAuthentication.d.ts.map +1 -1
- package/dist/clientAuthentication.js +55 -55
- package/dist/contractShape.d.ts +24 -0
- package/dist/contractShape.d.ts.map +1 -0
- package/dist/contractShape.js +24 -0
- package/dist/destinations.d.ts +149 -53
- package/dist/destinations.d.ts.map +1 -1
- package/dist/destinations.js +426 -160
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/quietLogger.d.ts +24 -0
- package/dist/quietLogger.d.ts.map +1 -0
- package/dist/quietLogger.js +63 -0
- package/package.json +8 -7
package/dist/binding.js
CHANGED
|
@@ -1,200 +1,373 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* A secret is bound to the resource it was obtained for and to
|
|
3
|
+
* A secret is bound to the resource it was obtained for and to everything that
|
|
4
|
+
* decides where it goes and whose it is.
|
|
4
5
|
*
|
|
5
|
-
* The session store keeps two strings beside the secret
|
|
6
|
-
*
|
|
7
|
-
* stored secret only when both equal what this destination's means give. The
|
|
8
|
-
* store keeps them as given; the broker canonicalises **both sides** before it
|
|
9
|
-
* compares, with the one function here (the broker's only, decided), so a store
|
|
10
|
-
* need not canonicalise and a legacy file's composed URI compares equal.
|
|
6
|
+
* The session store keeps two strings beside the secret, and the broker uses a
|
|
7
|
+
* stored secret only when both equal what this destination's means give:
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
* `client_id=<id>` for an issuer, none for a SAML ACS — parsed and
|
|
19
|
-
* re-encoded with `URLSearchParams`, so a value percent-encoded on one side
|
|
20
|
-
* and plain on the other compares equal;
|
|
21
|
-
* - no user info, no other parameter, no fragment.
|
|
9
|
+
* - **`issuedFor`**, the resource: `serviceUrl` with the SAP client, in the
|
|
10
|
+
* canonical form 4.x wrote (`resourceUri`). Both sides are canonicalised
|
|
11
|
+
* before they are compared, so a stored value written by any 4.x-era
|
|
12
|
+
* consumer compares equal.
|
|
13
|
+
* - **`issuedBy`**, a versioned record only the broker produces
|
|
14
|
+
* (`bindingRecord`):
|
|
22
15
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
16
|
+
* ```
|
|
17
|
+
* mcp-abap-adt-binding/2;<row>;<eleven address fields>;<trust>
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `row` is `authType/grantType` (or `provider/…` for the token API's
|
|
21
|
+
* consumer provider); each address field is the exact string the row hands
|
|
22
|
+
* its provider — `encodeURIComponent`-encoded, `""` when it hands none, never
|
|
23
|
+
* canonicalised — in the fixed order of `RECORD_FIELDS`; `trust` is the
|
|
24
|
+
* SHA-256 of the row's non-secret trust input (`trustDigest`), or `""`.
|
|
25
|
+
* The record is compared by exact equality and never parsed, split or
|
|
26
|
+
* matched; no regular expression touches it.
|
|
27
|
+
*
|
|
28
|
+
* No secret takes part in either string, nor a hash of one: a hash in a
|
|
29
|
+
* session file can be checked offline against guesses.
|
|
25
30
|
*/
|
|
26
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.RECORD_FIELDS = void 0;
|
|
33
|
+
exports.bindingRecord = bindingRecord;
|
|
34
|
+
exports.trustDigest = trustDigest;
|
|
27
35
|
exports.resourceUri = resourceUri;
|
|
28
|
-
exports.issuerUri = issuerUri;
|
|
29
|
-
exports.acsUri = acsUri;
|
|
30
36
|
exports.uaaBinding = uaaBinding;
|
|
31
37
|
exports.oidcBinding = oidcBinding;
|
|
32
38
|
exports.samlPureBinding = samlPureBinding;
|
|
39
|
+
exports.samlBearerBinding = samlBearerBinding;
|
|
40
|
+
exports.handedOverBinding = handedOverBinding;
|
|
33
41
|
exports.consumerBinding = consumerBinding;
|
|
34
42
|
exports.strategyBinding = strategyBinding;
|
|
35
|
-
exports.handedOverBinding = handedOverBinding;
|
|
36
43
|
exports.sameResource = sameResource;
|
|
37
|
-
exports.
|
|
44
|
+
exports.sameRecord = sameRecord;
|
|
45
|
+
exports.couldBeSeeded = couldBeSeeded;
|
|
38
46
|
exports.boundHere = boundHere;
|
|
47
|
+
const node_crypto_1 = require("node:crypto");
|
|
48
|
+
/** The record's version tag; a later format is `mcp-abap-adt-binding/3`. */
|
|
49
|
+
const RECORD_VERSION = 'mcp-abap-adt-binding/2';
|
|
50
|
+
/** What the trust input is hashed under, before its JSON. */
|
|
51
|
+
const TRUST_PREFIX = 'mcp-abap-adt-binding/2/trust\n';
|
|
52
|
+
/** The eleven address fields of the record, in their fixed order. */
|
|
53
|
+
exports.RECORD_FIELDS = [
|
|
54
|
+
'clientId',
|
|
55
|
+
'uaaUrl',
|
|
56
|
+
'oidcIssuerUrl',
|
|
57
|
+
'oidcTokenEndpoint',
|
|
58
|
+
'oidcAuthorizationEndpoint',
|
|
59
|
+
'oidcDeviceAuthorizationEndpoint',
|
|
60
|
+
'oidcAudience',
|
|
61
|
+
'samlIdpSsoUrl',
|
|
62
|
+
'samlAcsUrl',
|
|
63
|
+
'samlTokenUrl',
|
|
64
|
+
'certUrl',
|
|
65
|
+
];
|
|
66
|
+
function present(value) {
|
|
67
|
+
return typeof value === 'string' && value !== '';
|
|
68
|
+
}
|
|
69
|
+
const HIGH_SURROGATE_FIRST = 0xd800;
|
|
70
|
+
const LOW_SURROGATE_FIRST = 0xdc00;
|
|
71
|
+
const SURROGATE_LAST = 0xdfff;
|
|
72
|
+
/**
|
|
73
|
+
* `encodeURIComponent(value)`, total: a lone surrogate — on which the
|
|
74
|
+
* platform function throws — is written `%uXXXX`, which no
|
|
75
|
+
* `encodeURIComponent` output contains (its `%` is always followed by two hex
|
|
76
|
+
* digits), so two different strings never encode equal. Linear in the length.
|
|
77
|
+
*/
|
|
78
|
+
function encodeField(value) {
|
|
79
|
+
try {
|
|
80
|
+
return encodeURIComponent(value);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// A lone surrogate: encode unit by unit below.
|
|
84
|
+
}
|
|
85
|
+
let out = '';
|
|
86
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
87
|
+
const unit = value.charCodeAt(i);
|
|
88
|
+
if (unit >= HIGH_SURROGATE_FIRST && unit < LOW_SURROGATE_FIRST) {
|
|
89
|
+
const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
|
|
90
|
+
if (next >= LOW_SURROGATE_FIRST && next <= SURROGATE_LAST) {
|
|
91
|
+
out += encodeURIComponent(value.slice(i, i + 2));
|
|
92
|
+
i += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (unit >= HIGH_SURROGATE_FIRST && unit <= SURROGATE_LAST) {
|
|
97
|
+
out += `%u${unit.toString(16).toUpperCase()}`;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
out += encodeURIComponent(value.charAt(i));
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The `issuedBy` record: the version, the row, the eleven address fields in
|
|
106
|
+
* their fixed order — each the exact string, encoded, `""` when absent or
|
|
107
|
+
* `''` — and the trust digest. Every field is always present, and none holds
|
|
108
|
+
* `;`, so the string is unambiguous without being parsed.
|
|
109
|
+
*/
|
|
110
|
+
function bindingRecord(row, fields, trust) {
|
|
111
|
+
const parts = [RECORD_VERSION, row];
|
|
112
|
+
for (const name of exports.RECORD_FIELDS) {
|
|
113
|
+
const value = fields[name];
|
|
114
|
+
parts.push(present(value) ? encodeField(value) : '');
|
|
115
|
+
}
|
|
116
|
+
parts.push(trust);
|
|
117
|
+
return parts.join(';');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A value of the trust input as the means state it — a string, a number, a
|
|
121
|
+
* boolean, an array of strings — or `null` for an absent one. A row refuses a
|
|
122
|
+
* trust input of any other shape before it builds (`trustShaped`,
|
|
123
|
+
* `samlRefusal`, the certificate check in `getProvider`), so `null` never
|
|
124
|
+
* stands for a malformed value a built provider received; only `bindingOf`,
|
|
125
|
+
* whose means `getProvider` would refuse, can reach it.
|
|
126
|
+
*/
|
|
127
|
+
function trustValue(value) {
|
|
128
|
+
if (typeof value === 'string' || typeof value === 'boolean')
|
|
129
|
+
return value;
|
|
130
|
+
if (typeof value === 'number')
|
|
131
|
+
return Number.isFinite(value) ? value : null;
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
const copy = [];
|
|
134
|
+
for (const element of value) {
|
|
135
|
+
if (typeof element !== 'string')
|
|
136
|
+
return null;
|
|
137
|
+
copy.push(element);
|
|
138
|
+
}
|
|
139
|
+
return copy;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The trust digest: lower-case hex SHA-256 of the UTF-8 bytes of
|
|
145
|
+
* `"mcp-abap-adt-binding/2/trust\n"` and the JSON of the `[name, value]` pairs
|
|
146
|
+
* in the row's fixed order — no trimming, no canonicalisation. JSON of arrays
|
|
147
|
+
* and primitives has one output (lone surrogates escaped), so equal inputs
|
|
148
|
+
* give equal bytes.
|
|
149
|
+
*/
|
|
150
|
+
function trustDigest(pairs) {
|
|
151
|
+
const serialised = JSON.stringify(pairs.map(([name, value]) => [name, trustValue(value)]));
|
|
152
|
+
return (0, node_crypto_1.createHash)('sha256')
|
|
153
|
+
.update(`${TRUST_PREFIX}${serialised}`, 'utf8')
|
|
154
|
+
.digest('hex');
|
|
155
|
+
}
|
|
39
156
|
/** The default port a URL without one is taken to mean. */
|
|
40
157
|
const DEFAULT_PORTS = {
|
|
41
158
|
'https:': '443',
|
|
42
159
|
'http:': '80',
|
|
43
160
|
};
|
|
44
|
-
|
|
45
|
-
|
|
161
|
+
/** `value` without the slashes it ends with, in one backwards pass. */
|
|
162
|
+
function withoutTrailingSlashes(value) {
|
|
163
|
+
let end = value.length;
|
|
164
|
+
while (end > 0 && value.charCodeAt(end - 1) === 0x2f)
|
|
165
|
+
end -= 1;
|
|
166
|
+
return value.slice(0, end);
|
|
46
167
|
}
|
|
47
168
|
/**
|
|
48
|
-
* The canonical URI
|
|
49
|
-
*
|
|
169
|
+
* The canonical resource URI, as 4.x wrote it: scheme and host lower-cased
|
|
170
|
+
* (by `URL`, for http and https); the port explicit (`443` / `80` by
|
|
171
|
+
* default); the path without a trailing `/`, its case kept; one query
|
|
172
|
+
* parameter, `sap-client=<n>` — `sapClient` when given, else the one the URL
|
|
173
|
+
* carries — re-encoded with `URLSearchParams`; no user info, no other
|
|
174
|
+
* parameter, no fragment. A URL that does not parse, or has no host, has no
|
|
175
|
+
* canonical form: `undefined`, never guessed. For a stored value, call it
|
|
176
|
+
* without `sapClient`.
|
|
50
177
|
*/
|
|
51
|
-
function
|
|
52
|
-
if (!present(
|
|
178
|
+
function resourceUri(serviceUrl, sapClient) {
|
|
179
|
+
if (!present(serviceUrl))
|
|
53
180
|
return undefined;
|
|
54
181
|
let url;
|
|
55
182
|
try {
|
|
56
|
-
url = new URL(
|
|
183
|
+
url = new URL(serviceUrl);
|
|
57
184
|
}
|
|
58
185
|
catch {
|
|
59
186
|
return undefined;
|
|
60
187
|
}
|
|
61
|
-
// `URL` lower-cases the scheme, and the host of an http(s) URL. Another
|
|
62
|
-
// scheme's host is kept as written: no form beyond http(s) is defined yet.
|
|
63
188
|
const host = url.hostname;
|
|
64
189
|
if (host === '')
|
|
65
190
|
return undefined;
|
|
66
191
|
const scheme = url.protocol;
|
|
67
192
|
const port = url.port || DEFAULT_PORTS[scheme] || '';
|
|
68
|
-
const path = url.pathname
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
193
|
+
const path = withoutTrailingSlashes(url.pathname);
|
|
194
|
+
const client = present(sapClient)
|
|
195
|
+
? sapClient
|
|
196
|
+
: url.searchParams.get('sap-client');
|
|
197
|
+
const query = present(client)
|
|
198
|
+
? `?${new URLSearchParams([['sap-client', client]]).toString()}`
|
|
199
|
+
: '';
|
|
76
200
|
return `${scheme}//${host}${port ? `:${port}` : ''}${path}${query}`;
|
|
77
201
|
}
|
|
78
|
-
/**
|
|
79
|
-
* The resource: `serviceUrl` with the SAP client as `sap-client` — the
|
|
80
|
-
* destination's `sapClient` when it states one, over a `sap-client` already in
|
|
81
|
-
* the URL. For a stored value, call it without `sapClient`.
|
|
82
|
-
*/
|
|
83
|
-
function resourceUri(serviceUrl, sapClient) {
|
|
84
|
-
return canonical(serviceUrl, 'sap-client', sapClient);
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* The issuer and its client: the authorization server's base URL with
|
|
88
|
-
* `client_id`. For a stored value, call it without `clientId`.
|
|
89
|
-
*/
|
|
90
|
-
function issuerUri(issuerUrl, clientId) {
|
|
91
|
-
return canonical(issuerUrl, 'client_id', clientId);
|
|
92
|
-
}
|
|
93
|
-
/** The ACS of the system that set SAML session cookies: origin and path. */
|
|
94
|
-
function acsUri(acsUrl) {
|
|
95
|
-
return canonical(acsUrl, undefined);
|
|
96
|
-
}
|
|
97
202
|
/** The resource the means name: `serviceUrl` with `sapClient`. */
|
|
98
203
|
function resourceOf(means) {
|
|
99
204
|
return resourceUri(means.serviceUrl, present(means.sapClient) ? means.sapClient : undefined);
|
|
100
205
|
}
|
|
206
|
+
/** The certificate client's public certificate, for the trust input. */
|
|
207
|
+
function certificateOf(certificate) {
|
|
208
|
+
return certificate ? certificate.certificate : null;
|
|
209
|
+
}
|
|
210
|
+
/** `certUrl` is stated whenever the build read a certificate client. */
|
|
211
|
+
function certificateStated(certificate) {
|
|
212
|
+
return !certificate || present(certificate.certUrl);
|
|
213
|
+
}
|
|
101
214
|
/**
|
|
102
|
-
* The UAA grants (`authorization_code`, `client_credentials`, `passcode`)
|
|
103
|
-
*
|
|
215
|
+
* The UAA grants (`authorization_code`, `client_credentials`, `passcode`):
|
|
216
|
+
* the client id and `uaaUrl` as the provider receives them, and `certUrl`
|
|
217
|
+
* when the build read the certificate client; trust is the client
|
|
218
|
+
* certificate. Fully stated with the client, `uaaUrl` and — when read —
|
|
219
|
+
* `certUrl`.
|
|
104
220
|
*/
|
|
105
|
-
function uaaBinding(means, client) {
|
|
106
|
-
const stated = present(client?.uaaUrl) && present(client?.uaaClientId);
|
|
221
|
+
function uaaBinding(grant, means, client, certificate) {
|
|
107
222
|
return {
|
|
108
223
|
issuedFor: resourceOf(means),
|
|
109
|
-
issuedBy:
|
|
110
|
-
|
|
111
|
-
:
|
|
112
|
-
|
|
113
|
-
|
|
224
|
+
issuedBy: bindingRecord(`jwt/${grant}`, {
|
|
225
|
+
clientId: client?.uaaClientId,
|
|
226
|
+
uaaUrl: client?.uaaUrl,
|
|
227
|
+
certUrl: certificate?.certUrl,
|
|
228
|
+
}, trustDigest([['clientCertificate', certificateOf(certificate)]])),
|
|
229
|
+
fullyStated: present(client?.uaaClientId) &&
|
|
230
|
+
present(client?.uaaUrl) &&
|
|
231
|
+
certificateStated(certificate),
|
|
114
232
|
};
|
|
115
233
|
}
|
|
116
234
|
/**
|
|
117
|
-
* The OIDC grants: the
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
235
|
+
* The OIDC grants: the client id, `oidcIssuerUrl`, `oidcTokenEndpoint`, and
|
|
236
|
+
* the grant's own endpoint (`oidcAuthorizationEndpoint`,
|
|
237
|
+
* `oidcDeviceAuthorizationEndpoint`) or audience (`oidcAudience`), and
|
|
238
|
+
* `certUrl` when read; trust is the scopes, the `password` grant's
|
|
239
|
+
* `username`, the token-exchange token types, and the client certificate.
|
|
240
|
+
*
|
|
241
|
+
* Fully stated with the client, each endpoint the grant sends to — explicit,
|
|
242
|
+
* or the issuer it is discovered from — and `certUrl` when read;
|
|
243
|
+
* `token_exchange` never: its subject token, a secret, decides whose
|
|
244
|
+
* credential it obtains.
|
|
121
245
|
*/
|
|
122
|
-
function oidcBinding(means, client) {
|
|
123
|
-
const issuer = present(means.oidcIssuerUrl)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
246
|
+
function oidcBinding(grant, means, client, certificate) {
|
|
247
|
+
const issuer = present(means.oidcIssuerUrl);
|
|
248
|
+
const reaches = (endpoint) => issuer || present(endpoint);
|
|
249
|
+
const trust = [['oidcScopes', means.oidcScopes]];
|
|
250
|
+
if (grant === 'password')
|
|
251
|
+
trust.push(['username', means.username]);
|
|
252
|
+
if (grant === 'token_exchange') {
|
|
253
|
+
trust.push(['oidcSubjectTokenType', means.oidcSubjectTokenType], ['oidcActorTokenType', means.oidcActorTokenType]);
|
|
254
|
+
}
|
|
255
|
+
trust.push(['clientCertificate', certificateOf(certificate)]);
|
|
127
256
|
return {
|
|
128
257
|
issuedFor: resourceOf(means),
|
|
129
|
-
issuedBy:
|
|
130
|
-
|
|
131
|
-
|
|
258
|
+
issuedBy: bindingRecord(`jwt/${grant}`, {
|
|
259
|
+
clientId: client?.uaaClientId,
|
|
260
|
+
oidcIssuerUrl: means.oidcIssuerUrl,
|
|
261
|
+
oidcTokenEndpoint: means.oidcTokenEndpoint,
|
|
262
|
+
oidcAuthorizationEndpoint: grant === 'oidc_authorization_code'
|
|
263
|
+
? means.oidcAuthorizationEndpoint
|
|
264
|
+
: undefined,
|
|
265
|
+
oidcDeviceAuthorizationEndpoint: grant === 'device_code'
|
|
266
|
+
? means.oidcDeviceAuthorizationEndpoint
|
|
267
|
+
: undefined,
|
|
268
|
+
oidcAudience: grant === 'token_exchange' ? means.oidcAudience : undefined,
|
|
269
|
+
certUrl: certificate?.certUrl,
|
|
270
|
+
}, trustDigest(trust)),
|
|
271
|
+
fullyStated: grant !== 'token_exchange' &&
|
|
272
|
+
present(client?.uaaClientId) &&
|
|
273
|
+
reaches(means.oidcTokenEndpoint) &&
|
|
274
|
+
(grant !== 'oidc_authorization_code' ||
|
|
275
|
+
reaches(means.oidcAuthorizationEndpoint)) &&
|
|
276
|
+
(grant !== 'device_code' ||
|
|
277
|
+
reaches(means.oidcDeviceAuthorizationEndpoint)) &&
|
|
278
|
+
certificateStated(certificate),
|
|
132
279
|
};
|
|
133
280
|
}
|
|
281
|
+
/** The SAML trust input both SAML grants share, in order. */
|
|
282
|
+
function samlTrust(means) {
|
|
283
|
+
return [
|
|
284
|
+
['samlIdpCertificates', means.samlIdpCertificates],
|
|
285
|
+
['samlIdpEntityId', means.samlIdpEntityId],
|
|
286
|
+
['samlSpEntityId', means.samlSpEntityId],
|
|
287
|
+
['samlClockSkewMs', means.samlClockSkewMs],
|
|
288
|
+
['samlIdpInitiated', means.samlIdpInitiated],
|
|
289
|
+
];
|
|
290
|
+
}
|
|
134
291
|
/**
|
|
135
|
-
* `saml2_pure`: the
|
|
136
|
-
* cookies
|
|
292
|
+
* `saml2_pure`: the IdP's SSO URL and the ACS of the system that sets the
|
|
293
|
+
* cookies; trust is the SAML trust. Fully stated with both.
|
|
137
294
|
*/
|
|
138
295
|
function samlPureBinding(means) {
|
|
139
296
|
return {
|
|
140
297
|
issuedFor: resourceOf(means),
|
|
141
|
-
issuedBy:
|
|
142
|
-
|
|
143
|
-
issuerStated: present(means.samlAcsUrl),
|
|
298
|
+
issuedBy: bindingRecord('saml/saml2_pure', { samlIdpSsoUrl: means.samlIdpSsoUrl, samlAcsUrl: means.samlAcsUrl }, trustDigest(samlTrust(means))),
|
|
299
|
+
fullyStated: present(means.samlIdpSsoUrl) && present(means.samlAcsUrl),
|
|
144
300
|
};
|
|
145
301
|
}
|
|
146
302
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* then absent, and `getProvider` never seeds from that secret.
|
|
303
|
+
* `saml2_bearer`: the client id, `uaaUrl`, the IdP's SSO URL, the ACS,
|
|
304
|
+
* `samlTokenUrl`, and `certUrl` when read; trust is the SAML trust and the
|
|
305
|
+
* client certificate. Fully stated with the client, the IdP, the token
|
|
306
|
+
* endpoint (`samlTokenUrl`, or `uaaUrl`) and — when read — `certUrl`.
|
|
152
307
|
*/
|
|
153
|
-
function
|
|
154
|
-
return
|
|
308
|
+
function samlBearerBinding(means, client, certificate) {
|
|
309
|
+
return {
|
|
310
|
+
issuedFor: resourceOf(means),
|
|
311
|
+
issuedBy: bindingRecord('saml/saml2_bearer', {
|
|
312
|
+
clientId: client?.uaaClientId,
|
|
313
|
+
uaaUrl: client?.uaaUrl,
|
|
314
|
+
samlIdpSsoUrl: means.samlIdpSsoUrl,
|
|
315
|
+
samlAcsUrl: means.samlAcsUrl,
|
|
316
|
+
samlTokenUrl: means.samlTokenUrl,
|
|
317
|
+
certUrl: certificate?.certUrl,
|
|
318
|
+
}, trustDigest([
|
|
319
|
+
...samlTrust(means),
|
|
320
|
+
['clientCertificate', certificateOf(certificate)],
|
|
321
|
+
])),
|
|
322
|
+
fullyStated: present(client?.uaaClientId) &&
|
|
323
|
+
present(means.samlIdpSsoUrl) &&
|
|
324
|
+
(present(means.samlTokenUrl) || present(client?.uaaUrl)) &&
|
|
325
|
+
certificateStated(certificate),
|
|
326
|
+
};
|
|
155
327
|
}
|
|
156
328
|
/**
|
|
157
|
-
* A
|
|
158
|
-
*
|
|
159
|
-
*
|
|
329
|
+
* A `none` row: the resource, and the record — for `jwt` the client id,
|
|
330
|
+
* `uaaUrl` and `oidcIssuerUrl` as the means state them, for `saml` the ACS;
|
|
331
|
+
* no trust. A handed-over credential is compared by exact equality of the
|
|
332
|
+
* whole record; it is never seeded into a renewal, so it is not
|
|
333
|
+
* "fully stated".
|
|
160
334
|
*/
|
|
161
|
-
function
|
|
162
|
-
|
|
335
|
+
function handedOverBinding(authType, means, client) {
|
|
336
|
+
const fields = authType === 'saml'
|
|
337
|
+
? { samlAcsUrl: means.samlAcsUrl }
|
|
338
|
+
: {
|
|
339
|
+
clientId: client?.uaaClientId,
|
|
340
|
+
uaaUrl: client?.uaaUrl,
|
|
341
|
+
oidcIssuerUrl: means.oidcIssuerUrl,
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
issuedFor: resourceOf(means),
|
|
345
|
+
issuedBy: bindingRecord(`${authType}/none`, fields, ''),
|
|
346
|
+
fullyStated: false,
|
|
347
|
+
};
|
|
163
348
|
}
|
|
164
349
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* and `
|
|
350
|
+
* The token API with a consumer's provider: the `serviceUrl` it resolved,
|
|
351
|
+
* with the SAP client, and the record `provider/<row>` — for a factory the
|
|
352
|
+
* client id and `uaaUrl` of the client it was handed, for an instance (handed
|
|
353
|
+
* no client) none; no trust. Never fully stated: the broker cannot know what
|
|
354
|
+
* the factory composes, and an instance is handed no client.
|
|
168
355
|
*/
|
|
169
|
-
function
|
|
170
|
-
const issuedFor = resourceOf(means);
|
|
171
|
-
if (authType === 'saml') {
|
|
172
|
-
return {
|
|
173
|
-
issuedFor,
|
|
174
|
-
issuedBy: acsUri(means.samlAcsUrl),
|
|
175
|
-
issuerKind: 'acs',
|
|
176
|
-
issuerStated: present(means.samlAcsUrl),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
const clientId = present(client?.uaaClientId)
|
|
180
|
-
? client.uaaClientId
|
|
181
|
-
: undefined;
|
|
182
|
-
if (present(means.oidcIssuerUrl)) {
|
|
183
|
-
return {
|
|
184
|
-
issuedFor,
|
|
185
|
-
issuedBy: issuerUri(means.oidcIssuerUrl, clientId),
|
|
186
|
-
issuerKind: 'issuer',
|
|
187
|
-
issuerStated: true,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
const stated = present(client?.uaaUrl) && clientId !== undefined;
|
|
356
|
+
function consumerBinding(row, serviceUrl, sapClient, client) {
|
|
191
357
|
return {
|
|
192
|
-
issuedFor,
|
|
193
|
-
issuedBy:
|
|
194
|
-
|
|
195
|
-
issuerStated: stated,
|
|
358
|
+
issuedFor: resourceUri(serviceUrl, sapClient),
|
|
359
|
+
issuedBy: bindingRecord(row, { clientId: client?.uaaClientId, uaaUrl: client?.uaaUrl }, ''),
|
|
360
|
+
fullyStated: false,
|
|
196
361
|
};
|
|
197
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* A binding on the `clientAuthentication` strategy path: a resource that
|
|
365
|
+
* neither the means nor the stored session state matches
|
|
366
|
+
* (`Binding.unstatedResourceMatches`).
|
|
367
|
+
*/
|
|
368
|
+
function strategyBinding(binding) {
|
|
369
|
+
return { ...binding, unstatedResourceMatches: true };
|
|
370
|
+
}
|
|
198
371
|
/**
|
|
199
372
|
* The stored `issuedFor`, canonicalised, equals the destination's — or, with
|
|
200
373
|
* `unstatedResourceMatches`, neither states one.
|
|
@@ -205,19 +378,29 @@ function sameResource(stored, binding) {
|
|
|
205
378
|
}
|
|
206
379
|
return resourceUri(stored?.issuedFor) === binding.issuedFor;
|
|
207
380
|
}
|
|
208
|
-
/** The stored `issuedBy
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
381
|
+
/** The stored `issuedBy` is exactly this binding's record. */
|
|
382
|
+
function sameRecord(stored, binding) {
|
|
383
|
+
return stored?.issuedBy === binding.issuedBy;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Whether any stored secret could ever seed this binding's provider: the
|
|
387
|
+
* binding is fully stated and its resource can match — stated, or matching
|
|
388
|
+
* when neither side states one (`unstatedResourceMatches`). The condition
|
|
389
|
+
* `boundHere` needs beside the stored values.
|
|
390
|
+
*/
|
|
391
|
+
function couldBeSeeded(binding) {
|
|
392
|
+
return (binding.fullyStated &&
|
|
393
|
+
(binding.issuedFor !== undefined ||
|
|
394
|
+
binding.unstatedResourceMatches === true));
|
|
214
395
|
}
|
|
215
396
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
397
|
+
* A stored secret may seed this binding's provider — or have its refresh
|
|
398
|
+
* token carried — only when the binding is fully stated, the resource
|
|
399
|
+
* matches, and the stored `issuedBy` is exactly the record. A session with no
|
|
400
|
+
* `issuedBy`, or a 4.x one, never matches.
|
|
220
401
|
*/
|
|
221
402
|
function boundHere(stored, binding) {
|
|
222
|
-
return
|
|
403
|
+
return (binding.fullyStated &&
|
|
404
|
+
sameResource(stored, binding) &&
|
|
405
|
+
sameRecord(stored, binding));
|
|
223
406
|
}
|
package/dist/bindingOf.d.ts
CHANGED
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { DestinationGrant, IConnectionConfig } from '@mcp-abap-adt/interfaces-auth-broker';
|
|
8
8
|
import type { IAuthorizationConfig } from '@mcp-abap-adt/interfaces-auth-sap';
|
|
9
|
-
import type
|
|
10
|
-
import { type Binding } from './binding';
|
|
9
|
+
import { type Binding, type BoundCertificate, type BoundClient } from './binding';
|
|
11
10
|
/**
|
|
12
|
-
* The binding of a `jwt` or `saml` destination with the grant it states
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
15
|
-
*
|
|
11
|
+
* The binding of a `jwt` or `saml` destination with the grant it states — a
|
|
12
|
+
* pair of the closed list, checked before (`statedGrant`): the UAA grants,
|
|
13
|
+
* the OIDC grants, `saml2_pure`, `saml2_bearer` and `none`, each with the
|
|
14
|
+
* fields of its row. `certificate` is the certificate client the build
|
|
15
|
+
* read for the `clientAuthentication` strategy, or `null` when it read none.
|
|
16
16
|
*/
|
|
17
|
-
export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client: BoundClient | null): Binding;
|
|
17
|
+
export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client: BoundClient | null, certificate?: BoundCertificate | null): Binding;
|
|
18
|
+
/**
|
|
19
|
+
* The row of the token API's consumer provider: `provider/<authType>/<grant>`
|
|
20
|
+
* when the means state a pair of the closed list, else `provider/-`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function consumerRow(means: IConnectionConfig | null): string;
|
|
18
23
|
/** The two strings a session store keeps beside a secret; absent where the means cannot say. */
|
|
19
24
|
export interface SecretBinding {
|
|
20
25
|
issuedFor?: string;
|
|
@@ -22,16 +27,29 @@ export interface SecretBinding {
|
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* The binding the broker writes beside a secret for a destination with these
|
|
25
|
-
* means and this client
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `getProvider`
|
|
30
|
+
* means and this client: `issuedFor` the canonical resource, `issuedBy` the
|
|
31
|
+
* version-2 record of the row the means state, built from `means` and
|
|
32
|
+
* `client` alone.
|
|
33
|
+
*
|
|
34
|
+
* It is exactly what `getProvider` compares for a `none` row — its use: a
|
|
35
|
+
* consumer that hands over a credential the broker cannot obtain (a token or
|
|
36
|
+
* cookies it was given) writes it with this binding, and `getProvider`
|
|
37
|
+
* presents it; written with any other — a 4.x `bindingOf`'s included — it
|
|
38
|
+
* refuses. It is also exactly what `getProvider` writes for a token row built
|
|
39
|
+
* without a `clientAuthentication` strategy.
|
|
40
|
+
*
|
|
41
|
+
* It is **not** what a token row writes beside a `clientAuthentication`
|
|
42
|
+
* strategy: there the row's record also holds what the build read of the
|
|
43
|
+
* certificate client — its `certUrl`, and its certificate in the trust
|
|
44
|
+
* digest — and its client identity may be the certificate client's, none of
|
|
45
|
+
* which `bindingOf` is given. Nor is it the token API's record for a
|
|
46
|
+
* consumer's provider (`provider/…`).
|
|
30
47
|
*
|
|
31
48
|
* `means` are the key store's (`authType`, `grantType`, `serviceUrl`,
|
|
32
|
-
* `sapClient`, the
|
|
33
|
-
* destination has one. A destination that states no
|
|
34
|
-
*
|
|
49
|
+
* `sapClient`, the addresses and trust the row reads); `client` is the key
|
|
50
|
+
* store's client, when the destination has one. A destination that states no
|
|
51
|
+
* `jwt` / `saml` type, no grant, or a pair outside the closed list binds
|
|
52
|
+
* nothing: `{}`. Never logs, never throws.
|
|
35
53
|
*/
|
|
36
54
|
export declare function bindingOf(means: IConnectionConfig, client?: IAuthorizationConfig | null): SecretBinding;
|
|
37
55
|
//# sourceMappingURL=bindingOf.d.ts.map
|
package/dist/bindingOf.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindingOf.d.ts","sourceRoot":"","sources":["../src/bindingOf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"bindingOf.d.ts","sourceRoot":"","sources":["../src/bindingOf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAMjB,MAAM,WAAW,CAAC;AAGnB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,KAAK,GAAG,MAAM,EACxB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,EAC1B,WAAW,GAAE,gBAAgB,GAAG,IAAW,GAC1C,OAAO,CAUT;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,GAAG,MAAM,CAMnE;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,GAAE,oBAAoB,GAAG,IAAW,GACzC,aAAa,CAcf"}
|