@mcp-abap-adt/auth-broker 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,8 +19,9 @@
19
19
  * broker does not repeat or override any of them.
20
20
  */
21
21
  import type { IDeviceCodePresenter, OidcCallbackResult } from '@mcp-abap-adt/auth-providers';
22
- import type { IAssertionReplayStore, IAuthorizationStrategy, IAuthProvider, IRefreshableTokenProvider, ITokenRefresher } from '@mcp-abap-adt/interfaces-auth';
22
+ import type { IAssertionReplayStore, IAuthorizationStrategy, IAuthProvider, IClientAuthentication, IRefreshableTokenProvider, ITokenRefresher } from '@mcp-abap-adt/interfaces-auth';
23
23
  import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
24
+ import { type ClientAuthenticationStrategy } from './clientAuthentication';
24
25
  import type { IAuthorizationConfig, IConnectionConfig, IServiceKeyStore, ISessionStore } from './stores/interfaces';
25
26
  /**
26
27
  * Builds the provider for one destination, from what the stores hold for it.
@@ -31,10 +32,39 @@ import type { IAuthorizationConfig, IConnectionConfig, IServiceKeyStore, ISessio
31
32
  * - `connConfig`: the session's connection config, with `serviceUrl` resolved
32
33
  * and the last token the session stored, so the provider can reuse it while
33
34
  * it is valid.
35
+ * - `client` (optional): passed only beside a `clientAuthentication` strategy,
36
+ * for a destination whose grant authenticates a client — the strategy's
37
+ * answer, the client identity and the bound refresh token; never PEM. See
38
+ * `TokenProviderClient`. Without a strategy the factory gets three
39
+ * arguments, as 4.0.0.
34
40
  *
35
41
  * Called once per destination; the broker keeps the provider it returns.
36
42
  */
37
- export type TokenProviderFactory = (destination: string, authConfig: IAuthorizationConfig | null, connConfig: IConnectionConfig) => IRefreshableTokenProvider;
43
+ export type TokenProviderFactory = (destination: string, authConfig: IAuthorizationConfig | null, connConfig: IConnectionConfig, client?: TokenProviderClient) => IRefreshableTokenProvider;
44
+ /**
45
+ * The token API factory's fourth argument — present only when the broker was
46
+ * given a `clientAuthentication` strategy and the destination states a grant
47
+ * whose client authenticates; without one the factory is called with three
48
+ * arguments, as 4.0.0. It never carries a certificate, a key or a secret.
49
+ */
50
+ export interface TokenProviderClient {
51
+ /** The strategy's answer, resolved before the factory is called. */
52
+ readonly clientAuthentication?: IClientAuthentication;
53
+ /**
54
+ * The client's identity — the secret client's `uaaUrl` when the stores hold
55
+ * one, else the certificate client's; absent when there is neither.
56
+ */
57
+ readonly uaaUrl?: string;
58
+ /** The client id beside `uaaUrl`, from the same client. */
59
+ readonly clientId?: string;
60
+ /**
61
+ * The refresh token the session stored, only when the session is bound to
62
+ * this resource and this client identity — as `authConfig` carries it on
63
+ * the strategy path for a secret client, so a certificate client, whose
64
+ * `authConfig` is `null`, gets it too.
65
+ */
66
+ readonly refreshToken?: string;
67
+ }
38
68
  /** The grants whose provider takes an `IAuthorizationStrategy<string>`. */
39
69
  export type StrategyGrant = 'authorization_code' | 'passcode' | 'saml2_pure' | 'saml2_bearer';
40
70
  /**
@@ -71,6 +101,26 @@ export interface AuthBrokerConfig {
71
101
  samlCookies?: (destination: string) => (samlResponse: string) => Promise<string>;
72
102
  /** The replay store the SAML assertion validators share. */
73
103
  assertionReplayStore?: (destination: string) => IAssertionReplayStore;
104
+ /**
105
+ * How a destination's client authenticates to the authorization server, for
106
+ * every grant that authenticates one (`ClientAuthenticationGrant`). Called
107
+ * once when the destination's provider is built, with the client the key
108
+ * store answers and its certificate, read only if the strategy asks.
109
+ *
110
+ * A given strategy always answers one, or throws: there is no "nothing"
111
+ * answer, so an explicit choice never falls back to the secret. A throw
112
+ * becomes a `DestinationConfigError` naming `clientAuthentication`, in fixed
113
+ * words. Absent: the client secret, as in 4.0.0, and nothing
114
+ * certificate-related is read. Shipped: `fromServiceKeyCertificate()`,
115
+ * `fromServiceKeySecret({ encoding })`.
116
+ *
117
+ * It applies to the providers the broker builds: `getProvider`'s, and the
118
+ * token API's `provider` factory, which gets its answer as a fourth
119
+ * argument (`TokenProviderClient`) for a grant that authenticates a client.
120
+ * An instance given as `provider` is already composed by the consumer, so
121
+ * the token API uses it as given and calls no strategy for it.
122
+ */
123
+ clientAuthentication?: ClientAuthenticationStrategy;
74
124
  }
