@mcp-abap-adt/auth-broker 0.2.2 → 0.2.4

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
@@ -11,6 +11,58 @@ Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the co
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.2.4] - 2025-12-19
15
+
16
+ ### Changed
17
+ - **Comprehensive Error Handling**: Added robust error handling for all external operations
18
+ - **SessionStore errors**: Handle FILE_NOT_FOUND, PARSE_ERROR from session files (graceful degradation)
19
+ - **ServiceKeyStore errors**: Handle FILE_NOT_FOUND, PARSE_ERROR, INVALID_CONFIG from service key files (log and fallback)
20
+ - **TokenProvider errors**: Handle network errors (ECONNREFUSED, ETIMEDOUT, ENOTFOUND), validation errors, browser auth failures
21
+ - **Write operation errors**: Handle failures when saving tokens/config to session files
22
+ - All errors logged with detailed context (file paths, error codes, missing fields)
23
+ - Broker continues with fallback mechanisms when possible instead of crashing
24
+ - **Token Refresh Architecture**: Removed direct UAA HTTP requests from AuthBroker
25
+ - `getToken()` now uses provider's `refreshTokenFromSession()` (Step 2a) and `refreshTokenFromServiceKey()` (Step 2b) methods
26
+ - All authentication logic delegated to providers (XsuaaTokenProvider, BtpTokenProvider)
27
+ - Providers handle browser-based authentication and client_credentials flow internally
28
+ - Better error handling with typed errors from `@mcp-abap-adt/auth-providers@0.2.0`
29
+ - **Error Handling**: Improved error handling in token requests
30
+ - Network errors (connection issues) are now handled separately from HTTP errors (401, 403)
31
+ - Better error messages with UAA URL context when network errors occur
32
+ - No retry attempts for network errors (retries cannot fix infrastructure issues)
33
+
34
+ ### Fixed
35
+ - **Defensive Programming**: Treat all injected dependencies as untrusted
36
+ - File operations (session/service key stores) may fail - files missing, corrupted, permission issues
37
+ - Network operations (token provider) may fail - timeouts, connection refused, invalid responses
38
+ - All external operations wrapped in try-catch with specific error handling per operation type
39
+ - Prevents broker crashes when consumers misconfigure files or network issues occur
40
+ - **Network Error Detection**: Add proper network error detection in token requests
41
+ - Detect network errors: `ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND`, `ECONNRESET`, `ENETUNREACH`, `EHOSTUNREACH`
42
+ - Throw network errors immediately with clear error message indicating connectivity issues
43
+ - Prevents confusing error messages when VPN is down or server is unreachable
44
+ - Network errors now clearly indicate infrastructure issues vs authentication failures
45
+ - **Simplified Refresh**: `refreshToken()` now simply delegates to `getToken()` for full refresh flow
46
+ - Ensures consistent refresh behavior across all token operations
47
+ - No code duplication between getToken and refreshToken methods
48
+
49
+ ### Removed
50
+ - **Direct UAA Code**: Removed direct UAA request methods and old credential flow
51
+ - Removed `getTokenWithClientCredentials()` private method (logic moved to providers)
52
+ - Removed `refreshTokenDirect()` private method (logic moved to providers)
53
+ - Removed `allowClientCredentials` constructor parameter (handled by providers)
54
+ - Removed old "Step 2: UAA Credentials Flow" (replaced with provider-based Step 2a/2b)
55
+
56
+ ### Dependencies
57
+ - Updated `@mcp-abap-adt/interfaces` to `^0.2.3` for STORE_ERROR_CODES and TOKEN_PROVIDER_ERROR_CODES
58
+ - Updated `@mcp-abap-adt/auth-stores` to `^0.2.5` for typed errors (ParseError, FileNotFoundError, etc.)
59
+ - Updated `@mcp-abap-adt/auth-providers` to `^0.2.0` for new refresh methods and typed errors
60
+
61
+ ## [0.2.3] - 2025-12-18
62
+
63
+ ### Added
64
+ - `allowClientCredentials` config flag (default: true). Set to `false` to skip UAA client_credentials flow and force provider/browser-based login (useful for ABAP ADT backends that reject service tokens).
65
+
14
66
  ## [0.2.2] - 2025-12-13
15
67
 
16
68
  ### Changed
package/README.md CHANGED
@@ -52,6 +52,14 @@ const broker = new AuthBroker({
52
52
  serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'), // optional
53
53
  tokenProvider: new BtpTokenProvider(), // optional
54
54
  }, 'chrome', logger);
