@mcp-abap-adt/auth-providers 2.2.0 → 2.2.1

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 CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.1] - 2026-09-24
11
+
12
+ ### Fixed
13
+
14
+ - **`Saml2BearerProvider` spends its refresh token** (#23). `performRefresh()`
15
+ used to run a full interactive login on both of its branches, so a session
16
+ holding a valid refresh token still went through the IdP and a browser — and
17
+ a headless consumer got `BROWSER_AUTH_REQUIRED` from `auth-broker`. It now
18
+ sends a `refresh_token` grant to the same token endpoint and with the same
19
+ client credentials as the assertion exchange, keeps the refresh token it spent
20
+ when the response carries no new one, and falls back to a full login when the
21
+ grant is refused.
22
+
10
23
  ## [2.2.0] - 2026-09-24
11
24
 
12
25
  ### Dependencies
package/README.md CHANGED
@@ -378,6 +378,14 @@ const provider = new Saml2BearerProvider({
378
378
  const broker = new AuthBroker({ tokenProvider: provider }, 'none');
379
379
  ```
380
380
 
381
+ **Refresh.** When the token endpoint returns a `refresh_token` with the SAML
382
+ bearer exchange, `Saml2BearerProvider` spends it once the access token expires:
383
+ a `refresh_token` grant to the same endpoint (`tokenUrl`, or `uaaUrl` +
384
+ `/oauth/token`) with the same client credentials, and no assertion, strategy or
385
+ browser involved. Pass a stored one back as `refreshToken` in the config and the
386
+ next `getTokens()` uses it. If the grant is refused, or no refresh token was
387
+ ever issued, the provider falls back to a full login through `authorization`.
388
+
381
389
  Pure SAML example (cookie-based):
382
390
 
383
391
  ```typescript
@@ -9,4 +9,12 @@ export interface Saml2TokenExchangeResponse {
9
9
  tokenType?: string;
10
10
  }
11
11
  export declare function exchangeSamlAssertion(samlResponse: string, tokenUrl: string, clientId: string | undefined, clientSecret: string | undefined, logger?: ILogger): Promise<Saml2TokenExchangeResponse>;
12
+ /**
13
+ * Spends a refresh token obtained from a SAML bearer exchange.
14
+ *
15
+ * Sent to the same token endpoint, with the same client authentication, as the
16
+ * exchange that issued it — so an explicit `tokenUrl` is honoured rather than
17
+ * rebuilt from a UAA base URL.
18
+ */
19
+ export declare function refreshSamlBearerToken(refreshToken: string, tokenUrl: string, clientId: string | undefined, clientSecret?: string, logger?: ILogger): Promise<Saml2TokenExchangeResponse>;
12
20
  //# sourceMappingURL=saml2TokenExchange.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"saml2TokenExchange.d.ts","sourceRoot":"","sources":["../../src/auth/saml2TokenExchange.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CAwCrC"}
1
+ {"version":3,"file":"saml2TokenExchange.d.ts","sourceRoot":"","sources":["../../src/auth/saml2TokenExchange.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CAwCrC;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,CAAC,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CAwCrC"}
@@ -7,6 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.exchangeSamlAssertion = exchangeSamlAssertion;
10
+ exports.refreshSamlBearerToken = refreshSamlBearerToken;
10
11
  const axios_1 = __importDefault(require("axios"));
11
12
  function toBasicAuth(clientId, clientSecret) {
12
13
  return Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
@@ -49,3 +50,48 @@ async function exchangeSamlAssertion(samlResponse, tokenUrl, clientId, clientSec
49
50
  tokenType: data.token_type,
50
51
  };
51
52
  }
53
+ /**
54
+ * Spends a refresh token obtained from a SAML bearer exchange.
55
+ *
56
+ * Sent to the same token endpoint, with the same client authentication, as the
57
+ * exchange that issued it — so an explicit `tokenUrl` is honoured rather than
58
+ * rebuilt from a UAA base URL.
59
+ */
60
+ async function refreshSamlBearerToken(refreshToken, tokenUrl, clientId, clientSecret, logger) {
61
+ const params = new URLSearchParams();
62
+ params.append('grant_type', 'refresh_token');
63
+ params.append('refresh_token', refreshToken);
64
+ if (clientId) {
65
+ params.append('client_id', clientId);
66
+ }
67
+ logger?.info('[SAML] Refreshing token', { tokenUrl });
68
+ const headers = {
69
+ 'Content-Type': 'application/x-www-form-urlencoded',
70
+ };
71
+ if (clientId && clientSecret) {
72
+ headers.Authorization = `Basic ${toBasicAuth(clientId, clientSecret)}`;
73
+ }
74
+ let response;
75
+ try {
76
+ response = await axios_1.default.post(tokenUrl, params.toString(), { headers });
77
+ }
78
+ catch (error) {
79
+ if (axios_1.default.isAxiosError(error)) {
80
+ logger?.error('[SAML] Token refresh failed', {
81
+ status: error.response?.status,
82
+ data: error.response?.data,
83
+ });
84
+ }
85
+ throw error;
86
+ }
87
+ const data = response.data;
88
+ if (!data?.access_token) {
89
+ throw new Error('Refresh response missing access_token');
90
+ }
91
+ return {
92
+ accessToken: data.access_token,
93
+ refreshToken: data.refresh_token,
94
+ expiresIn: data.expires_in,
95
+ tokenType: data.token_type,
96
+ };
97
+ }
@@ -17,6 +17,11 @@ export declare class Saml2BearerProvider extends BaseTokenProvider {
17
17
  constructor(config: Saml2BearerProviderConfig);
18
18
  protected getAuthType(): OAuth2GrantType;
19
19
  protected performLogin(): Promise<ITokenResult>;
20
+ /**
21
+ * Spends the refresh token at the endpoint that issued it. A failure is
22
+ * thrown rather than handled: `BaseTokenProvider.getTokens()` drops the
23
+ * refresh token and falls back to `performLogin()`.
24
+ */
20
25
  protected performRefresh(): Promise<ITokenResult>;
21
26
  }
22
27
  //# sourceMappingURL=Saml2BearerProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Saml2BearerProvider.d.ts","sourceRoot":"","sources":["../../src/providers/Saml2BearerProvider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EAChB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,cAAc,CAAC;AAOtB,MAAM,WAAW,yBACf,SAAQ,iBAAiB,EACvB,yBAAyB;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAoB,SAAQ,iBAAiB;IACxD,OAAO,CAAC,MAAM,CAA4B;gBAE9B,MAAM,EAAE,yBAAyB;IAiB7C,SAAS,CAAC,WAAW,IAAI,eAAe;cAIxB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;cAoBrC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CAMxD"}
1
+ {"version":3,"file":"Saml2BearerProvider.d.ts","sourceRoot":"","sources":["../../src/providers/Saml2BearerProvider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EAChB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAK9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,cAAc,CAAC;AAOtB,MAAM,WAAW,yBACf,SAAQ,iBAAiB,EACvB,yBAAyB;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAoB,SAAQ,iBAAiB;IACxD,OAAO,CAAC,MAAM,CAA4B;gBAE9B,MAAM,EAAE,yBAAyB;IAiB7C,SAAS,CAAC,WAAW,IAAI,eAAe;cAIxB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAoBrD;;;;OAIG;cACa,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CAqBxD"}
@@ -42,11 +42,23 @@ class Saml2BearerProvider extends BaseTokenProvider_1.BaseTokenProvider {
42
42
  tokenType: 'jwt',
43
43
  };
44
44
  }
45
+ /**
46
+ * Spends the refresh token at the endpoint that issued it. A failure is
47
+ * thrown rather than handled: `BaseTokenProvider.getTokens()` drops the
48
+ * refresh token and falls back to `performLogin()`.
49
+ */
45
50
  async performRefresh() {
46
51
  if (!this.refreshToken) {
47
- return this.performLogin();
52
+ throw new Error('Refresh token is required for refresh');
48
53
  }
49
- return this.performLogin();
54
+ const tokens = await (0, saml2TokenExchange_1.refreshSamlBearerToken)(this.refreshToken, (0, saml2Utils_1.resolveTokenUrl)(this.config), this.config.clientId, this.config.clientSecret, this.logger);
55
+ return {
56
+ authorizationToken: tokens.accessToken,
57
+ refreshToken: tokens.refreshToken || this.refreshToken,
58
+ authType: interfaces_auth_1.AUTH_TYPE_SAML2_BEARER,
59
+ expiresIn: tokens.expiresIn,
60
+ tokenType: 'jwt',
61
+ };
50
62
  }
51
63
  }
52
64
  exports.Saml2BearerProvider = Saml2BearerProvider;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-providers",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Token providers for MCP ABAP ADT auth-broker",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",