75
125
  /**
76
126
  * AuthBroker manages authentication tokens for destinations
@@ -101,6 +151,7 @@ export declare class AuthBroker {
101
151
  private readonly deviceCodePresenter;
102
152
  private readonly samlCookies;
103
153
  private readonly assertionReplayStore;
154
+ private readonly clientAuthentication;
104
155
  /** getProvider's writes of the session secret, retried on their own. */
105
156
  private readonly writer;
106
157
  /**
@@ -163,12 +214,29 @@ export declare class AuthBroker {
163
214
  */
164
215
  private consumerProviderFor;
165
216
  /**
166
- * The credentials a consumer's factory is built with: the session's own when
167
- * it holds them, else the service key's, carrying the refresh token the
168
- * session stored — the 3.x order. A session store of auth-stores 3
217
+ * The token API's strategy path: with a `clientAuthentication` strategy and a
218
+ * destination stating a grant whose client authenticates, the strategy's
219
+ * answer — resolved inside the guard — and the client identity: the secret
220
+ * client, else the certificate client's, read through the context's memoised
221
+ * `readCertificate`. `null` without a strategy (4.0.0: nothing
222
+ * certificate-related is asked) and for a grant that authenticates no
223
+ * client (`saml2_pure`, `none`), as `getProvider` decides.
224
+ *
225
+ * The strategy is told the grant, so a destination that states none is
226
+ * refused before it is called rather than built without it. (An instance
227
+ * given as `provider` is the consumer's own composition: the strategy
228
+ * applies only to what the broker builds, so it is not used there.)
229
+ */
230
+ private consumerClientAuthentication;
231
+ /**
232
+ * What a consumer's factory's credentials are composed from, read in the
233
+ * 3.x order: the session's authorization config; only when it has none, the
234
+ * session and then the key store's client. A session store of auth-stores 3
169
235
  * answers no client, so the key store's is what is found.
170
236
  */
171
- private resolveAuthorizationConfig;
237
+ private readAuthorization;
238
+ /** The session the store holds for the destination, or null. */
239
+ private loadStoredSession;
172
240
  /**
173
241
  * `serviceUrl` for a consumer's provider, in the 3.x order: the session's,
174
242
  * else the key store's, else an error before the provider is asked.
@@ -250,8 +318,10 @@ export declare class AuthBroker {
250
318
  * write time, so a result without one does not erase the stored one — for
251
319
  * a provider the broker built, only a stored one bound where this one is:
252
320
  * a refresh token obtained for another resource or from another client is
253
- * not carried into this secret. A consumer's provider was seeded
254
- * with whatever the session held, so whatever it holds is carried.
321
+ * not carried into this secret. So too for a consumer's factory beside a
322
+ * `clientAuthentication` strategy, seeded only with a bound session. A
323
+ * consumer's provider without one was seeded with whatever the session
324
+ * held, so whatever it holds is carried (4.0.0).
255
325
  * - `issuedFor` / `issuedBy`: the binding computed when the provider was
256
326
  * built, each left out when the means lack its source — so the
257
327
  * store clears it.
@@ -1 +1 @@
1
- {"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"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"}
@@ -24,6 +24,7 @@ exports.AuthBroker = void 0;
24
24
  const interfaces_auth_1 = require("@mcp-abap-adt/interfaces-auth");
25
25
  const binding_1 = require("./binding");
26
26
  const bindingOf_1 = require("./bindingOf");
27
+ const clientAuthentication_1 = require("./clientAuthentication");
27
28
  const DestinationConfigError_1 = require("./DestinationConfigError");
28
29
  const destinations_1 = require("./destinations");
29
30
  const SessionWriter_1 = require("./SessionWriter");
@@ -43,6 +44,37 @@ const SECRET_FIELDS = [
43
44
  'issuedBy',
44
45
  ];
45
46
  const isSecretField = (field) => SECRET_FIELDS.includes(field);
47
+ /**
48
+ * The connection means a strategy-path seed carries — the connection's own,
49
+ * none of them a secret. Everything else a store's connection read answers
50
+ * (credentials, grant data, fields outside the type) stays behind.
51
+ */
52
+ const SEED_MEANS = [
53
+ 'serviceUrl',
54
+ 'sapClient',
55
+ 'language',
56
+ 'authType',
57
+ 'grantType',
58
+ ];
59
+ /**
60
+ * The token API factory's seed on the `clientAuthentication` strategy path,
61
+ * from an allowlist only: `SEED_MEANS`, and the session secret's own fields
62
+ * (`SECRET_FIELDS`: the token or cookies, the expiry, the binding) only when
63
+ * that same read is bound here — each secret judged by the read it came from.
64
+ * `serviceUrl` is the one resolved for the destination.
65
+ */
66
+ function strategySeed(connConfig, binding, serviceUrl) {
67
+ const source = (connConfig ?? {});
68
+ const fields = connConfig && (0, binding_1.boundHere)(connConfig, binding)
69
+ ? [...SEED_MEANS, ...SECRET_FIELDS]
70
+ : SEED_MEANS;
71
+ const seed = {};
72
+ for (const field of fields) {
73
+ if (source[field] !== undefined)
74
+ seed[field] = source[field];
75
+ }
76
+ return { ...seed, serviceUrl };
77
+ }
46
78
  /** When the result expires, in epoch ms: its own `expiresAt`, else `expiresIn` from now. */
