@mcp-abap-adt/auth-broker 0.2.3 → 0.2.5

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