@mcp-abap-adt/auth-broker 4.0.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 +273 -1
- package/README.md +1028 -908
- package/dist/AuthBroker.d.ts +353 -99
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +806 -258
- 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 +137 -64
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +330 -131
- package/dist/bindingOf.d.ts +33 -14
- 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 +124 -0
- package/dist/clientAuthentication.d.ts.map +1 -0
- package/dist/clientAuthentication.js +218 -0
- 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 +174 -53
- package/dist/destinations.d.ts.map +1 -1
- package/dist/destinations.js +460 -150
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -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 +9 -7
package/dist/binding.js
CHANGED
|
@@ -1,207 +1,406 @@
|
|
|
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;
|
|
33
|
-
exports.
|
|
39
|
+
exports.samlBearerBinding = samlBearerBinding;
|
|
34
40
|
exports.handedOverBinding = handedOverBinding;
|
|
41
|
+
exports.consumerBinding = consumerBinding;
|
|
42
|
+
exports.strategyBinding = strategyBinding;
|
|
35
43
|
exports.sameResource = sameResource;
|
|
36
|
-
exports.
|
|
44
|
+
exports.sameRecord = sameRecord;
|
|
45
|
+
exports.couldBeSeeded = couldBeSeeded;
|
|
37
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
|
+
}
|
|
38
156
|
/** The default port a URL without one is taken to mean. */
|
|
39
157
|
const DEFAULT_PORTS = {
|
|
40
158
|
'https:': '443',
|
|
41
159
|
'http:': '80',
|
|
42
160
|
};
|
|
43
|
-
|
|
44
|
-
|
|
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);
|
|
45
167
|
}
|
|
46
168
|
/**
|
|
47
|
-
* The canonical URI
|
|
48
|
-
*
|
|
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`.
|
|
49
177
|
*/
|
|
50
|
-
function
|
|
51
|
-
if (!present(
|
|
178
|
+
function resourceUri(serviceUrl, sapClient) {
|
|
179
|
+
if (!present(serviceUrl))
|
|
52
180
|
return undefined;
|
|
53
181
|
let url;
|
|
54
182
|
try {
|
|
55
|
-
url = new URL(
|
|
183
|
+
url = new URL(serviceUrl);
|
|
56
184
|
}
|
|
57
185
|
catch {
|
|
58
186
|
return undefined;
|
|
59
187
|
}
|
|
60
|
-
// `URL` lower-cases the scheme, and the host of an http(s) URL. Another
|
|
61
|
-
// scheme's host is kept as written: no form beyond http(s) is defined yet.
|
|
62
188
|
const host = url.hostname;
|
|
63
189
|
if (host === '')
|
|
64
190
|
return undefined;
|
|
65
191
|
const scheme = url.protocol;
|
|
66
192
|
const port = url.port || DEFAULT_PORTS[scheme] || '';
|
|
67
|
-
const path = url.pathname
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
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
|
+
: '';
|
|
75
200
|
return `${scheme}//${host}${port ? `:${port}` : ''}${path}${query}`;
|
|
76
201
|
}
|
|
77
|
-
/**
|
|
78
|
-
* The resource: `serviceUrl` with the SAP client as `sap-client` — the
|
|
79
|
-
* destination's `sapClient` when it states one, over a `sap-client` already in
|
|
80
|
-
* the URL. For a stored value, call it without `sapClient`.
|
|
81
|
-
*/
|
|
82
|
-
function resourceUri(serviceUrl, sapClient) {
|
|
83
|
-
return canonical(serviceUrl, 'sap-client', sapClient);
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* The issuer and its client: the authorization server's base URL with
|
|
87
|
-
* `client_id`. For a stored value, call it without `clientId`.
|
|
88
|
-
*/
|
|
89
|
-
function issuerUri(issuerUrl, clientId) {
|
|
90
|
-
return canonical(issuerUrl, 'client_id', clientId);
|
|
91
|
-
}
|
|
92
|
-
/** The ACS of the system that set SAML session cookies: origin and path. */
|
|
93
|
-
function acsUri(acsUrl) {
|
|
94
|
-
return canonical(acsUrl, undefined);
|
|
95
|
-
}
|
|
96
202
|
/** The resource the means name: `serviceUrl` with `sapClient`. */
|
|
97
203
|
function resourceOf(means) {
|
|
98
204
|
return resourceUri(means.serviceUrl, present(means.sapClient) ? means.sapClient : undefined);
|
|
99
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
|
+
}
|
|
100
214
|
/**
|
|
101
|
-
* The UAA grants (`authorization_code`, `client_credentials`, `passcode`)
|
|
102
|
-
*
|
|
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`.
|
|
103
220
|
*/
|
|
104
|
-
function uaaBinding(means, client) {
|
|
105
|
-
const stated = present(client?.uaaUrl) && present(client?.uaaClientId);
|
|
221
|
+
function uaaBinding(grant, means, client, certificate) {
|
|
106
222
|
return {
|
|
107
223
|
issuedFor: resourceOf(means),
|
|
108
|
-
issuedBy:
|
|
109
|
-
|
|
110
|
-
:
|
|
111
|
-
|
|
112
|
-
|
|
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),
|
|
113
232
|
};
|
|
114
233
|
}
|
|
115
234
|
/**
|
|
116
|
-
* The OIDC grants: the
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
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.
|
|
120
245
|
*/
|
|
121
|
-
function oidcBinding(means, client) {
|
|
122
|
-
const issuer = present(means.oidcIssuerUrl)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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)]);
|
|
126
256
|
return {
|
|
127
257
|
issuedFor: resourceOf(means),
|
|
128
|
-
issuedBy:
|
|
129
|
-
|
|
130
|
-
|
|
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),
|
|
131
279
|
};
|
|
132
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
|
+
}
|
|
133
291
|
/**
|
|
134
|
-
* `saml2_pure`: the
|
|
135
|
-
* 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.
|
|
136
294
|
*/
|
|
137
295
|
function samlPureBinding(means) {
|
|
138
296
|
return {
|
|
139
297
|
issuedFor: resourceOf(means),
|
|
140
|
-
issuedBy:
|
|
141
|
-
|
|
142
|
-
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),
|
|
143
300
|
};
|
|
144
301
|
}
|
|
145
302
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* 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`.
|
|
151
307
|
*/
|
|
152
|
-
function
|
|
153
|
-
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
|
+
};
|
|
154
327
|
}
|
|
155
328
|
/**
|
|
156
|
-
* A `none` row: the resource
|
|
157
|
-
*
|
|
158
|
-
*
|
|
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".
|
|
159
334
|
*/
|
|
160
335
|
function handedOverBinding(authType, means, client) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
issuerStated: present(means.samlAcsUrl),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
const clientId = present(client?.uaaClientId)
|
|
171
|
-
? client.uaaClientId
|
|
172
|
-
: undefined;
|
|
173
|
-
if (present(means.oidcIssuerUrl)) {
|
|
174
|
-
return {
|
|
175
|
-
issuedFor,
|
|
176
|
-
issuedBy: issuerUri(means.oidcIssuerUrl, clientId),
|
|
177
|
-
issuerKind: 'issuer',
|
|
178
|
-
issuerStated: true,
|
|
336
|
+
const fields = authType === 'saml'
|
|
337
|
+
? { samlAcsUrl: means.samlAcsUrl }
|
|
338
|
+
: {
|
|
339
|
+
clientId: client?.uaaClientId,
|
|
340
|
+
uaaUrl: client?.uaaUrl,
|
|
341
|
+
oidcIssuerUrl: means.oidcIssuerUrl,
|
|
179
342
|
};
|
|
180
|
-
}
|
|
181
|
-
const stated = present(client?.uaaUrl) && clientId !== undefined;
|
|
182
343
|
return {
|
|
183
|
-
issuedFor,
|
|
184
|
-
issuedBy:
|
|
185
|
-
|
|
186
|
-
issuerStated: stated,
|
|
344
|
+
issuedFor: resourceOf(means),
|
|
345
|
+
issuedBy: bindingRecord(`${authType}/none`, fields, ''),
|
|
346
|
+
fullyStated: false,
|
|
187
347
|
};
|
|
188
348
|
}
|
|
189
|
-
/**
|
|
349
|
+
/**
|
|
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.
|
|
355
|
+
*/
|
|
356
|
+
function consumerBinding(row, serviceUrl, sapClient, client) {
|
|
357
|
+
return {
|
|
358
|
+
issuedFor: resourceUri(serviceUrl, sapClient),
|
|
359
|
+
issuedBy: bindingRecord(row, { clientId: client?.uaaClientId, uaaUrl: client?.uaaUrl }, ''),
|
|
360
|
+
fullyStated: false,
|
|
361
|
+
};
|
|
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
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* The stored `issuedFor`, canonicalised, equals the destination's — or, with
|
|
373
|
+
* `unstatedResourceMatches`, neither states one.
|
|
374
|
+
*/
|
|
190
375
|
function sameResource(stored, binding) {
|
|
191
|
-
|
|
192
|
-
|
|
376
|
+
if (binding.issuedFor === undefined) {
|
|
377
|
+
return (binding.unstatedResourceMatches === true && !present(stored?.issuedFor));
|
|
378
|
+
}
|
|
379
|
+
return resourceUri(stored?.issuedFor) === binding.issuedFor;
|
|
380
|
+
}
|
|
381
|
+
/** The stored `issuedBy` is exactly this binding's record. */
|
|
382
|
+
function sameRecord(stored, binding) {
|
|
383
|
+
return stored?.issuedBy === binding.issuedBy;
|
|
193
384
|
}
|
|
194
|
-
/**
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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));
|
|
200
395
|
}
|
|
201
396
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
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.
|
|
204
401
|
*/
|
|
205
402
|
function boundHere(stored, binding) {
|
|
206
|
-
return
|
|
403
|
+
return (binding.fullyStated &&
|
|
404
|
+
sameResource(stored, binding) &&
|
|
405
|
+
sameRecord(stored, binding));
|
|
207
406
|
}
|
package/dist/bindingOf.d.ts
CHANGED
|
@@ -6,14 +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 Binding } from './binding';
|
|
9
|
+
import { type Binding, type BoundCertificate, type BoundClient } from './binding';
|
|
10
10
|
/**
|
|
11
|
-
* The binding of a `jwt` or `saml` destination with the grant it states
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
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.
|
|
15
16
|
*/
|
|
16
|
-
export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client:
|
|
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;
|
|
17
23
|
/** The two strings a session store keeps beside a secret; absent where the means cannot say. */
|
|
18
24
|
export interface SecretBinding {
|
|
19
25
|
issuedFor?: string;
|
|
@@ -21,16 +27,29 @@ export interface SecretBinding {
|
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* The binding the broker writes beside a secret for a destination with these
|
|
24
|
-
* means and this client
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `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/…`).
|
|
29
47
|
*
|
|
30
48
|
* `means` are the key store's (`authType`, `grantType`, `serviceUrl`,
|
|
31
|
-
* `sapClient`, the
|
|
32
|
-
* destination has one. A destination that states no
|
|
33
|
-
*
|
|
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.
|
|
34
53
|
*/
|
|
35
54
|
export declare function bindingOf(means: IConnectionConfig, client?: IAuthorizationConfig | null): SecretBinding;
|
|
36
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,EACL,KAAK,OAAO,
|
|
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"}
|