55
+
56
+ // Disable direct client_credentials (force provider/browser flow, e.g., for ABAP ADT)
57
+ const brokerNoClientCreds = new AuthBroker({
58
+ sessionStore: new AbapSessionStore('/path/to/destinations'),
59
+ serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'),
60
+ tokenProvider: new BtpTokenProvider(),
61
+ allowClientCredentials: false,
62
+ }, 'chrome', logger);
55
63
  ```
56
64
 
57
65
  ### Session + Service Key (For Initialization)
@@ -410,6 +418,54 @@ Gets authentication token for destination. Implements a three-step flow:
410
418
  - If `sessionStore` contains valid UAA credentials, neither `serviceKeyStore` nor `tokenProvider` are required. Direct UAA HTTP requests will be used automatically.
411
419
  - `tokenProvider` is only needed for browser authentication or when direct UAA requests fail.
412
420
  - Token validation is performed only when checking existing session. Tokens obtained through refresh/UAA/browser authentication are not validated before being saved.
421
+ - **Store errors are handled gracefully**: If service key files are missing or malformed, the broker logs the error and continues with fallback mechanisms (session store data or provider-based auth)
422
+
423
+ ##### Error Handling
424
+
425
+ The broker implements comprehensive error handling for all external operations, treating all injected dependencies as untrusted:
426
+
427
+ ```typescript
428
+ import { STORE_ERROR_CODES } from '@mcp-abap-adt/interfaces';
429
+
430
+ try {
431
+ const token = await broker.getToken('TRIAL');
432
+ } catch (error: any) {
433
+ // Broker handles errors internally where possible, but critical errors propagate
434
+ console.error('Failed to get token:', error.message);
435
+ }
436
+ ```
437
+
438
+ **Error Categories** (handled by broker with graceful degradation):
439
+
440
+ **1. SessionStore Errors** (reading session files):
441
+ - `STORE_ERROR_CODES.FILE_NOT_FOUND` - Session file missing (logged, tries serviceKeyStore fallback)
442
+ - `STORE_ERROR_CODES.PARSE_ERROR` - Corrupted session file (logged with file path, tries fallback)
443
+ - Write failures when saving tokens (logged and thrown - critical)
444
+
445
+ **2. ServiceKeyStore Errors** (reading service key files):
446
+ - `STORE_ERROR_CODES.FILE_NOT_FOUND` - Service key file missing (logged, continues with session data)
447
+ - `STORE_ERROR_CODES.PARSE_ERROR` - Invalid JSON in service key (logged with file path and cause)
448
+ - `STORE_ERROR_CODES.INVALID_CONFIG` - Missing required fields (logged with missing field names)
449
+ - `STORE_ERROR_CODES.STORAGE_ERROR` - Permission/write errors (logged)
450
+
451
+ **3. TokenProvider Errors** (network operations):
452
+ - Network errors: `ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND` (logged, throws with descriptive message)
453
+ - `VALIDATION_ERROR` - Missing required auth fields (logged with field names, throws)
454
+ - `BROWSER_AUTH_ERROR` - Browser authentication failed or cancelled (logged, throws)
455
+ - `REFRESH_ERROR` - Token refresh failed at UAA server (logged, throws)
456
+
457
+ **Defensive Design Principles:**
458
+ - **All external operations wrapped in try-catch**: Files may be missing/corrupted, network may fail
459
+ - **Graceful degradation**: Store errors trigger fallback mechanisms (serviceKey → session → provider)
460
+ - **Detailed error context**: Logs include file paths, error codes, missing fields for debugging
461
+ - **Fail-fast for critical errors**: Write failures and provider errors throw immediately (cannot recover)
462
+ - **No assumptions about injected dependencies**: All stores/providers treated as potentially unreliable
463
+
464
+ Example error scenarios handled:
465
+ - Session file deleted mid-operation → uses service key
466
+ - Service key has invalid JSON → logs parse error, uses session data
467
+ - Network timeout during token refresh → logs timeout, throws descriptive error
468
+ - File permission denied → logs error with file path, throws
413
469
 
414
470
  ##### `refreshToken(destination: string): Promise<string>`
415
471
 
@@ -596,4 +652,3 @@ Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the co
596
652
  ## License
597
653
 
598
654
  MIT
599
-
@@ -12,9 +12,12 @@ export interface AuthBrokerConfig {
12
12
  sessionStore: ISessionStore;
13
13
  /** Service key store (optional) - stores and retrieves service keys */
14
14
  serviceKeyStore?: IServiceKeyStore;
15
- /** Token provider (optional) - handles token refresh and authentication flows. If not provided, direct UAA HTTP requests will be used when UAA credentials are available */
16
- tokenProvider?: ITokenProvider;
15
+ /** Token provider (required) - handles token refresh and authentication flows through browser-based authorization (e.g., XSUAA provider) */
16
+ tokenProvider: ITokenProvider;
17
17
  }
18
+ /**
19
+ * AuthBroker manages JWT authentication tokens for destinations
20
+ */
18
21
  export declare class AuthBroker {
19
22
  private browser;
20
23
  private logger;
@@ -26,63 +29,52 @@ export declare class AuthBroker {
26
29
  * @param config Configuration object with stores and token provider
27
30
  * - sessionStore: Store for session data (required)
28
31
  * - serviceKeyStore: Store for service keys (optional)
29
- * - tokenProvider: Token provider implementing ITokenProvider interface (optional). If not provided, direct UAA HTTP requests will be used when UAA credentials are available
32
+ * - tokenProvider: Token provider implementing ITokenProvider interface (required) - handles browser-based authorization
30
33
  * @param browser Optional browser name for authentication (chrome, edge, firefox, system, none).
31
34
  * Default: 'system' (system default browser).
32
35
  * Use 'none' to print URL instead of opening browser.
33
36
  * @param logger Optional logger instance implementing ILogger interface. If not provided, uses no-op logger.
34
37
  */
35
38
  constructor(config: AuthBrokerConfig, browser?: string, logger?: ILogger);
36
- /**
37
- * Refresh token using refresh_token grant type (direct UAA HTTP request)
38
- * @param refreshToken Refresh token
39
- * @param authConfig UAA authorization configuration
40
- * @returns Promise that resolves to new tokens
41
- */
42
- private refreshTokenDirect;
43
- /**
44
- * Get token using client_credentials grant type (direct UAA HTTP request)
45
- * @param authConfig UAA authorization configuration
46
- * @returns Promise that resolves to access token
47
- */
48
- private getTokenWithClientCredentials;
49
39
  /**
50
40
  * Get authentication token for destination.
51
- * Implements a three-step flow: Step 0 (initialize), Step 1 (refresh), Step 2 (UAA).
41
+ * Uses tokenProvider for all authentication operations (browser-based authorization).
52
42
  *
53
43
  * **Flow:**
54
44
  * **Step 0: Initialize Session with Token (if needed)**
55
45
  * - Check if session has `authorizationToken` AND UAA credentials
56
46
  * - If both are empty AND serviceKeyStore is available:
57
- * - Try direct UAA request from service key (if UAA credentials available)
58
- * - If failed and tokenProvider available use provider
59
- * - If session has token OR UAA credentials → proceed to Step 1
47
+ * - Get UAA credentials from service key
48
+ * - Use tokenProvider for browser-based authentication
49
+ * - Save token and refresh token to session
50
+ *
51
+ * **Step 1: Token Validation**
52
+ * - If token exists in session, validate it (if provider supports validation)
53
+ * - If valid → return token
54
+ * - If invalid or no token → continue to refresh
60
55
  *
61
- * **Step 1: Refresh Token Flow**
56
+ * **Step 2: Refresh Token Flow**
62
57
  * - Check if refresh token exists in session
63
58
  * - If refresh token exists:
64
- * - Try direct UAA refresh (if UAA credentials in session)
65
- * - If failed and tokenProvider available → use provider
66
- * - If successful → return new token
67
- * - Otherwise → proceed to Step 2
59
+ * - Use tokenProvider to refresh token (browser-based or refresh grant)
60
+ * - Save new token to session
61
+ * - Return new token
62
+ * - Otherwise → proceed to Step 3
68
63
  *
69
- * **Step 2: UAA Credentials Flow**
70
- * - Check if UAA credentials exist in session or service key
71
- * - Try direct UAA client_credentials request (if UAA credentials available)
72
- * - If failed and tokenProvider available → use provider
73
- * - If successful → return new token
74
- * - If all failed → return error
64
+ * **Step 3: New Token Flow**
65
+ * - Get UAA credentials from session or service key
66
+ * - Use tokenProvider for browser-based authentication
67
+ * - Save new token to session
68
+ * - Return new token
75
69
  *
76
70
  * **Important Notes:**
77
- * - If sessionStore contains valid UAA credentials, neither serviceKeyStore nor tokenProvider are required.
78
- * Direct UAA HTTP requests will be used automatically.
79
- * - tokenProvider is only needed when:
80
- * - Initializing session from service key via browser authentication (Step 0)
81
- * - Direct UAA requests fail and fallback to provider is needed
71
+ * - All authentication is handled by tokenProvider (e.g., XSUAA provider)
72
+ * - Provider uses browser-based authorization to ensure proper role assignment
73
+ * - Direct UAA HTTP requests are not used to avoid role assignment issues
82
74
  *
83
75
  * @param destination Destination name (e.g., "TRIAL")
84
76
  * @returns Promise that resolves to JWT token string
85
- * @throws Error if session initialization fails or all authentication methods failed
77
+ * @throws Error if session initialization fails or authentication failed
86
78
  */
87
79
  getToken(destination: string): Promise<string>;
88
80
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAW,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC/G,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAa7C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,YAAY,EAAE,aAAa,CAAC;IAC5B,uEAAuE;IACvE,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,4KAA4K;IAC5K,aAAa,CAAC,EAAE,cAAc,CAAC;CAChC;AAcD,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAA6B;IAElD;;;;;;;;;;OAUG;gBAED,MAAM,EAAE,gBAAgB,EACxB,OAAO,CAAC,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,OAAO;IAgElB;;;;;OAKG;YACW,kBAAkB;IA4ChC;;;;OAIG;YACW,6BAA6B;IA0C3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;IACG,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA0SpD;;;;;OAKG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAuFxD;;;;OAIG;IACG,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA2BvF;;;;OAIG;IACG,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;CAyBlF"}
1
+ {"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAA8C,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC/G,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAa7C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,YAAY,EAAE,aAAa,CAAC;IAC5B,uEAAuE;IACvE,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,4IAA4I;IAC5I,aAAa,EAAE,cAAc,CAAC;CAC/B;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAiB;IAEtC;;;;;;;;;;OAUG;gBAED,MAAM,EAAE,gBAAgB,EACxB,OAAO,CAAC,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,OAAO;IAsElB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACG,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAkWpD;;;;;OAKG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOxD;;;;OAIG;IACG,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA4CvF;;;;OAIG;IACG,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;CA0ClF"}
@@ -2,12 +2,9 @@
2
2
  /**
3
3
  * Main AuthBroker class for managing JWT tokens based on destinations
4
4
  */
5
- var __importDefault = (this && this.__importDefault) || function (mod) {
6
- return (mod && mod.__esModule) ? mod : { "default": mod };
7
- };
8
5
  Object.defineProperty(exports, "__esModule", { value: true });
9
6
  exports.AuthBroker = void 0;
10
- const axios_1 = __importDefault(require("axios"));
7
+ const interfaces_1 = require("@mcp-abap-adt/interfaces");
11
8
  /**
12
9
  * No-op logger implementation for default fallback when logger is not provided
13
10
  */
@@ -17,6 +14,9 @@ const noOpLogger = {
17
14
  warn: () => { },
18
15
  debug: () => { },
19
16
  };
17
+ /**
18
+ * AuthBroker manages JWT authentication tokens for destinations
19
+ */
20
20
  class AuthBroker {
21
21
  browser;
22
22
  logger;
@@ -28,7 +28,7 @@ class AuthBroker {
28
28
  * @param config Configuration object with stores and token provider
29
29
  * - sessionStore: Store for session data (required)
30
30
  * - serviceKeyStore: Store for service keys (optional)
31
- * - tokenProvider: Token provider implementing ITokenProvider interface (optional). If not provided, direct UAA HTTP requests will be used when UAA credentials are available
31
+ * - tokenProvider: Token provider implementing ITokenProvider interface (required) - handles browser-based authorization
32
32
  * @param browser Optional browser name for authentication (chrome, edge, firefox, system, none).
33
33
  * Default: 'system' (system default browser).
34
34
  * Use 'none' to print URL instead of opening browser.
@@ -43,6 +43,10 @@ class AuthBroker {
43
43
  if (!config.sessionStore) {
44
44
  throw new Error('AuthBroker: sessionStore is required');
45
45
  }
46
+ // Validate required tokenProvider
47
+ if (!config.tokenProvider) {
48
+ throw new Error('AuthBroker: tokenProvider is required');
49
+ }
46
50
  // Validate that stores and provider are correctly instantiated (have required methods)
47
51
  const sessionStore = config.sessionStore;
48
52
  const tokenProvider = config.tokenProvider;
@@ -60,13 +64,11 @@ class AuthBroker {
60
64
  if (typeof sessionStore.setConnectionConfig !== 'function') {
61
65
  throw new Error('AuthBroker: sessionStore.setConnectionConfig must be a function');
62
66
  }
63
- // Check tokenProvider methods (if provided)
64
- if (tokenProvider) {
65
- if (typeof tokenProvider.getConnectionConfig !== 'function') {
66
- throw new Error('AuthBroker: tokenProvider.getConnectionConfig must be a function');
67
- }
68
- // validateToken is optional, so we don't check it
67
+ // Check tokenProvider methods (required)
68
+ if (typeof tokenProvider.getConnectionConfig !== 'function') {
69
+ throw new Error('AuthBroker: tokenProvider.getConnectionConfig must be a function');
69
70
  }
71
+ // validateToken is optional, so we don't check it
70
72
  // Check serviceKeyStore methods (if provided)
71
73
  if (serviceKeyStore) {
72
74
  if (typeof serviceKeyStore.getServiceKey !== 'function') {
@@ -86,150 +88,105 @@ class AuthBroker {
86
88
  this.logger = logger || noOpLogger;
87
89
  // Log successful initialization
88
90
  const hasServiceKeyStore = !!this.serviceKeyStore;
89
- const hasTokenProvider = !!this.tokenProvider;
90
- this.logger?.debug(`AuthBroker initialized: sessionStore(ok), serviceKeyStore(${hasServiceKeyStore ? 'ok' : 'none'}), tokenProvider(${hasTokenProvider ? 'ok' : 'none'})`);
91
- }
92
- /**
93
- * Refresh token using refresh_token grant type (direct UAA HTTP request)
94
- * @param refreshToken Refresh token
95
- * @param authConfig UAA authorization configuration
96
- * @returns Promise that resolves to new tokens
97
- */
98
- async refreshTokenDirect(refreshToken, authConfig) {
99
- if (!authConfig.uaaUrl || !authConfig.uaaClientId || !authConfig.uaaClientSecret) {
100
- throw new Error('UAA credentials incomplete: uaaUrl, uaaClientId, and uaaClientSecret are required');
101
- }
102
- const tokenUrl = `${authConfig.uaaUrl}/oauth/token`;
103
- const params = new URLSearchParams();
104
- params.append('grant_type', 'refresh_token');
105
- params.append('refresh_token', refreshToken);
106
- const authString = Buffer.from(`${authConfig.uaaClientId}:${authConfig.uaaClientSecret}`).toString('base64');
107
- try {
108
- const response = await (0, axios_1.default)({
109
- method: 'post',
110
- url: tokenUrl,
111
- headers: {
112
- Authorization: `Basic ${authString}`,
113
- 'Content-Type': 'application/x-www-form-urlencoded',
114
- },
115
- data: params.toString(),
116
- timeout: 30000,
117
- });
118
- if (response.data && response.data.access_token) {
119
- return {
120
- accessToken: response.data.access_token,
121
- refreshToken: response.data.refresh_token || refreshToken,
122
- expiresIn: response.data.expires_in,
123
- };
124
- }
125
- else {
126
- throw new Error('Response does not contain access_token');
127
- }
128
- }
129
- catch (error) {
130
- if (error.response) {
131
- throw new Error(`Token refresh failed (${error.response.status}): ${JSON.stringify(error.response.data)}`);
132
- }
133
- else {
134
- throw new Error(`Token refresh failed: ${error.message}`);
135
- }
136
- }
137
- }
138
- /**
139
- * Get token using client_credentials grant type (direct UAA HTTP request)
140
- * @param authConfig UAA authorization configuration
141
- * @returns Promise that resolves to access token
142
- */
143
- async getTokenWithClientCredentials(authConfig) {
144
- if (!authConfig.uaaUrl || !authConfig.uaaClientId || !authConfig.uaaClientSecret) {
145
- throw new Error('UAA credentials incomplete: uaaUrl, uaaClientId, and uaaClientSecret are required');
146
- }
147
- const tokenUrl = `${authConfig.uaaUrl}/oauth/token`;
148
- const params = new URLSearchParams();
149
- params.append('grant_type', 'client_credentials');
150
- params.append('client_id', authConfig.uaaClientId);
151
- params.append('client_secret', authConfig.uaaClientSecret);
152
- try {
153
- const response = await (0, axios_1.default)({
154
- method: 'post',
155
- url: tokenUrl,
156
- headers: {
157
- 'Content-Type': 'application/x-www-form-urlencoded',
158
- },
159
- data: params.toString(),
160
- timeout: 30000,
161
- });
162
- if (response.data && response.data.access_token) {
163
- return {
164
- accessToken: response.data.access_token,
165
- refreshToken: response.data.refresh_token,
166
- expiresIn: response.data.expires_in,
167
- };
168
- }
169
- else {
170
- throw new Error('Response does not contain access_token');
171
- }
172
- }
173
- catch (error) {
174
- if (error.response) {
175
- throw new Error(`Client credentials authentication failed (${error.response.status}): ${JSON.stringify(error.response.data)}`);
176
- }
177
- else {
178
- throw new Error(`Client credentials authentication failed: ${error.message}`);
179
- }
180
- }
91
+ this.logger?.debug(`AuthBroker initialized: sessionStore(ok), serviceKeyStore(${hasServiceKeyStore ? 'ok' : 'none'}), tokenProvider(ok)`);
181
92
  }
182
93
  /**
183
94
  * Get authentication token for destination.
184
- * Implements a three-step flow: Step 0 (initialize), Step 1 (refresh), Step 2 (UAA).
95
+ * Uses tokenProvider for all authentication operations (browser-based authorization).
185
96
  *
186
97
  * **Flow:**
187
98
  * **Step 0: Initialize Session with Token (if needed)**
188
99
  * - Check if session has `authorizationToken` AND UAA credentials
189
100
  * - If both are empty AND serviceKeyStore is available:
190
- * - Try direct UAA request from service key (if UAA credentials available)
191
- * - If failed and tokenProvider available use provider
192
- * - If session has token OR UAA credentials → proceed to Step 1
101
+ * - Get UAA credentials from service key
102
+ * - Use tokenProvider for browser-based authentication
103
+ * - Save token and refresh token to session
104
+ *
105
+ * **Step 1: Token Validation**
106
+ * - If token exists in session, validate it (if provider supports validation)
107
+ * - If valid → return token
108
+ * - If invalid or no token → continue to refresh
193
109
  *
194
- * **Step 1: Refresh Token Flow**
110
+ * **Step 2: Refresh Token Flow**
195
111
  * - Check if refresh token exists in session
196
112
  * - If refresh token exists:
197
- * - Try direct UAA refresh (if UAA credentials in session)
198
- * - If failed and tokenProvider available → use provider
199
- * - If successful → return new token
200
- * - Otherwise → proceed to Step 2
113
+ * - Use tokenProvider to refresh token (browser-based or refresh grant)
114
+ * - Save new token to session
115
+ * - Return new token
116
+ * - Otherwise → proceed to Step 3
201
117
  *
202
- * **Step 2: UAA Credentials Flow**
203
- * - Check if UAA credentials exist in session or service key
204
- * - Try direct UAA client_credentials request (if UAA credentials available)
205
- * - If failed and tokenProvider available → use provider
206
- * - If successful → return new token
207
- * - If all failed → return error
118
+ * **Step 3: New Token Flow**
119
+ * - Get UAA credentials from session or service key
120
+ * - Use tokenProvider for browser-based authentication
121
+ * - Save new token to session
122
+ * - Return new token
208
123
  *
209
124
  * **Important Notes:**
210
- * - If sessionStore contains valid UAA credentials, neither serviceKeyStore nor tokenProvider are required.
211
- * Direct UAA HTTP requests will be used automatically.
212
- * - tokenProvider is only needed when:
213
- * - Initializing session from service key via browser authentication (Step 0)
214
- * - Direct UAA requests fail and fallback to provider is needed
125
+ * - All authentication is handled by tokenProvider (e.g., XSUAA provider)
126
+ * - Provider uses browser-based authorization to ensure proper role assignment
127
+ * - Direct UAA HTTP requests are not used to avoid role assignment issues
215
128
  *
216
129
  * @param destination Destination name (e.g., "TRIAL")
217
130
  * @returns Promise that resolves to JWT token string
218
- * @throws Error if session initialization fails or all authentication methods failed
131
+ * @throws Error if session initialization fails or authentication failed
219
132
  */
220
133
  async getToken(destination) {
221
134
  this.logger?.debug(`Getting token for destination: ${destination}`);
222
135
  // Step 0: Initialize Session with Token (if needed)
223
- const connConfig = await this.sessionStore.getConnectionConfig(destination);
224
- const authConfig = await this.sessionStore.getAuthorizationConfig(destination);
136
+ let connConfig = null;
137
+ let authConfig = null;
138
+ try {
139
+ connConfig = await this.sessionStore.getConnectionConfig(destination);
140
+ }
141
+ catch (error) {
142
+ // Handle typed store errors from session store
143
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
144
+ this.logger?.debug(`Session file not found for ${destination}: ${error.filePath || 'unknown path'}`);
145
+ }
146
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
147
+ this.logger?.warn(`Failed to parse session file for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
148
+ }
149
+ else {
150
+ this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${error.message}`);
151
+ }
152
+ }
153
+ try {
154
+ authConfig = await this.sessionStore.getAuthorizationConfig(destination);
155
+ }
156
+ catch (error) {
157
+ // Handle typed store errors from session store
158
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
159
+ this.logger?.debug(`Session file not found for ${destination}: ${error.filePath || 'unknown path'}`);
160
+ }
161
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
162
+ this.logger?.warn(`Failed to parse session file for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
163
+ }
164
+ else {
165
+ this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${error.message}`);
166
+ }
167
+ }
225
168
  // Check if session has serviceUrl (required)
226
169
  // If not in session, try to get it from serviceKeyStore
227
170
  let serviceUrl = connConfig?.serviceUrl;
228
171
  if (!serviceUrl && this.serviceKeyStore) {
229
- const serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
230
- serviceUrl = serviceKeyConnConfig?.serviceUrl;
231
- if (serviceUrl) {
232
- this.logger?.debug(`serviceUrl not in session for ${destination}, found in serviceKeyStore`);
172
+ try {
173
+ const serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
174
+ serviceUrl = serviceKeyConnConfig?.serviceUrl;
175
+ if (serviceUrl) {
176
+ this.logger?.debug(`serviceUrl not in session for ${destination}, found in serviceKeyStore`);
177
+ }
178
+ }
179
+ catch (error) {
180
+ // Handle typed store errors
181
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
182
+ this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
183
+ }
184
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
185
+ this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
186
+ }
187
+ else {
188
+ this.logger?.warn(`Failed to get serviceUrl from service key store for ${destination}: ${error.message}`);
189
+ }
233
190
  }
234
191
  }
235
192
  if (!serviceUrl) {
@@ -256,32 +213,31 @@ class AuthBroker {
256
213
  this.logger?.error(`Step 0: Service key for ${destination} missing UAA credentials`);
257
214
  throw new Error(`Service key for destination "${destination}" does not contain UAA credentials`);
258
215
  }
259
- // Try direct UAA request first if UAA credentials are available in service key
216
+ // Use tokenProvider for browser-based authentication
217
+ this.logger?.debug(`Step 0: Authenticating via provider (browser) for ${destination} using service key UAA credentials`);
260
218
  let tokenResult;
261
219
  try {
262
- // Use direct UAA HTTP request (preferred when UAA credentials are available)
263
- this.logger?.debug(`Step 0: Authenticating via direct UAA request for ${destination} using service key UAA credentials`);
264
- const uaaResult = await this.getTokenWithClientCredentials(serviceKeyAuthConfig);
265
- tokenResult = {
266
- connectionConfig: {
267
- authorizationToken: uaaResult.accessToken,
268
- },
269
- refreshToken: uaaResult.refreshToken,
270
- };
271
- }
272
- catch (directError) {
273
- this.logger?.debug(`Step 0: Direct UAA request failed for ${destination}: ${directError.message}, trying provider`);
274
- // If direct UAA failed and we have provider, try provider
275
- if (this.tokenProvider) {
276
- this.logger?.debug(`Step 0: Authenticating via provider for ${destination} using service key UAA credentials`);
277
- tokenResult = await this.tokenProvider.getConnectionConfig(serviceKeyAuthConfig, {
278
- browser: this.browser,
279
- logger: this.logger,
280
- });
220
+ tokenResult = await this.tokenProvider.getConnectionConfig(serviceKeyAuthConfig, {
221
+ browser: this.browser,
222
+ logger: this.logger,
223
+ });
224
+ }
225
+ catch (error) {
226
+ // Handle provider errors (network, auth, validation)
227
+ if (error.code === 'VALIDATION_ERROR') {
228
+ this.logger?.error(`Step 0: Provider validation error for ${destination}: missing ${error.missingFields?.join(', ') || 'required fields'}`);
229
+ throw new Error(`Cannot initialize session for destination "${destination}": provider validation failed - missing ${error.missingFields?.join(', ') || 'required fields'}`);
230
+ }
231
+ else if (error.code === 'BROWSER_AUTH_ERROR') {
232
+ this.logger?.error(`Step 0: Browser authentication failed for ${destination}: ${error.message}`);
233
+ throw new Error(`Cannot initialize session for destination "${destination}": browser authentication failed - ${error.message}`);
281
234
  }
282
- else {
283
- throw directError; // No provider, re-throw direct error
235
+ else if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
236
+ this.logger?.error(`Step 0: Network error for ${destination}: ${error.code}`);
237
+ throw new Error(`Cannot initialize session for destination "${destination}": network error - cannot reach authentication server (${error.code})`);
284
238
  }
239
+ this.logger?.error(`Step 0: Provider error for ${destination}: ${error.message}`);
240
+ throw new Error(`Cannot initialize session for destination "${destination}": provider error - ${error.message}`);
285
241
  }
286
242
  const tokenLength = tokenResult.connectionConfig.authorizationToken?.length || 0;
287
243
  this.logger?.info(`Step 0: Token initialized for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
@@ -292,31 +248,61 @@ class AuthBroker {
292
248
  serviceUrl: tokenResult.connectionConfig.serviceUrl || serviceKeyConnConfig?.serviceUrl || serviceUrl,
293
249
  };
294
250
  // Save token and UAA credentials to session
295
- await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
296
- await this.sessionStore.setAuthorizationConfig(destination, {
297
- ...serviceKeyAuthConfig,
298
- refreshToken: tokenResult.refreshToken || serviceKeyAuthConfig.refreshToken,
299
- });
251
+ try {
252
+ await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
253
+ }
254
+ catch (error) {
255
+ this.logger?.error(`Step 0: Failed to save connection config to session for ${destination}: ${error.message}`);
256
+ throw new Error(`Failed to save connection config for destination "${destination}": ${error.message}`);
257
+ }
258
+ try {
259
+ await this.sessionStore.setAuthorizationConfig(destination, {
260
+ ...serviceKeyAuthConfig,
261
+ refreshToken: tokenResult.refreshToken || serviceKeyAuthConfig.refreshToken,
262
+ });
263
+ }
264
+ catch (error) {
265
+ this.logger?.error(`Step 0: Failed to save authorization config to session for ${destination}: ${error.message}`);
266
+ throw new Error(`Failed to save authorization config for destination "${destination}": ${error.message}`);
267
+ }
300
268
  return tokenResult.connectionConfig.authorizationToken;
301
269
  }