47
79
  function expiryOf(result) {
48
80
  if (typeof result.expiresAt === 'number')
@@ -58,6 +90,39 @@ function obtainsTokens(provider) {
58
90
  return (typeof candidate.getTokens === 'function' &&
59
91
  typeof candidate.refreshTokens === 'function');
60
92
  }
93
+ /** The refresh token a stored session holds, unchecked. */
94
+ function storedRefreshTokenOf(session) {
95
+ return typeof session?.refreshToken === 'string'
96
+ ? session.refreshToken
97
+ : undefined;
98
+ }
99
+ /**
100
+ * The 3.x composition, the no-strategy path: the session's client as it is,
101
+ * else the key store's with `refreshToken` — the one given, else the key's
102
+ * own. (The strategy path takes `strategyAuthorization`.)
103
+ */
104
+ function composeAuthorization(read, refreshToken) {
105
+ if (read.sessionAuth)
106
+ return read.sessionAuth;
107
+ if (!read.keyAuth)
108
+ return null;
109
+ return {
110
+ ...read.keyAuth,
111
+ refreshToken: refreshToken ?? read.keyAuth.refreshToken,
112
+ };
113
+ }
114
+ /**
115
+ * The client on the `clientAuthentication` strategy path: the session's own
116
+ * client, else the key store's, carrying only `refreshToken` — the one the
117
+ * session read whose binding was checked holds, when bound — never a refresh
118
+ * token either client read carried itself.
119
+ */
120
+ function strategyAuthorization(read, refreshToken) {
121
+ // The same allowlist the strategy is told (`contextClient`): nothing else a
122
+ // store's authorization-config read carried reaches the factory.
123
+ const client = (0, clientAuthentication_1.contextClient)(read.sessionAuth ?? read.keyAuth);
124
+ return client ? { ...client, refreshToken } : null;
125
+ }
61
126
  /** A stored string that counts as present: `''` is none. */
62
127
  function present(value) {
63
128
  return typeof value === 'string' && value !== '';
@@ -105,6 +170,7 @@ class AuthBroker {
105
170
  deviceCodePresenter;
106
171
  samlCookies;
107
172
  assertionReplayStore;
173
+ clientAuthentication;
108
174
  /** getProvider's writes of the session secret, retried on their own. */
109
175
  writer;
110
176
  /**
@@ -158,6 +224,7 @@ class AuthBroker {
158
224
  this.deviceCodePresenter = config.deviceCodePresenter;
159
225
  this.samlCookies = config.samlCookies;
160
226
  this.assertionReplayStore = config.assertionReplayStore;
227
+ this.clientAuthentication = config.clientAuthentication;
161
228
  this.logger = logger ?? noOpLogger;
162
229
  this.writer = new SessionWriter_1.SessionWriter(async (destination, bound) => {
163
230
  try {
@@ -268,14 +335,14 @@ class AuthBroker {
268
335
  async obtainFromConsumer(destination, method, means) {
269
336
  const connConfig = await this.read(destination, 'session connection config', () => this.sessionStore.getConnectionConfig(destination));
270
337
  const serviceUrl = await this.resolveServiceUrl(destination, connConfig);
271
- const { provider, binding } = await this.consumerProviderFor(destination, serviceUrl, connConfig, means);
338
+ const { provider, binding, carry } = await this.consumerProviderFor(destination, serviceUrl, connConfig, means);
272
339
  this.logger.debug(`[AuthBroker] ${method} for ${destination}`);
273
340
  const result = checked(destination, await provider[method]());
274
341
  await this.writer.submit(destination, {
275
342
  result: { ...result, expiresAt: expiryOf(result) },
276
343
  original: result,
277
344
  binding,
278
- carry: 'any',
345
+ carry,
279
346
  });
280
347
  this.throwFailedWrite(destination, result);
281
348
  return result;
@@ -309,22 +376,76 @@ class AuthBroker {
309
376
  return {
310
377
  provider,
311
378
  binding: (0, binding_1.consumerBinding)(serviceUrl, sapClient, null),
379
+ carry: 'any',
312
380
  };
313
381
  }
314
- const client = await this.resolveAuthorizationConfig(destination);
315
- const built = provider(destination, client, {
316
- ...(connConfig ?? {}),
317
- serviceUrl,
318
- });
382
+ const read = await this.readAuthorization(destination);
383
+ const strategic = await this.consumerClientAuthentication(destination, means,
384
+ // The strategy is told the client, never a refresh token read with it.
385
+ strategyAuthorization(read, undefined));
386
+ let built;
387
+ let client;
388
+ let binding;
389
+ let carry;
390
+ if (strategic) {
391
+ // The strategy's answer and the client identity, resolved before the
392
+ // factory; the factory called inside the same guard: a throw is fixed
393
+ // words, nothing of the thrown value, no cause. The stored refresh
394
+ // token is carried only when the session is bound to this resource
395
+ // and this client identity — never to another authorization server.
396
+ binding = (0, binding_1.strategyBinding)((0, binding_1.consumerBinding)(serviceUrl, sapClient, strategic.identity));
397
+ const session = read.sessionRead
398
+ ? read.session
399
+ : await this.loadStoredSession(destination);
400
+ const bound = !!session && (0, binding_1.boundHere)(session, binding);
401
+ const boundRefreshToken = bound
402
+ ? storedRefreshTokenOf(session)
403
+ : undefined;
404
+ // The client without any refresh token of its own read: a session
405
+ // store's `getAuthorizationConfig` is another read than the session
406
+ // whose binding was checked, and another process may have written
407
+ // between them. The refresh token comes only from that checked read.
408
+ client = strategyAuthorization(read, boundRefreshToken);
409
+ // Each stored secret is judged by the read it came from: the seed's
410
+ // token, cookies and expiry by the binding the connection read itself
411
+ // carries — another process may have written between the two reads —
412
+ // else only the means seed the provider.
413
+ const seed = strategySeed(connConfig, binding, serviceUrl);
414
+ carry = 'bound';
415
+ const refreshToken = client ? client.refreshToken : boundRefreshToken;
416
+ const fourth = {
417
+ clientAuthentication: strategic.clientAuthentication,
418
+ ...(strategic.identity
419
+ ? {
420
+ uaaUrl: strategic.identity.uaaUrl,
421
+ clientId: strategic.identity.uaaClientId,
422
+ }
423
+ : {}),
424
+ ...(present(refreshToken) ? { refreshToken } : {}),
425
+ };
426
+ try {
427
+ built = provider(destination, client, seed, fourth);
428
+ }
429
+ catch {
430
+ throw new DestinationConfigError_1.DestinationConfigError(destination, ['provider', 'clientAuthentication'], 'the token provider factory failed beside the clientAuthentication strategy');
431
+ }
432
+ }
433
+ else {
434
+ // 4.0.0: the stored secret and refresh token carried over as read.
435
+ client = composeAuthorization(read, storedRefreshTokenOf(read.session));
436
+ binding = (0, binding_1.consumerBinding)(serviceUrl, sapClient, client);
437
+ carry = 'any';
438
+ built = provider(destination, client, {
439
+ ...(connConfig ?? {}),
440
+ serviceUrl,
441
+ });
442
+ }
319
443
  this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
320
444
  hasCredentials: !!client,
321
445
  hasRefreshToken: !!client?.refreshToken,
322
446
  hasStoredToken: !!(connConfig?.authorizationToken || connConfig?.sessionCookies),
323
447
  });
324
- return {
325
- provider: built,
326
- binding: (0, binding_1.consumerBinding)(serviceUrl, sapClient, client),
327
- };
448
+ return { provider: built, binding, carry };
328
449
  });
329
450
  this.consumerBuilt.set(destination, build);
330
451
  build.catch(() => {
@@ -335,31 +456,61 @@ class AuthBroker {
335
456
  return build;
336
457
  }
337
458
  /**
338
- * The credentials a consumer's factory is built with: the session's own when
339
- * it holds them, else the service key's, carrying the refresh token the
340
- * session stored — the 3.x order. A session store of auth-stores 3
459
+ * The token API's strategy path: with a `clientAuthentication` strategy and a
460
+ * destination stating a grant whose client authenticates, the strategy's
461
+ * answer — resolved inside the guard — and the client identity: the secret
462
+ * client, else the certificate client's, read through the context's memoised
463
+ * `readCertificate`. `null` without a strategy (4.0.0: nothing
464
+ * certificate-related is asked) and for a grant that authenticates no
465
+ * client (`saml2_pure`, `none`), as `getProvider` decides.
466
+ *
467
+ * The strategy is told the grant, so a destination that states none is
468
+ * refused before it is called rather than built without it. (An instance
469
+ * given as `provider` is the consumer's own composition: the strategy
470
+ * applies only to what the broker builds, so it is not used there.)
471
+ */
472
+ async consumerClientAuthentication(destination, means, client) {
473
+ const strategy = this.clientAuthentication;
474
+ if (!strategy) {
475
+ return null;
476
+ }
477
+ // basic and snc never get here: the token API refuses them first
478
+ // (`statedForTokens`).
479
+ if (!means || (means.authType !== 'jwt' && means.authType !== 'saml')) {
480
+ throw new DestinationConfigError_1.DestinationConfigError(destination, ['authType', 'grantType'], 'a clientAuthentication strategy is told the grant: the destination must state its authType and grantType');
481
+ }
482
+ const grant = (0, destinations_1.statedGrant)(destination, means.authType, means);
483
+ if (grant === 'saml2_pure' || grant === 'none') {
484
+ return null;
485
+ }
486
+ const serviceKeyStore = this.serviceKeyStore;
487
+ const context = (0, clientAuthentication_1.clientAuthenticationContext)(destination, grant, client, () => this.read(destination, 'client certificate', async () => serviceKeyStore?.getClientCertificate
488
+ ? serviceKeyStore.getClientCertificate(destination)
489
+ : null));
490
+ const clientAuthentication = await (0, clientAuthentication_1.resolveClientAuthentication)(strategy, context);
491
+ return { clientAuthentication, identity: await (0, clientAuthentication_1.clientIdentity)(context) };
492
+ }
493
+ /**
494
+ * What a consumer's factory's credentials are composed from, read in the
495
+ * 3.x order: the session's authorization config; only when it has none, the
496
+ * session and then the key store's client. A session store of auth-stores 3
341
497
  * answers no client, so the key store's is what is found.
342
498
  */
343
- async resolveAuthorizationConfig(destination) {
499
+ async readAuthorization(destination) {
344
500
  const sessionAuth = await this.read(destination, 'session authorization config', () => this.sessionStore.getAuthorizationConfig(destination));
345
501
  if (sessionAuth) {
346
- return sessionAuth;
502
+ return { sessionAuth, keyAuth: null, session: null, sessionRead: false };
347
503
  }
348
- const session = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
349
- const storedRefreshToken = typeof session?.refreshToken === 'string'
350
- ? session.refreshToken
351
- : undefined;
504
+ const session = await this.loadStoredSession(destination);
352
505
  const serviceKeyStore = this.serviceKeyStore;
353
506
  const keyAuth = serviceKeyStore
354
507
  ? await this.read(destination, 'service key authorization config', () => serviceKeyStore.getAuthorizationConfig(destination))
355
508
  : null;
356
- if (!keyAuth) {
357
- return null;
358
- }
359
- return {
360
- ...keyAuth,
361
- refreshToken: storedRefreshToken ?? keyAuth.refreshToken,
362
- };
509
+ return { sessionAuth: null, keyAuth, session, sessionRead: true };
510
+ }
511
+ /** The session the store holds for the destination, or null. */
512
+ loadStoredSession(destination) {
513
+ return this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
363
514
  }
364
515
  /**
365
516
  * `serviceUrl` for a consumer's provider, in the 3.x order: the session's,
@@ -509,10 +660,29 @@ class AuthBroker {
509
660
  // saml2_pure), what a secret for this destination is bound to
510
661
  // — written with every secret, and required of a stored one
511
662
  // before it seeds — and the stored secret when it is.
512
- const client = grant === 'saml2_pure'
663
+ const secretClient = grant === 'saml2_pure'
513
664
  ? null
514
665
  : await this.read(destination, 'client', () => serviceKeyStore.getAuthorizationConfig(destination));
515
- const binding = (0, bindingOf_1.destinationBinding)(authType, grant, stated, client);
666
+ // The consumer's client authentication, resolved inside the guard
667
+ // before any provider exists: a strategy that throws leaves nothing
668
+ // built and nothing cached. Then the client identity the row and the
669
+ // binding take — the secret client, else the certificate client's —
670
+ // so the binding and the seed are chosen from it. Without a strategy
671
+ // nothing of this runs: 4.0.0's client, nothing certificate-related.
672
+ let client = secretClient;
673
+ let clientAuthentication;
674
+ if (this.clientAuthentication && grant !== 'saml2_pure') {
675
+ const context = (0, clientAuthentication_1.clientAuthenticationContext)(destination, grant, secretClient, () => this.read(destination, 'client certificate', async () => serviceKeyStore.getClientCertificate
676
+ ? serviceKeyStore.getClientCertificate(destination)
677
+ : null));
678
+ clientAuthentication = await (0, clientAuthentication_1.resolveClientAuthentication)(this.clientAuthentication, context);
679
+ client = await (0, clientAuthentication_1.clientIdentity)(context);
680
+ }
681
+ const computed = (0, bindingOf_1.destinationBinding)(authType, grant, stated, client);
682
+ // The strategy path: a resource neither side states matches.
683
+ const binding = clientAuthentication
684
+ ? (0, binding_1.strategyBinding)(computed)
685
+ : computed;
516
686
  const stored = grant === 'client_credentials'
517
687
  ? null
518
688
  : await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
@@ -531,6 +701,7 @@ class AuthBroker {
531
701
  destination,
532
702
  client,
533
703
  secret,
704
+ ...(clientAuthentication ? { clientAuthentication } : {}),
534
705
  logger: this.logger,
535
706
  onTokens,
536
707
  };
@@ -623,8 +794,10 @@ class AuthBroker {
623
794
  * write time, so a result without one does not erase the stored one — for
624
795
  * a provider the broker built, only a stored one bound where this one is:
625
796
  * a refresh token obtained for another resource or from another client is
626
- * not carried into this secret. A consumer's provider was seeded
627
- * with whatever the session held, so whatever it holds is carried.
797
+ * not carried into this secret. So too for a consumer's factory beside a
798
+ * `clientAuthentication` strategy, seeded only with a bound session. A
799
+ * consumer's provider without one was seeded with whatever the session
800
+ * held, so whatever it holds is carried (4.0.0).
628
801
  * - `issuedFor` / `issuedBy`: the binding computed when the provider was
629
802
  * built, each left out when the means lack its source — so the
630
803
  * store clears it.
package/dist/binding.d.ts CHANGED
@@ -24,6 +24,8 @@
24
24
  */
25
25
  import type { IConfig, IConnectionConfig } from '@mcp-abap-adt/interfaces-auth-broker';
26
26
  import type { IAuthorizationConfig } from '@mcp-abap-adt/interfaces-auth-sap';
27
+ /** What a binding reads of a client: who it is, never its secret. */
28
+ export type BoundClient = Pick<IAuthorizationConfig, 'uaaUrl' | 'uaaClientId'>;
27
29
  /**
28
30
  * The resource: `serviceUrl` with the SAP client as `sap-client` — the
29
31
  * destination's `sapClient` when it states one, over a `sap-client` already in
@@ -51,19 +53,28 @@ export interface Binding {
51
53
  * matches nothing.
52
54
  */
53
55
  issuerStated: boolean;
56
+ /**
57
+ * Set on the `clientAuthentication` strategy path only: a resource neither
58
+ * side states — no `issuedFor` computed (no service URL, or one that does
59
+ * not parse, as the CLI's XSUAA placeholder) and none stored — matches, so
60
+ * the issuer and client alone decide. A resource stated on one side only
61
+ * never matches. Without it (the 4.0.0 path) an absent resource matches
62
+ * nothing.
63
+ */
64
+ unstatedResourceMatches?: true;
54
65
  }
55
66
  /**
56
67
  * The UAA grants (`authorization_code`, `client_credentials`, `passcode`) and
57
68
  * `saml2_bearer`, whose token UAA issues to its client: the resource, and `uaaUrl` with `uaaClientId` — both required to match.
58
69
  */
59
- export declare function uaaBinding(means: IConnectionConfig, client: IAuthorizationConfig | null): Binding;
70
+ export declare function uaaBinding(means: IConnectionConfig, client: BoundClient | null): Binding;
60
71
  /**
61
72
  * The OIDC grants: the resource, and the issuer — `oidcIssuerUrl`, else
62
73
  * `uaaUrl` — with `uaaClientId`; both required to match. Means that state
63
74
  * neither issuer (explicit endpoints alone) bind no stored secret: one is then
64
75
  * never seeded.
65
76
  */
66
- export declare function oidcBinding(means: IConnectionConfig, client: IAuthorizationConfig | null): Binding;
77
+ export declare function oidcBinding(means: IConnectionConfig, client: BoundClient | null): Binding;
67
78
  /**
68
79
  * `saml2_pure`: the resource, and the ACS of the system that sets the
69
80
  * cookies — `samlAcsUrl`, origin and path; both required to match.
@@ -76,20 +87,31 @@ export declare function samlPureBinding(means: IConnectionConfig): Binding;
76
87
  * handed no client, so no issuer is claimed for what it obtains: `issuedBy` is
77
88
  * then absent, and `getProvider` never seeds from that secret.
78
89
  */
79
- export declare function consumerBinding(serviceUrl: string, sapClient: string | undefined, client: IAuthorizationConfig | null): Binding;
90
+ export declare function consumerBinding(serviceUrl: string, sapClient: string | undefined, client: BoundClient | null): Binding;
91
+ /**
92
+ * A binding on the `clientAuthentication` strategy path: a resource that
93
+ * neither the means nor the stored session state matches, so the issuer and
94
+ * client alone decide (`Binding.unstatedResourceMatches`).
95
+ */
96
+ export declare function strategyBinding(binding: Binding): Binding;
80
97
  /**
81
98
  * A `none` row: the resource always; the issuer only when the means state one
82
99
  * — `oidcIssuerUrl` (with `uaaClientId` when stated) or the client (`uaaUrl`
83
100
  * and `uaaClientId`) for `jwt`, `samlAcsUrl` for `saml`.
84
101
  */
85
- export declare function handedOverBinding(authType: 'jwt' | 'saml', means: IConnectionConfig, client: IAuthorizationConfig | null): Binding;
86
- /** The stored `issuedFor`, canonicalised, equals the destination's. */
102
+ export declare function handedOverBinding(authType: 'jwt' | 'saml', means: IConnectionConfig, client: BoundClient | null): Binding;
103
+ /**
104
+ * The stored `issuedFor`, canonicalised, equals the destination's — or, with
105
+ * `unstatedResourceMatches`, neither states one.
106
+ */
87
107
  export declare function sameResource(stored: IConfig | null, binding: Binding): boolean;
88
108
  /** The stored `issuedBy`, canonicalised as the destination's kind, equals it. */
89
109
  export declare function sameIssuer(stored: IConfig | null, binding: Binding): boolean;
90
110
  /**
91
111
  * An obtained secret is this destination's only when **both** stored values
92
- * equal the computed ones; either absent, on either side, is not a match.
112
+ * equal the computed ones; either absent, on either side, is not a match —
113
+ * save a resource absent on both sides of a strategy-path binding
114
+ * (`unstatedResourceMatches`).
93
115
  */
94
116
  export declare function boundHere(stored: IConfig | null, binding: Binding): boolean;
95
117
  //# sourceMappingURL=binding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACV,OAAO,EACP,iBAAiB,EAClB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AA6C9E;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,OAAO,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,SAAS,CAEpB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,SAAS,EAAE,OAAO,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAEpB;AAED,4EAA4E;AAC5E,wBAAgB,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE1D;AAED,mFAAmF;AACnF,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE1C,+FAA+F;AAC/F,MAAM,WAAW,OAAO;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,UAAU,EAAE,UAAU,CAAC;IACvB;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAC;CACvB;AAUD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAClC,OAAO,CAUT;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAClC,OAAO,CAWT;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAOjE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAClC,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,KAAK,GAAG,MAAM,EACxB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAClC,OAAO,CA4BT;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAC1B,MAAM,EAAE,OAAO,GAAG,IAAI,EACtB,OAAO,EAAE,OAAO,GACf,OAAO,CAKT;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAI5E;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAE3E"}
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACV,OAAO,EACP,iBAAiB,EAClB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAE9E,qEAAqE;AACrE,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,oBAAoB,EAAE,QAAQ,GAAG,aAAa,CAAC,CAAC;AA6C/E;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,OAAO,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,SAAS,CAEpB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,SAAS,EAAE,OAAO,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAEpB;AAED,4EAA4E;AAC5E,wBAAgB,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE1D;AAED,mFAAmF;AACnF,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE1C,+FAA+F;AAC/F,MAAM,WAAW,OAAO;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,UAAU,EAAE,UAAU,CAAC;IACvB;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,uBAAuB,CAAC,EAAE,IAAI,CAAC;CAChC;AAUD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CAUT;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CAWT;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAOjE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,KAAK,GAAG,MAAM,EACxB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CA4BT;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,OAAO,GAAG,IAAI,EACtB,OAAO,EAAE,OAAO,GACf,OAAO,CAOT;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAI5E;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAE3E"}
package/dist/binding.js CHANGED
@@ -31,6 +31,7 @@ exports.uaaBinding = uaaBinding;
31
31
  exports.oidcBinding = oidcBinding;
32
32
  exports.samlPureBinding = samlPureBinding;
33
33
  exports.consumerBinding = consumerBinding;
34
+ exports.strategyBinding = strategyBinding;
34
35
  exports.handedOverBinding = handedOverBinding;
35
36
  exports.sameResource = sameResource;
36
37
  exports.sameIssuer = sameIssuer;
@@ -152,6 +153,14 @@ function samlPureBinding(means) {
152
153
  function consumerBinding(serviceUrl, sapClient, client) {
153
154
  return uaaBinding({ serviceUrl, sapClient }, client);
154
155
  }
156
+ /**
157
+ * A binding on the `clientAuthentication` strategy path: a resource that
158
+ * neither the means nor the stored session state matches, so the issuer and
159
+ * client alone decide (`Binding.unstatedResourceMatches`).
160
+ */
161
+ function strategyBinding(binding) {
162
+ return { ...binding, unstatedResourceMatches: true };
163
+ }
155
164
  /**
156
165
  * A `none` row: the resource always; the issuer only when the means state one
157
166
  * — `oidcIssuerUrl` (with `uaaClientId` when stated) or the client (`uaaUrl`
@@ -186,10 +195,15 @@ function handedOverBinding(authType, means, client) {
186
195
  issuerStated: stated,
187
196
  };
188
197
  }
189
- /** The stored `issuedFor`, canonicalised, equals the destination's. */
198
+ /**
199
+ * The stored `issuedFor`, canonicalised, equals the destination's — or, with
200
+ * `unstatedResourceMatches`, neither states one.
201
+ */
190
202
  function sameResource(stored, binding) {
191
- return (binding.issuedFor !== undefined &&
192
- resourceUri(stored?.issuedFor) === binding.issuedFor);
203
+ if (binding.issuedFor === undefined) {
204
+ return (binding.unstatedResourceMatches === true && !present(stored?.issuedFor));
205
+ }
206
+ return resourceUri(stored?.issuedFor) === binding.issuedFor;
193
207
  }
194
208
  /** The stored `issuedBy`, canonicalised as the destination's kind, equals it. */
195
209
  function sameIssuer(stored, binding) {
@@ -200,7 +214,9 @@ function sameIssuer(stored, binding) {
200
214
  }
201
215
  /**
202
216
  * An obtained secret is this destination's only when **both** stored values
203
- * equal the computed ones; either absent, on either side, is not a match.
217
+ * equal the computed ones; either absent, on either side, is not a match —
218
+ * save a resource absent on both sides of a strategy-path binding
219
+ * (`unstatedResourceMatches`).
204
220
  */
205
221
  function boundHere(stored, binding) {
206
222
  return sameResource(stored, binding) && sameIssuer(stored, binding);
@@ -6,6 +6,7 @@
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 { BoundClient } from './binding';
9
10
  import { type Binding } from './binding';
10
11
  /**
11
12
  * The binding of a `jwt` or `saml` destination with the grant it states: the
@@ -13,7 +14,7 @@ import { type Binding } from './binding';
13
14
  * the issuer and the client, `saml2_pure` by its ACS, and `none` by what the
14
15
  * means state.
15
16
  */
16
- export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client: IAuthorizationConfig | null): Binding;
17
+ export declare function destinationBinding(authType: 'jwt' | 'saml', grant: DestinationGrant, means: IConnectionConfig, client: BoundClient | null): Binding;
17
18
  /** The two strings a session store keeps beside a secret; absent where the means cannot say. */
18
19
  export interface SecretBinding {
19
20
  issuedFor?: string;
@@ -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,EAKb,MAAM,WAAW,CAAC;AAGnB;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,KAAK,GAAG,MAAM,EACxB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAClC,OAAO,CAKT;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,GAAE,oBAAoB,GAAG,IAAW,GACzC,aAAa,CAcf"}
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,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EACL,KAAK,OAAO,EAKb,MAAM,WAAW,CAAC;AAGnB;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,KAAK,GAAG,MAAM,EACxB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CAKT;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,iBAAiB,EACxB,MAAM,GAAE,oBAAoB,GAAG,IAAW,GACzC,aAAa,CAcf"}