302
270
  catch (error) {
271
+ // Handle typed store errors
272
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
273
+ this.logger?.error(`Step 0: Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
274
+ throw new Error(`Cannot initialize session for destination "${destination}": service key file not found`);
275
+ }
276
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
277
+ this.logger?.error(`Step 0: Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
278
+ throw new Error(`Cannot initialize session for destination "${destination}": service key parsing failed - ${error.message}`);
279
+ }
280
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.INVALID_CONFIG) {
281
+ this.logger?.error(`Step 0: Invalid service key config for ${destination}: missing fields ${error.missingFields?.join(', ') || 'unknown'}`);
282
+ throw new Error(`Cannot initialize session for destination "${destination}": invalid service key - missing ${error.missingFields?.join(', ') || 'required fields'}`);
283
+ }
303
284
  this.logger?.error(`Step 0: Failed to initialize session for ${destination}: ${error.message}`);
304
- const errorMessage = `Cannot initialize session for destination "${destination}": ${error.message}. ` +
305
- `Ensure serviceKeyStore contains valid service key with UAA credentials${this.tokenProvider ? ' or provide tokenProvider for alternative authentication' : ''}.`;
306
- throw new Error(errorMessage);
285
+ throw new Error(`Cannot initialize session for destination "${destination}": ${error.message}`);
307
286
  }
308
287
  }
309
288
  // If we have a token, validate it first
310
- if (hasToken && connConfig.authorizationToken) {
289
+ if (hasToken && connConfig?.authorizationToken) {
311
290
  this.logger?.debug(`Step 0: Token found for ${destination}, validating`);
312
291
  // Validate token if provider supports validation and we have service URL
313
292
  if (this.tokenProvider?.validateToken && serviceUrl) {
314
- const isValid = await this.tokenProvider.validateToken(connConfig.authorizationToken, serviceUrl);
315
- if (isValid) {
316
- this.logger?.info(`Step 0: Token valid for ${destination}: token(${connConfig.authorizationToken.length} chars)`);
317
- return connConfig.authorizationToken;
293
+ try {
294
+ const isValid = await this.tokenProvider.validateToken(connConfig.authorizationToken, serviceUrl);
295
+ if (isValid) {
296
+ this.logger?.info(`Step 0: Token valid for ${destination}: token(${connConfig.authorizationToken.length} chars)`);
297
+ return connConfig.authorizationToken;
298
+ }
299
+ this.logger?.debug(`Step 0: Token invalid for ${destination}, continuing to refresh`);
300
+ }
301
+ catch (error) {
302
+ // Validation failed due to network/server error - log and continue to refresh
303
+ this.logger?.warn(`Step 0: Token validation failed for ${destination} (network error): ${error.message}. Continuing to refresh.`);
304
+ // Don't throw - continue to refresh flow
318
305
  }
319
- this.logger?.debug(`Step 0: Token invalid for ${destination}, continuing to refresh`);
320
306
  }
321
307
  else {
322
308
  // No service URL or provider doesn't support validation - just return token
@@ -324,165 +310,167 @@ class AuthBroker {
324
310
  return connConfig.authorizationToken;
325
311
  }
326
312
  }
327
- // Step 1: Refresh Token Flow
328
- this.logger?.debug(`Step 1: Checking refresh token for ${destination}`);
313
+ // Step 2: Refresh Token Flow
314
+ this.logger?.debug(`Step 2: Attempting token refresh for ${destination}`);
315
+ // Get UAA credentials from session or service key
316
+ let serviceKeyAuthConfig = null;
317
+ if (!authConfig && this.serviceKeyStore) {
318
+ try {
319
+ serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
320
+ }
321
+ catch (error) {
322
+ // Handle typed store errors
323
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
324
+ this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
325
+ }
326
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
327
+ this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
328
+ }
329
+ else {
330
+ this.logger?.warn(`Failed to get UAA credentials from service key store for ${destination}: ${error.message}`);
331
+ }
332
+ }
333
+ }
334
+ const uaaCredentials = authConfig || serviceKeyAuthConfig;
335
+ if (!uaaCredentials || !uaaCredentials.uaaUrl || !uaaCredentials.uaaClientId || !uaaCredentials.uaaClientSecret) {
336
+ const errorMessage = `Step 2: UAA credentials not found for ${destination}. ` +
337
+ `Session has no UAA credentials${this.serviceKeyStore ? ' and serviceKeyStore has no UAA credentials' : ' and serviceKeyStore is not available'}.`;
338
+ this.logger?.error(errorMessage);
339
+ throw new Error(errorMessage);
340
+ }
341
+ // Try refresh from session first (if refresh token exists)
329
342
  const refreshToken = authConfig?.refreshToken;
330
343
  if (refreshToken) {
331
344
  try {
332
- this.logger?.debug(`Step 1: Trying refresh token flow for ${destination}`);
333
- // Get UAA credentials from session or service key
334
- const uaaCredentials = authConfig || (this.serviceKeyStore ? await this.serviceKeyStore.getAuthorizationConfig(destination) : null);
335
- if (!uaaCredentials || !uaaCredentials.uaaUrl || !uaaCredentials.uaaClientId || !uaaCredentials.uaaClientSecret) {
336
- throw new Error('UAA credentials not found in session and serviceKeyStore not available');
337
- }
345
+ this.logger?.debug(`Step 2a: Trying refreshTokenFromSession for ${destination}`);
346
+ const authConfigWithRefresh = { ...uaaCredentials, refreshToken };
338
347
  let tokenResult;
339
- // Try direct UAA request if UAA credentials are available
340
- if (uaaCredentials.uaaUrl && uaaCredentials.uaaClientId && uaaCredentials.uaaClientSecret) {
341
- try {
342
- this.logger?.debug(`Step 1: Trying direct UAA refresh for ${destination}`);
343
- const uaaResult = await this.refreshTokenDirect(refreshToken, uaaCredentials);
344
- tokenResult = {
345
- connectionConfig: {
346
- authorizationToken: uaaResult.accessToken,
347
- },
348
- refreshToken: uaaResult.refreshToken,
349
- };
350
- this.logger?.debug(`Step 1: Direct UAA refresh succeeded for ${destination}`);
351
- }
352
- catch (directError) {
353
- this.logger?.debug(`Step 1: Direct UAA refresh failed for ${destination}: ${directError.message}, trying provider`);
354
- // If direct UAA failed and we have provider, try provider
355
- if (this.tokenProvider) {
356
- const authConfigWithRefresh = { ...uaaCredentials, refreshToken };
357
- tokenResult = await this.tokenProvider.getConnectionConfig(authConfigWithRefresh, {
358
- browser: this.browser,
359
- logger: this.logger,
360
- });
361
- }
362
- else {
363
- throw directError; // No provider, re-throw direct error
364
- }
365
- }
366
- }
367
- else if (this.tokenProvider) {
368
- // No UAA credentials but have provider
369
- const authConfigWithRefresh = { ...uaaCredentials, refreshToken };
370
- tokenResult = await this.tokenProvider.getConnectionConfig(authConfigWithRefresh, {
348
+ try {
349
+ tokenResult = await this.tokenProvider.refreshTokenFromSession(authConfigWithRefresh, {
371
350
  browser: this.browser,
372
351
  logger: this.logger,
373
352
  });
374
353
  }
375
- else {
376
- throw new Error('UAA credentials incomplete and tokenProvider not available');
354
+ catch (providerError) {
355
+ // Handle provider network/auth errors
356
+ if (providerError.code === 'ECONNREFUSED' || providerError.code === 'ETIMEDOUT' || providerError.code === 'ENOTFOUND') {
357
+ this.logger?.debug(`Step 2a: Network error during refreshTokenFromSession for ${destination}: ${providerError.code}. Trying refreshTokenFromServiceKey`);
358
+ throw providerError; // Re-throw to trigger fallback to Step 2b
359
+ }
360
+ throw providerError; // Re-throw other errors
377
361
  }
378
362
  const tokenLength = tokenResult.connectionConfig.authorizationToken?.length || 0;
379
- this.logger?.info(`Step 1: Token refreshed for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
380
- // Get serviceUrl from session or service key (use the one we already have from the beginning of the method)
381
- const finalServiceUrl = tokenResult.connectionConfig.serviceUrl ||
382
- serviceUrl ||
383
- (this.serviceKeyStore ? (await this.serviceKeyStore.getConnectionConfig(destination))?.serviceUrl : undefined);
363
+ this.logger?.info(`Step 2a: Token refreshed from session for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
364
+ // Get serviceUrl from session or service key
365
+ let serviceKeyServiceUrl;
366
+ if (this.serviceKeyStore) {
367
+ try {
368
+ const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
369
+ serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
370
+ }
371
+ catch (error) {
372
+ this.logger?.debug(`Could not get serviceUrl from service key store: ${error.message}`);
373
+ }
374
+ }
375
+ const finalServiceUrl = tokenResult.connectionConfig.serviceUrl || serviceUrl || serviceKeyServiceUrl;
384
376
  const connectionConfigWithServiceUrl = {
385
377
  ...tokenResult.connectionConfig,
386
378
  serviceUrl: finalServiceUrl,
387
379
  };
388
380
  // Update session with new token
389
- await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
381
+ try {
382
+ await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
383
+ }
384
+ catch (error) {
385
+ this.logger?.error(`Step 2a: Failed to save connection config to session for ${destination}: ${error.message}`);
386
+ throw new Error(`Failed to save connection config for destination "${destination}": ${error.message}`);
387
+ }
390
388
  if (tokenResult.refreshToken) {
391
- await this.sessionStore.setAuthorizationConfig(destination, {
392
- ...uaaCredentials,
393
- refreshToken: tokenResult.refreshToken,
394
- });
389
+ try {
390
+ await this.sessionStore.setAuthorizationConfig(destination, {
391
+ ...uaaCredentials,
392
+ refreshToken: tokenResult.refreshToken,
393
+ });
394
+ }
395
+ catch (error) {
396
+ this.logger?.error(`Step 2a: Failed to save authorization config to session for ${destination}: ${error.message}`);
397
+ throw new Error(`Failed to save authorization config for destination "${destination}": ${error.message}`);
398
+ }
395
399
  }
396
400
  return tokenResult.connectionConfig.authorizationToken;
397
401
  }
398
402
  catch (error) {
399
- this.logger?.debug(`Step 1: Refresh token flow failed for ${destination}: ${error.message}, trying Step 2`);
400
- // Continue to Step 2
403
+ this.logger?.debug(`Step 2a: refreshTokenFromSession failed for ${destination}: ${error.message}, trying refreshTokenFromServiceKey`);
404
+ // Continue to try service key refresh
401
405
  }
402
406
  }
403
407
  else {
404
- this.logger?.debug(`Step 1: No refresh token found for ${destination}, proceeding to Step 2`);
405
- }
406
- // Step 2: UAA Credentials Flow
407
- this.logger?.debug(`Step 2: Checking UAA credentials for ${destination}`);
408
- // Get UAA credentials from session or service key
409
- const uaaCredentials = authConfig || (this.serviceKeyStore ? await this.serviceKeyStore.getAuthorizationConfig(destination) : null);
410
- if (!uaaCredentials || !uaaCredentials.uaaUrl || !uaaCredentials.uaaClientId || !uaaCredentials.uaaClientSecret) {
411
- const errorMessage = `Step 2: UAA credentials not found for ${destination}. ` +
412
- `Session has no UAA credentials${this.serviceKeyStore ? ' and serviceKeyStore has no UAA credentials' : ' and serviceKeyStore is not available'}.`;
413
- this.logger?.error(errorMessage);
414
- throw new Error(errorMessage);
408
+ this.logger?.debug(`Step 2a: No refresh token in session for ${destination}, skipping to service key refresh`);
415
409
  }
410
+ // Try refresh from service key (browser authentication)
416
411
  try {
417
- this.logger?.debug(`Step 2: Trying UAA (client_credentials) flow for ${destination}`);
418
- let tokenResult;
419
- // Try direct UAA request first if UAA credentials are available
420
- if (uaaCredentials.uaaUrl && uaaCredentials.uaaClientId && uaaCredentials.uaaClientSecret) {
412
+ this.logger?.debug(`Step 2b: Trying refreshTokenFromServiceKey for ${destination}`);
413
+ const tokenResult = await this.tokenProvider.refreshTokenFromServiceKey(uaaCredentials, {
414
+ browser: this.browser,
415
+ logger: this.logger,
416
+ });
417
+ const tokenLength = tokenResult.connectionConfig.authorizationToken?.length || 0;
418
+ this.logger?.info(`Step 2b: Token refreshed from service key for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
419
+ // Get serviceUrl from session or service key
420
+ let serviceKeyServiceUrl;
421
+ if (this.serviceKeyStore) {
421
422
  try {
422
- this.logger?.debug(`Step 2: Trying direct UAA client_credentials for ${destination}`);
423
- const uaaResult = await this.getTokenWithClientCredentials(uaaCredentials);
424
- tokenResult = {
425
- connectionConfig: {
426
- authorizationToken: uaaResult.accessToken,
427
- },
428
- refreshToken: uaaResult.refreshToken,
429
- };
430
- this.logger?.debug(`Step 2: Direct UAA client_credentials succeeded for ${destination}`);
431
- }
432
- catch (directError) {
433
- this.logger?.debug(`Step 2: Direct UAA client_credentials failed for ${destination}: ${directError.message}, trying provider`);
434
- // If direct UAA failed and we have provider, try provider
435
- if (this.tokenProvider) {
436
- const authConfigWithoutRefresh = { ...uaaCredentials, refreshToken: undefined };
437
- tokenResult = await this.tokenProvider.getConnectionConfig(authConfigWithoutRefresh, {
438
- browser: this.browser,
439
- logger: this.logger,
440
- });
441
- }
442
- else {
443
- throw directError; // No provider, re-throw direct error
444
- }
423
+ const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
424
+ serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
425
+ }
426
+ catch (error) {
427
+ this.logger?.debug(`Could not get serviceUrl from service key store: ${error.message}`);
445
428
  }
446
429
  }
447
- else if (this.tokenProvider) {
448
- // No UAA credentials but have provider
449
- const authConfigWithoutRefresh = { ...uaaCredentials, refreshToken: undefined };
450
- tokenResult = await this.tokenProvider.getConnectionConfig(authConfigWithoutRefresh, {
451
- browser: this.browser,
452
- logger: this.logger,
453
- });
454
- }
455
- else {
456
- throw new Error('UAA credentials incomplete and tokenProvider not available');
457
- }
458
- const tokenLength = tokenResult.connectionConfig.authorizationToken?.length || 0;
459
- this.logger?.info(`Step 2: Token obtained via UAA for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
460
- // Get serviceUrl from session or service key (use the one we already have from the beginning of the method)
461
- const finalServiceUrl = tokenResult.connectionConfig.serviceUrl ||
462
- serviceUrl ||
463
- (this.serviceKeyStore ? (await this.serviceKeyStore.getConnectionConfig(destination))?.serviceUrl : undefined);
430
+ const finalServiceUrl = tokenResult.connectionConfig.serviceUrl || serviceUrl || serviceKeyServiceUrl;
464
431
  const connectionConfigWithServiceUrl = {
465
432
  ...tokenResult.connectionConfig,
466
433
  serviceUrl: finalServiceUrl,
467
434
  };
468
435
  // Update session with new token
469
- await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
436
+ try {
437
+ await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
438
+ }
439
+ catch (error) {
440
+ this.logger?.error(`Step 2b: Failed to save connection config to session for ${destination}: ${error.message}`);
441
+ throw new Error(`Failed to save connection config for destination "${destination}": ${error.message}`);
442
+ }
470
443
  if (tokenResult.refreshToken) {
471
- await this.sessionStore.setAuthorizationConfig(destination, {
472
- ...uaaCredentials,
473
- refreshToken: tokenResult.refreshToken,
474
- });
444
+ try {
445
+ await this.sessionStore.setAuthorizationConfig(destination, {
446
+ ...uaaCredentials,
447
+ refreshToken: tokenResult.refreshToken,
448
+ });
449
+ }
450
+ catch (error) {
451
+ this.logger?.error(`Step 2b: Failed to save authorization config to session for ${destination}: ${error.message}`);
452
+ throw new Error(`Failed to save authorization config for destination "${destination}": ${error.message}`);
453
+ }
475
454
  }
476
455
  return tokenResult.connectionConfig.authorizationToken;
477
456
  }
478
457
  catch (error) {
479
- this.logger?.error(`Step 2: UAA flow failed for ${destination}: ${error.message}`);
480
- // If we have serviceKeyStore, we already tried it, so throw error
481
- const errorMessage = `All authentication methods failed for destination "${destination}". ` +
482
- `Step 1 (refresh token): ${refreshToken ? 'failed' : 'not available'}. ` +
483
- `Step 2 (UAA credentials): failed (${error.message}).`;
484
- this.logger?.error(errorMessage);
485
- throw new Error(errorMessage);
458
+ this.logger?.error(`Step 2b: refreshTokenFromServiceKey failed for ${destination}: ${error.message}`);
459
+ // Determine error cause and throw meaningful error
460
+ if (error.code === 'VALIDATION_ERROR') {
461
+ throw new Error(`Token refresh failed: Missing required fields in authConfig - ${error.missingFields?.join(', ')}`);
462
+ }
463
+ else if (error.code === 'BROWSER_AUTH_ERROR') {
464
+ throw new Error(`Token refresh failed: Browser authentication failed or was cancelled - ${error.message}`);
465
+ }
466
+ else if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
467
+ throw new Error(`Token refresh failed: Network error - ${error.code}: Cannot reach authentication server`);
468
+ }
469
+ else if (error.code === 'SERVICE_KEY_ERROR') {
470
+ throw new Error(`Token refresh failed: Service key not found or invalid for ${destination}`);
471
+ }
472
+ // Generic error
473
+ throw new Error(`Token refresh failed for ${destination}: ${error.message}`);
486
474
  }
487
475
  }
488
476
  /**
@@ -493,81 +481,8 @@ class AuthBroker {
493
481
  */
494
482
  async refreshToken(destination) {
495
483
  this.logger?.debug(`Force refreshing token for destination: ${destination}`);
496
- // Get authorization config from session or service key
497
- const sessionAuthConfig = await this.sessionStore.getAuthorizationConfig(destination);
498
- const serviceKeyAuthConfig = this.serviceKeyStore
499
- ? await this.serviceKeyStore.getAuthorizationConfig(destination)
500
- : null;
501
- const authConfig = sessionAuthConfig || serviceKeyAuthConfig;
502
- if (!authConfig) {
503
- this.logger?.error(`Authorization config not found for ${destination}`);
504
- throw new Error(`Authorization config not found for destination "${destination}". ` +
505
- `Session has no UAA credentials${this.serviceKeyStore ? ' and serviceKeyStore has no UAA credentials' : ' and serviceKeyStore is not available'}.`);
506
- }
507
- // Get refresh token from session or service key
508
- const refreshToken = sessionAuthConfig?.refreshToken || authConfig.refreshToken;
509
- this.logger?.debug(`Refresh token check for ${destination}: hasRefreshToken(${!!refreshToken})`);
510
- let tokenResult;
511
- // Try direct UAA request if UAA credentials are available
512
- if (authConfig.uaaUrl && authConfig.uaaClientId && authConfig.uaaClientSecret && refreshToken) {
513
- try {
514
- this.logger?.debug(`Trying direct UAA refresh for ${destination}`);
515
- const uaaResult = await this.refreshTokenDirect(refreshToken, authConfig);
516
- tokenResult = {
517
- connectionConfig: {
518
- authorizationToken: uaaResult.accessToken,
519
- },
520
- refreshToken: uaaResult.refreshToken,
521
- };
522
- }
523
- catch (directError) {
524
- this.logger?.debug(`Direct UAA refresh failed for ${destination}: ${directError.message}, trying provider`);
525
- // If direct UAA failed and we have provider, try provider
526
- if (this.tokenProvider) {
527
- const authConfigWithRefresh = { ...authConfig, refreshToken };
528
- tokenResult = await this.tokenProvider.getConnectionConfig(authConfigWithRefresh, {
529
- browser: this.browser,
530
- logger: this.logger,
531
- });
532
- }
533
- else {
534
- throw directError; // No provider, re-throw direct error
535
- }
536
- }
537
- }
538
- else if (this.tokenProvider) {
539
- // No UAA credentials or refresh token, but have provider
540
- const authConfigWithRefresh = { ...authConfig, refreshToken };
541
- tokenResult = await this.tokenProvider.getConnectionConfig(authConfigWithRefresh, {
542
- browser: this.browser,
543
- logger: this.logger,
544
- });
545
- }
546
- else {
547
- throw new Error('UAA credentials incomplete and tokenProvider not available');
548
- }
549
- const tokenLength = tokenResult.connectionConfig.authorizationToken?.length || 0;
550
- this.logger?.info(`Token refreshed for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
551
- // Get serviceUrl from session or service key
552
- const connConfig = await this.sessionStore.getConnectionConfig(destination);
553
- const serviceKeyConnConfig = this.serviceKeyStore
554
- ? await this.serviceKeyStore.getConnectionConfig(destination)
555
- : null;
556
- const connectionConfigWithServiceUrl = {
557
- ...tokenResult.connectionConfig,
558
- serviceUrl: tokenResult.connectionConfig.serviceUrl ||
559
- connConfig?.serviceUrl ||
560
- serviceKeyConnConfig?.serviceUrl,
561
- };
562
- // Update or create session with new token (stores handle creation if session doesn't exist)
563
- await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
564
- if (tokenResult.refreshToken) {
565
- await this.sessionStore.setAuthorizationConfig(destination, {
566
- ...authConfig,
567
- refreshToken: tokenResult.refreshToken,
568
- });
569
- }
570
- return tokenResult.connectionConfig.authorizationToken;
484
+ // Call getToken to trigger full refresh flow
485
+ return this.getToken(destination);
571
486
  }
572
487
  /**
573
488
  * Get authorization configuration for destination
@@ -578,7 +493,13 @@ class AuthBroker {
578
493
  this.logger?.debug(`Getting authorization config for ${destination}`);
579
494
  // Try session store first (has tokens)
580
495
  this.logger?.debug(`Checking session store for authorization config: ${destination}`);
581
- const sessionAuthConfig = await this.sessionStore.getAuthorizationConfig(destination);
496
+ let sessionAuthConfig = null;
497
+ try {
498
+ sessionAuthConfig = await this.sessionStore.getAuthorizationConfig(destination);
499
+ }
500
+ catch (error) {
501
+ this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${error.message}`);
502
+ }
582
503
  if (sessionAuthConfig) {
583
504
  this.logger?.debug(`Authorization config from session for ${destination}: hasUaaUrl(${!!sessionAuthConfig.uaaUrl}), hasRefreshToken(${!!sessionAuthConfig.refreshToken})`);
584
505
  return sessionAuthConfig;
@@ -586,7 +507,22 @@ class AuthBroker {
586
507
  // Fall back to service key store (has UAA credentials) if available
587
508
  if (this.serviceKeyStore) {
588
509
  this.logger?.debug(`Checking service key store for authorization config: ${destination}`);
589
- const serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
510
+ let serviceKeyAuthConfig = null;
511
+ try {
512
+ serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
513
+ }
514
+ catch (error) {
515
+ // Handle typed store errors
516
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
517
+ this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
518
+ }
519
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
520
+ this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
521
+ }
522
+ else {
523
+ this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${error.message}`);
524
+ }
525
+ }
590
526
  if (serviceKeyAuthConfig) {
591
527
  this.logger?.debug(`Authorization config from service key for ${destination}: hasUaaUrl(${!!serviceKeyAuthConfig.uaaUrl})`);
592
528
  return serviceKeyAuthConfig;
@@ -606,14 +542,35 @@ class AuthBroker {
606
542
  async getConnectionConfig(destination) {
607
543
  this.logger?.debug(`Getting connection config for ${destination}`);
608
544
  // Try session store first (has tokens and URLs)
609
- const sessionConnConfig = await this.sessionStore.getConnectionConfig(destination);
545
+ let sessionConnConfig = null;
546
+ try {
547
+ sessionConnConfig = await this.sessionStore.getConnectionConfig(destination);
548
+ }
549
+ catch (error) {
550
+ this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${error.message}`);
551
+ }
610
552
  if (sessionConnConfig) {
611
553
  this.logger?.debug(`Connection config from session for ${destination}: token(${sessionConnConfig.authorizationToken?.length || 0} chars), serviceUrl(${sessionConnConfig.serviceUrl ? 'yes' : 'no'})`);
612
554
  return sessionConnConfig;
613
555
  }
614
556
  // Fall back to service key store (has URLs but no tokens) if available
615
557
  if (this.serviceKeyStore) {
616
- const serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
558
+ let serviceKeyConnConfig = null;
559
+ try {
560
+ serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
561
+ }
562
+ catch (error) {
563
+ // Handle typed store errors
564
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
565
+ this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
566
+ }
567
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
568
+ this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
569
+ }
570
+ else {
571
+ this.logger?.warn(`Failed to get connection config from service key store for ${destination}: ${error.message}`);
572
+ }
573
+ }
617
574
  if (serviceKeyConnConfig) {
618
575
  this.logger?.debug(`Connection config from service key for ${destination}: serviceUrl(${serviceKeyConnConfig.serviceUrl ? 'yes' : 'no'}), token(none)`);
619
576
  return serviceKeyConnConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-broker",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "JWT authentication broker for MCP ABAP ADT - manages tokens based on destination headers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -51,12 +51,12 @@
51
51
  "node": ">=18.0.0"
52
52
  },
53
53
  "dependencies": {
54
- "@mcp-abap-adt/interfaces": "^0.1.16",
54
+ "@mcp-abap-adt/interfaces": "^0.2.3",
55
55
  "axios": "^1.13.2"
56
56
  },
57
57
  "devDependencies": {
58
- "@mcp-abap-adt/auth-providers": "^0.1.4",
59
- "@mcp-abap-adt/auth-stores": "^0.2.1",
58
+ "@mcp-abap-adt/auth-providers": "^0.2.0",
59
+ "@mcp-abap-adt/auth-stores": "^0.2.5",
60
60
  "@types/express": "^5.0.5",
61
61
  "@types/jest": "^30.0.0",
62
62
  "@types/js-yaml": "^4.0.9",