@mcp-abap-adt/auth-broker 0.2.8 → 0.2.10
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 +42 -0
- package/README.md +32 -0
- package/dist/AuthBroker.d.ts +53 -3
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +464 -339
- package/dist/__tests__/helpers/configHelpers.d.ts.map +1 -1
- package/dist/__tests__/helpers/configHelpers.js +4 -5
- package/dist/__tests__/helpers/testLogger.d.ts.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/providers/ITokenProvider.d.ts +2 -2
- package/dist/providers/ITokenProvider.d.ts.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.d.ts.map +1 -1
- package/dist/stores/interfaces.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -4
package/dist/AuthBroker.js
CHANGED
|
@@ -14,6 +14,23 @@ const noOpLogger = {
|
|
|
14
14
|
warn: () => { },
|
|
15
15
|
debug: () => { },
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Helper function to check if error has a code property
|
|
19
|
+
*/
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: Helper function needs to accept any error type
|
|
21
|
+
function hasErrorCode(error) {
|
|
22
|
+
return (error !== null &&
|
|
23
|
+
typeof error === 'object' &&
|
|
24
|
+
'code' in error &&
|
|
25
|
+
typeof error.code === 'string');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Helper function to get error message safely
|
|
29
|
+
*/
|
|
30
|
+
// biome-ignore lint/suspicious/noExplicitAny: Helper function needs to accept any error type
|
|
31
|
+
function getErrorMessage(error) {
|
|
32
|
+
return error instanceof Error ? error.message : String(error);
|
|
33
|
+
}
|
|
17
34
|
/**
|
|
18
35
|
* AuthBroker manages JWT authentication tokens for destinations
|
|
19
36
|
*/
|
|
@@ -94,82 +111,55 @@ class AuthBroker {
|
|
|
94
111
|
this.logger?.debug(`AuthBroker initialized: sessionStore(ok), serviceKeyStore(${hasServiceKeyStore ? 'ok' : 'none'}), tokenProvider(ok)`);
|
|
95
112
|
}
|
|
96
113
|
/**
|
|
97
|
-
*
|
|
98
|
-
* Uses tokenProvider for all authentication operations (browser-based authorization).
|
|
99
|
-
*
|
|
100
|
-
* **Flow:**
|
|
101
|
-
* **Step 0: Initialize Session with Token (if needed)**
|
|
102
|
-
* - Check if session has `authorizationToken` AND UAA credentials
|
|
103
|
-
* - If both are empty AND serviceKeyStore is available:
|
|
104
|
-
* - Get UAA credentials from service key
|
|
105
|
-
* - Use tokenProvider for browser-based authentication
|
|
106
|
-
* - Save token and refresh token to session
|
|
107
|
-
*
|
|
108
|
-
* **Step 1: Token Validation**
|
|
109
|
-
* - If token exists in session, validate it (if provider supports validation)
|
|
110
|
-
* - If valid → return token
|
|
111
|
-
* - If invalid or no token → continue to refresh
|
|
112
|
-
*
|
|
113
|
-
* **Step 2: Refresh Token Flow**
|
|
114
|
-
* - Check if refresh token exists in session
|
|
115
|
-
* - If refresh token exists:
|
|
116
|
-
* - Use tokenProvider to refresh token (browser-based or refresh grant)
|
|
117
|
-
* - Save new token to session
|
|
118
|
-
* - Return new token
|
|
119
|
-
* - Otherwise → proceed to Step 3
|
|
120
|
-
*
|
|
121
|
-
* **Step 3: New Token Flow**
|
|
122
|
-
* - Get UAA credentials from session or service key
|
|
123
|
-
* - Use tokenProvider for browser-based authentication
|
|
124
|
-
* - Save new token to session
|
|
125
|
-
* - Return new token
|
|
126
|
-
*
|
|
127
|
-
* **Important Notes:**
|
|
128
|
-
* - All authentication is handled by tokenProvider (e.g., XSUAA provider)
|
|
129
|
-
* - Provider uses browser-based authorization to ensure proper role assignment
|
|
130
|
-
* - Direct UAA HTTP requests are not used to avoid role assignment issues
|
|
131
|
-
*
|
|
132
|
-
* @param destination Destination name (e.g., "TRIAL")
|
|
133
|
-
* @returns Promise that resolves to JWT token string
|
|
134
|
-
* @throws Error if session initialization fails or authentication failed
|
|
114
|
+
* Load session data (connection and authorization configs)
|
|
135
115
|
*/
|
|
136
|
-
async
|
|
137
|
-
this.logger?.debug(`Getting token for destination: ${destination}`);
|
|
138
|
-
// Step 0: Initialize Session with Token (if needed)
|
|
116
|
+
async loadSessionData(destination) {
|
|
139
117
|
let connConfig = null;
|
|
140
118
|
let authConfig = null;
|
|
141
119
|
try {
|
|
142
120
|
connConfig = await this.sessionStore.getConnectionConfig(destination);
|
|
143
121
|
}
|
|
144
122
|
catch (error) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
123
|
+
if (hasErrorCode(error)) {
|
|
124
|
+
if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
125
|
+
this.logger?.debug(`Session file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
126
|
+
}
|
|
127
|
+
else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
128
|
+
this.logger?.warn(`Failed to parse session file for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
132
|
+
}
|
|
151
133
|
}
|
|
152
134
|
else {
|
|
153
|
-
this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${error
|
|
135
|
+
this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
154
136
|
}
|
|
155
137
|
}
|
|
156
138
|
try {
|
|
157
139
|
authConfig = await this.sessionStore.getAuthorizationConfig(destination);
|
|
158
140
|
}
|
|
159
141
|
catch (error) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
142
|
+
if (hasErrorCode(error)) {
|
|
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'} - ${getErrorMessage(error)}`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
151
|
+
}
|
|
166
152
|
}
|
|
167
153
|
else {
|
|
168
|
-
this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${error
|
|
154
|
+
this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
169
155
|
}
|
|
170
156
|
}
|
|
171
|
-
|
|
172
|
-
|
|
157
|
+
return { connConfig, authConfig };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get serviceUrl from session or service key store
|
|
161
|
+
*/
|
|
162
|
+
async getServiceUrl(destination, connConfig) {
|
|
173
163
|
let serviceUrl = connConfig?.serviceUrl;
|
|
174
164
|
if (!serviceUrl && this.serviceKeyStore) {
|
|
175
165
|
try {
|
|
@@ -180,15 +170,19 @@ class AuthBroker {
|
|
|
180
170
|
}
|
|
181
171
|
}
|
|
182
172
|
catch (error) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
173
|
+
if (hasErrorCode(error)) {
|
|
174
|
+
if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
175
|
+
this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
176
|
+
}
|
|
177
|
+
else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
178
|
+
this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.logger?.warn(`Failed to get serviceUrl from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
182
|
+
}
|
|
189
183
|
}
|
|
190
184
|
else {
|
|
191
|
-
this.logger?.warn(`Failed to get serviceUrl from service key store for ${destination}: ${error
|
|
185
|
+
this.logger?.warn(`Failed to get serviceUrl from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
192
186
|
}
|
|
193
187
|
}
|
|
194
188
|
}
|
|
@@ -197,307 +191,388 @@ class AuthBroker {
|
|
|
197
191
|
throw new Error(`Session for destination "${destination}" is missing required field 'serviceUrl'. ` +
|
|
198
192
|
`SessionStore must contain initial session with serviceUrl${this.serviceKeyStore ? ' or serviceKeyStore must contain serviceUrl' : ''}.`);
|
|
199
193
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`Provide serviceKeyStore to initialize from service key.`);
|
|
211
|
-
}
|
|
212
|
-
try {
|
|
213
|
-
// Get UAA credentials from service key
|
|
214
|
-
const serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
|
|
215
|
-
if (!serviceKeyAuthConfig || !serviceKeyAuthConfig.uaaUrl || !serviceKeyAuthConfig.uaaClientId || !serviceKeyAuthConfig.uaaClientSecret) {
|
|
216
|
-
this.logger?.error(`Step 0: Service key for ${destination} missing UAA credentials`);
|
|
217
|
-
throw new Error(`Service key for destination "${destination}" does not contain UAA credentials`);
|
|
218
|
-
}
|
|
219
|
-
// Check if browser auth is allowed
|
|
220
|
-
if (!this.allowBrowserAuth) {
|
|
221
|
-
const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. ` +
|
|
222
|
-
`Either enable browser auth or provide a valid session with token.`);
|
|
223
|
-
error.code = 'BROWSER_AUTH_REQUIRED';
|
|
224
|
-
error.destination = destination;
|
|
225
|
-
this.logger?.error(`Step 0: Browser auth required but disabled for ${destination}`);
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
// Use tokenProvider for browser-based authentication
|
|
229
|
-
this.logger?.debug(`Step 0: Authenticating via provider (browser) for ${destination} using service key UAA credentials`);
|
|
230
|
-
let tokenResult;
|
|
231
|
-
try {
|
|
232
|
-
tokenResult = await this.tokenProvider.getConnectionConfig(serviceKeyAuthConfig, {
|
|
233
|
-
browser: this.browser,
|
|
234
|
-
logger: this.logger,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
// Handle provider errors (network, auth, validation)
|
|
239
|
-
if (error.code === 'VALIDATION_ERROR') {
|
|
240
|
-
this.logger?.error(`Step 0: Provider validation error for ${destination}: missing ${error.missingFields?.join(', ') || 'required fields'}`);
|
|
241
|
-
throw new Error(`Cannot initialize session for destination "${destination}": provider validation failed - missing ${error.missingFields?.join(', ') || 'required fields'}`);
|
|
242
|
-
}
|
|
243
|
-
else if (error.code === 'BROWSER_AUTH_ERROR') {
|
|
244
|
-
this.logger?.error(`Step 0: Browser authentication failed for ${destination}: ${error.message}`);
|
|
245
|
-
throw new Error(`Cannot initialize session for destination "${destination}": browser authentication failed - ${error.message}`);
|
|
246
|
-
}
|
|
247
|
-
else if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
|
|
248
|
-
this.logger?.error(`Step 0: Network error for ${destination}: ${error.code}`);
|
|
249
|
-
throw new Error(`Cannot initialize session for destination "${destination}": network error - cannot reach authentication server (${error.code})`);
|
|
250
|
-
}
|
|
251
|
-
this.logger?.error(`Step 0: Provider error for ${destination}: ${error.message}`);
|
|
252
|
-
throw new Error(`Cannot initialize session for destination "${destination}": provider error - ${error.message}`);
|
|
253
|
-
}
|
|
254
|
-
const tokenLength = tokenResult.connectionConfig.authorizationToken?.length || 0;
|
|
255
|
-
this.logger?.info(`Step 0: Token initialized for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
|
|
256
|
-
// Get serviceUrl from service key store if not in connectionConfig
|
|
257
|
-
const serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
|
|
258
|
-
const connectionConfigWithServiceUrl = {
|
|
259
|
-
...tokenResult.connectionConfig,
|
|
260
|
-
serviceUrl: tokenResult.connectionConfig.serviceUrl || serviceKeyConnConfig?.serviceUrl || serviceUrl,
|
|
261
|
-
};
|
|
262
|
-
// Save token and UAA credentials to session
|
|
263
|
-
try {
|
|
264
|
-
await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
this.logger?.error(`Step 0: Failed to save connection config to session for ${destination}: ${error.message}`);
|
|
268
|
-
throw new Error(`Failed to save connection config for destination "${destination}": ${error.message}`);
|
|
269
|
-
}
|
|
270
|
-
try {
|
|
271
|
-
await this.sessionStore.setAuthorizationConfig(destination, {
|
|
272
|
-
...serviceKeyAuthConfig,
|
|
273
|
-
refreshToken: tokenResult.refreshToken || serviceKeyAuthConfig.refreshToken,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
catch (error) {
|
|
277
|
-
this.logger?.error(`Step 0: Failed to save authorization config to session for ${destination}: ${error.message}`);
|
|
278
|
-
throw new Error(`Failed to save authorization config for destination "${destination}": ${error.message}`);
|
|
279
|
-
}
|
|
280
|
-
return tokenResult.connectionConfig.authorizationToken;
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
// Re-throw BROWSER_AUTH_REQUIRED error without wrapping
|
|
284
|
-
if (error.code === 'BROWSER_AUTH_REQUIRED') {
|
|
285
|
-
throw error;
|
|
286
|
-
}
|
|
287
|
-
// Handle typed store errors
|
|
288
|
-
if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
289
|
-
this.logger?.error(`Step 0: Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
290
|
-
throw new Error(`Cannot initialize session for destination "${destination}": service key file not found`);
|
|
291
|
-
}
|
|
292
|
-
else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
293
|
-
this.logger?.error(`Step 0: Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
|
|
294
|
-
throw new Error(`Cannot initialize session for destination "${destination}": service key parsing failed - ${error.message}`);
|
|
295
|
-
}
|
|
296
|
-
else if (error.code === interfaces_1.STORE_ERROR_CODES.INVALID_CONFIG) {
|
|
297
|
-
this.logger?.error(`Step 0: Invalid service key config for ${destination}: missing fields ${error.missingFields?.join(', ') || 'unknown'}`);
|
|
298
|
-
throw new Error(`Cannot initialize session for destination "${destination}": invalid service key - missing ${error.missingFields?.join(', ') || 'required fields'}`);
|
|
299
|
-
}
|
|
300
|
-
this.logger?.error(`Step 0: Failed to initialize session for ${destination}: ${error.message}`);
|
|
301
|
-
throw new Error(`Cannot initialize session for destination "${destination}": ${error.message}`);
|
|
302
|
-
}
|
|
194
|
+
return serviceUrl;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get UAA credentials from session or service key
|
|
198
|
+
*/
|
|
199
|
+
async getUaaCredentials(destination, authConfig) {
|
|
200
|
+
if (authConfig?.uaaUrl &&
|
|
201
|
+
authConfig?.uaaClientId &&
|
|
202
|
+
authConfig?.uaaClientSecret) {
|
|
203
|
+
return authConfig;
|
|
303
204
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.logger?.debug(`Step 0: Token found for ${destination}, validating`);
|
|
307
|
-
// Validate token if provider supports validation and we have service URL
|
|
308
|
-
if (this.tokenProvider?.validateToken && serviceUrl) {
|
|
309
|
-
try {
|
|
310
|
-
const isValid = await this.tokenProvider.validateToken(connConfig.authorizationToken, serviceUrl);
|
|
311
|
-
if (isValid) {
|
|
312
|
-
this.logger?.info(`Step 0: Token valid for ${destination}: token(${connConfig.authorizationToken.length} chars)`);
|
|
313
|
-
return connConfig.authorizationToken;
|
|
314
|
-
}
|
|
315
|
-
this.logger?.debug(`Step 0: Token invalid for ${destination}, continuing to refresh`);
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
// Validation failed due to network/server error - log and continue to refresh
|
|
319
|
-
this.logger?.warn(`Step 0: Token validation failed for ${destination} (network error): ${error.message}. Continuing to refresh.`);
|
|
320
|
-
// Don't throw - continue to refresh flow
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
// No service URL or provider doesn't support validation - just return token
|
|
325
|
-
this.logger?.info(`Step 0: Token found for ${destination} (no validation): token(${connConfig.authorizationToken.length} chars)`);
|
|
326
|
-
return connConfig.authorizationToken;
|
|
327
|
-
}
|
|
205
|
+
if (!this.serviceKeyStore) {
|
|
206
|
+
throw new Error(`UAA credentials not found for ${destination}. Session has no UAA credentials and serviceKeyStore is not available.`);
|
|
328
207
|
}
|
|
329
|
-
// Step 2: Refresh Token Flow
|
|
330
|
-
this.logger?.debug(`Step 2: Attempting token refresh for ${destination}`);
|
|
331
|
-
// Get UAA credentials from session or service key
|
|
332
208
|
let serviceKeyAuthConfig = null;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
209
|
+
try {
|
|
210
|
+
serviceKeyAuthConfig =
|
|
211
|
+
await this.serviceKeyStore.getAuthorizationConfig(destination);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (hasErrorCode(error)) {
|
|
339
215
|
if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
340
216
|
this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
341
217
|
}
|
|
342
218
|
else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
343
|
-
this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error
|
|
219
|
+
this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
344
220
|
}
|
|
345
221
|
else {
|
|
346
|
-
this.logger?.warn(`Failed to get UAA credentials from service key store for ${destination}: ${error
|
|
222
|
+
this.logger?.warn(`Failed to get UAA credentials from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
347
223
|
}
|
|
348
224
|
}
|
|
225
|
+
else {
|
|
226
|
+
this.logger?.warn(`Failed to get UAA credentials from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
227
|
+
}
|
|
349
228
|
}
|
|
350
229
|
const uaaCredentials = authConfig || serviceKeyAuthConfig;
|
|
351
|
-
if (!uaaCredentials ||
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
throw new Error(
|
|
230
|
+
if (!uaaCredentials ||
|
|
231
|
+
!uaaCredentials.uaaUrl ||
|
|
232
|
+
!uaaCredentials.uaaClientId ||
|
|
233
|
+
!uaaCredentials.uaaClientSecret) {
|
|
234
|
+
throw new Error(`UAA credentials not found for ${destination}. Session has no UAA credentials${this.serviceKeyStore ? ' and serviceKeyStore has no UAA credentials' : ' and serviceKeyStore is not available'}.`);
|
|
356
235
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
236
|
+
return uaaCredentials;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Save token and config to session
|
|
240
|
+
*/
|
|
241
|
+
async saveTokenToSession(destination, connectionConfig, authorizationConfig) {
|
|
242
|
+
try {
|
|
243
|
+
await this.sessionStore.setConnectionConfig(destination, connectionConfig);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
this.logger?.error(`Failed to save connection config to session for ${destination}: ${getErrorMessage(error)}`);
|
|
247
|
+
throw new Error(`Failed to save connection config for destination "${destination}": ${getErrorMessage(error)}`);
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
await this.sessionStore.setAuthorizationConfig(destination, authorizationConfig);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
this.logger?.error(`Failed to save authorization config to session for ${destination}: ${getErrorMessage(error)}`);
|
|
254
|
+
throw new Error(`Failed to save authorization config for destination "${destination}": ${getErrorMessage(error)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Initialize session from service key (Step 0)
|
|
259
|
+
*/
|
|
260
|
+
async initializeSessionFromServiceKey(destination, serviceUrl) {
|
|
261
|
+
if (!this.serviceKeyStore) {
|
|
262
|
+
throw new Error(`Cannot initialize session for destination "${destination}": authorizationToken is empty, UAA credentials are empty, and serviceKeyStore is not available. Provide serviceKeyStore to initialize from service key.`);
|
|
263
|
+
}
|
|
264
|
+
const serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
|
|
265
|
+
if (!serviceKeyAuthConfig ||
|
|
266
|
+
!serviceKeyAuthConfig.uaaUrl ||
|
|
267
|
+
!serviceKeyAuthConfig.uaaClientId ||
|
|
268
|
+
!serviceKeyAuthConfig.uaaClientSecret) {
|
|
269
|
+
throw new Error(`Service key for destination "${destination}" does not contain UAA credentials`);
|
|
270
|
+
}
|
|
271
|
+
if (!this.allowBrowserAuth) {
|
|
272
|
+
const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Either enable browser auth or provide a valid session with token.`);
|
|
273
|
+
error.code = 'BROWSER_AUTH_REQUIRED';
|
|
274
|
+
error.destination = destination;
|
|
275
|
+
this.logger?.error(`Step 0: Browser auth required but disabled for ${destination}`);
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
this.logger?.debug(`Step 0: Authenticating via provider (browser) for ${destination} using service key UAA credentials`);
|
|
279
|
+
let tokenResult;
|
|
280
|
+
try {
|
|
281
|
+
tokenResult = await this.tokenProvider.getConnectionConfig(serviceKeyAuthConfig, {
|
|
282
|
+
browser: this.browser,
|
|
283
|
+
logger: this.logger,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (hasErrorCode(error)) {
|
|
288
|
+
if (error.code === 'VALIDATION_ERROR') {
|
|
289
|
+
throw new Error(`Cannot initialize session for destination "${destination}": provider validation failed - missing ${error.missingFields?.join(', ') || 'required fields'}`);
|
|
290
|
+
}
|
|
291
|
+
else if (error.code === 'BROWSER_AUTH_ERROR') {
|
|
292
|
+
throw new Error(`Cannot initialize session for destination "${destination}": browser authentication failed - ${getErrorMessage(error)}`);
|
|
293
|
+
}
|
|
294
|
+
else if (error.code === 'ECONNREFUSED' ||
|
|
295
|
+
error.code === 'ETIMEDOUT' ||
|
|
296
|
+
error.code === 'ENOTFOUND') {
|
|
297
|
+
throw new Error(`Cannot initialize session for destination "${destination}": network error - cannot reach authentication server (${error.code})`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
throw new Error(`Cannot initialize session for destination "${destination}": provider error - ${getErrorMessage(error)}`);
|
|
301
|
+
}
|
|
302
|
+
const token = tokenResult.connectionConfig.authorizationToken;
|
|
303
|
+
if (!token) {
|
|
304
|
+
throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
|
|
305
|
+
}
|
|
306
|
+
const tokenLength = token.length;
|
|
307
|
+
this.logger?.info(`Step 0: Token initialized for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
|
|
308
|
+
// Get serviceUrl from service key store if not in connectionConfig
|
|
309
|
+
const serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
|
|
310
|
+
const connectionConfigWithServiceUrl = {
|
|
311
|
+
...tokenResult.connectionConfig,
|
|
312
|
+
serviceUrl: tokenResult.connectionConfig.serviceUrl ||
|
|
313
|
+
serviceKeyConnConfig?.serviceUrl ||
|
|
314
|
+
serviceUrl,
|
|
315
|
+
};
|
|
316
|
+
await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, {
|
|
317
|
+
...serviceKeyAuthConfig,
|
|
318
|
+
refreshToken: tokenResult.refreshToken || serviceKeyAuthConfig.refreshToken,
|
|
319
|
+
});
|
|
320
|
+
return token;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Validate existing token (Step 1)
|
|
324
|
+
*/
|
|
325
|
+
async validateExistingToken(destination, token, serviceUrl) {
|
|
326
|
+
if (!this.tokenProvider?.validateToken) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const isValid = await this.tokenProvider.validateToken(token, serviceUrl);
|
|
331
|
+
if (isValid) {
|
|
332
|
+
this.logger?.info(`Step 1: Token valid for ${destination}: token(${token.length} chars)`);
|
|
333
|
+
return true;
|
|
417
334
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
335
|
+
this.logger?.debug(`Step 1: Token invalid for ${destination}, continuing to refresh`);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
// Validation failed due to network/server error - log and continue to refresh
|
|
340
|
+
this.logger?.warn(`Step 1: Token validation failed for ${destination} (network error): ${getErrorMessage(error)}. Continuing to refresh.`);
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Refresh token from session (Step 2a)
|
|
346
|
+
*/
|
|
347
|
+
async refreshTokenFromSession(destination, uaaCredentials, refreshToken, serviceUrl) {
|
|
348
|
+
this.logger?.debug(`Step 2a: Trying refreshTokenFromSession for ${destination}`);
|
|
349
|
+
const authConfigWithRefresh = { ...uaaCredentials, refreshToken };
|
|
350
|
+
let tokenResult;
|
|
351
|
+
try {
|
|
352
|
+
tokenResult = await this.tokenProvider.refreshTokenFromSession(authConfigWithRefresh, {
|
|
353
|
+
browser: this.browser,
|
|
354
|
+
logger: this.logger,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (hasErrorCode(error)) {
|
|
359
|
+
if (error.code === 'ECONNREFUSED' ||
|
|
360
|
+
error.code === 'ETIMEDOUT' ||
|
|
361
|
+
error.code === 'ENOTFOUND') {
|
|
362
|
+
this.logger?.debug(`Step 2a: Network error during refreshTokenFromSession for ${destination}: ${error.code}. Trying refreshTokenFromServiceKey`);
|
|
363
|
+
throw error; // Re-throw to trigger fallback to Step 2b
|
|
364
|
+
}
|
|
421
365
|
}
|
|
366
|
+
throw error; // Re-throw other errors
|
|
422
367
|
}
|
|
423
|
-
|
|
424
|
-
|
|
368
|
+
const token = tokenResult.connectionConfig.authorizationToken;
|
|
369
|
+
if (!token) {
|
|
370
|
+
throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
|
|
425
371
|
}
|
|
426
|
-
|
|
427
|
-
|
|
372
|
+
const tokenLength = token.length;
|
|
373
|
+
this.logger?.info(`Step 2a: Token refreshed from session for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
|
|
374
|
+
// Get serviceUrl from session or service key
|
|
375
|
+
let serviceKeyServiceUrl;
|
|
376
|
+
if (this.serviceKeyStore) {
|
|
377
|
+
try {
|
|
378
|
+
const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
|
|
379
|
+
serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
this.logger?.debug(`Could not get serviceUrl from service key store: ${getErrorMessage(error)}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const finalServiceUrl = tokenResult.connectionConfig.serviceUrl ||
|
|
386
|
+
serviceUrl ||
|
|
387
|
+
serviceKeyServiceUrl;
|
|
388
|
+
const connectionConfigWithServiceUrl = {
|
|
389
|
+
...tokenResult.connectionConfig,
|
|
390
|
+
serviceUrl: finalServiceUrl,
|
|
391
|
+
};
|
|
392
|
+
const authorizationConfig = {
|
|
393
|
+
...uaaCredentials,
|
|
394
|
+
refreshToken: tokenResult.refreshToken || refreshToken,
|
|
395
|
+
};
|
|
396
|
+
await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, authorizationConfig);
|
|
397
|
+
return token;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Refresh token from service key (Step 2b)
|
|
401
|
+
*/
|
|
402
|
+
async refreshTokenFromServiceKey(destination, uaaCredentials, serviceUrl) {
|
|
428
403
|
if (!this.allowBrowserAuth) {
|
|
429
|
-
const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled.
|
|
430
|
-
`Token refresh via session failed and browser auth is not allowed. ` +
|
|
431
|
-
`Either enable browser auth or ensure a valid refresh token exists in session.`);
|
|
404
|
+
const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Token refresh via session failed and browser auth is not allowed. Either enable browser auth or ensure a valid refresh token exists in session.`);
|
|
432
405
|
error.code = 'BROWSER_AUTH_REQUIRED';
|
|
433
406
|
error.destination = destination;
|
|
434
407
|
this.logger?.error(`Step 2b: Browser auth required but disabled for ${destination}`);
|
|
435
408
|
throw error;
|
|
436
409
|
}
|
|
410
|
+
this.logger?.debug(`Step 2b: Trying refreshTokenFromServiceKey for ${destination}`);
|
|
411
|
+
let tokenResult;
|
|
437
412
|
try {
|
|
438
|
-
|
|
439
|
-
const tokenResult = await this.tokenProvider.refreshTokenFromServiceKey(uaaCredentials, {
|
|
413
|
+
tokenResult = await this.tokenProvider.refreshTokenFromServiceKey(uaaCredentials, {
|
|
440
414
|
browser: this.browser,
|
|
441
415
|
logger: this.logger,
|
|
442
416
|
});
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
if (hasErrorCode(error)) {
|
|
420
|
+
if (error.code === 'VALIDATION_ERROR') {
|
|
421
|
+
throw new Error(`Token refresh failed: Missing required fields in authConfig - ${error.missingFields?.join(', ')}`);
|
|
422
|
+
}
|
|
423
|
+
else if (error.code === 'BROWSER_AUTH_ERROR') {
|
|
424
|
+
throw new Error(`Token refresh failed: Browser authentication failed or was cancelled - ${getErrorMessage(error)}`);
|
|
451
425
|
}
|
|
452
|
-
|
|
453
|
-
|
|
426
|
+
else if (error.code === 'ECONNREFUSED' ||
|
|
427
|
+
error.code === 'ETIMEDOUT' ||
|
|
428
|
+
error.code === 'ENOTFOUND') {
|
|
429
|
+
throw new Error(`Token refresh failed: Network error - ${error.code}: Cannot reach authentication server`);
|
|
430
|
+
}
|
|
431
|
+
else if (error.code === 'SERVICE_KEY_ERROR') {
|
|
432
|
+
throw new Error(`Token refresh failed: Service key not found or invalid for ${destination}`);
|
|
454
433
|
}
|
|
455
434
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
};
|
|
461
|
-
|
|
435
|
+
throw new Error(`Token refresh failed for ${destination}: ${getErrorMessage(error)}`);
|
|
436
|
+
}
|
|
437
|
+
const token = tokenResult.connectionConfig.authorizationToken;
|
|
438
|
+
if (!token) {
|
|
439
|
+
throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
|
|
440
|
+
}
|
|
441
|
+
const tokenLength = token.length;
|
|
442
|
+
this.logger?.info(`Step 2b: Token refreshed from service key for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
|
|
443
|
+
// Get serviceUrl from session or service key
|
|
444
|
+
let serviceKeyServiceUrl;
|
|
445
|
+
if (this.serviceKeyStore) {
|
|
462
446
|
try {
|
|
463
|
-
await this.
|
|
447
|
+
const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
|
|
448
|
+
serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
|
|
464
449
|
}
|
|
465
450
|
catch (error) {
|
|
466
|
-
this.logger?.
|
|
467
|
-
|
|
451
|
+
this.logger?.debug(`Could not get serviceUrl from service key store: ${getErrorMessage(error)}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const finalServiceUrl = tokenResult.connectionConfig.serviceUrl ||
|
|
455
|
+
serviceUrl ||
|
|
456
|
+
serviceKeyServiceUrl;
|
|
457
|
+
const connectionConfigWithServiceUrl = {
|
|
458
|
+
...tokenResult.connectionConfig,
|
|
459
|
+
serviceUrl: finalServiceUrl,
|
|
460
|
+
};
|
|
461
|
+
const authorizationConfig = {
|
|
462
|
+
...uaaCredentials,
|
|
463
|
+
refreshToken: tokenResult.refreshToken || uaaCredentials.refreshToken,
|
|
464
|
+
};
|
|
465
|
+
await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, authorizationConfig);
|
|
466
|
+
return token;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Get authentication token for destination.
|
|
470
|
+
* Uses tokenProvider for all authentication operations (browser-based authorization).
|
|
471
|
+
*
|
|
472
|
+
* **Flow:**
|
|
473
|
+
* **Step 0: Initialize Session with Token (if needed)**
|
|
474
|
+
* - Check if session has `authorizationToken` AND UAA credentials
|
|
475
|
+
* - If both are empty AND serviceKeyStore is available:
|
|
476
|
+
* - Get UAA credentials from service key
|
|
477
|
+
* - Use tokenProvider for browser-based authentication
|
|
478
|
+
* - Save token and refresh token to session
|
|
479
|
+
*
|
|
480
|
+
* **Step 1: Token Validation**
|
|
481
|
+
* - If token exists in session, validate it (if provider supports validation)
|
|
482
|
+
* - If valid → return token
|
|
483
|
+
* - If invalid or no token → continue to refresh
|
|
484
|
+
*
|
|
485
|
+
* **Step 2: Refresh Token Flow**
|
|
486
|
+
* - Check if refresh token exists in session
|
|
487
|
+
* - If refresh token exists:
|
|
488
|
+
* - Use tokenProvider to refresh token (browser-based or refresh grant)
|
|
489
|
+
* - Save new token to session
|
|
490
|
+
* - Return new token
|
|
491
|
+
* - Otherwise → proceed to Step 3
|
|
492
|
+
*
|
|
493
|
+
* **Step 3: New Token Flow**
|
|
494
|
+
* - Get UAA credentials from session or service key
|
|
495
|
+
* - Use tokenProvider for browser-based authentication
|
|
496
|
+
* - Save new token to session
|
|
497
|
+
* - Return new token
|
|
498
|
+
*
|
|
499
|
+
* **Important Notes:**
|
|
500
|
+
* - All authentication is handled by tokenProvider (e.g., XSUAA provider)
|
|
501
|
+
* - Provider uses browser-based authorization to ensure proper role assignment
|
|
502
|
+
* - Direct UAA HTTP requests are not used to avoid role assignment issues
|
|
503
|
+
*
|
|
504
|
+
* @param destination Destination name (e.g., "TRIAL")
|
|
505
|
+
* @returns Promise that resolves to JWT token string
|
|
506
|
+
* @throws Error if session initialization fails or authentication failed
|
|
507
|
+
*/
|
|
508
|
+
async getToken(destination) {
|
|
509
|
+
this.logger?.debug(`Getting token for destination: ${destination}`);
|
|
510
|
+
// Load session data
|
|
511
|
+
const { connConfig, authConfig } = await this.loadSessionData(destination);
|
|
512
|
+
// Get serviceUrl (required)
|
|
513
|
+
const serviceUrl = await this.getServiceUrl(destination, connConfig);
|
|
514
|
+
// Check if we have token or UAA credentials
|
|
515
|
+
const hasToken = !!connConfig?.authorizationToken;
|
|
516
|
+
const hasUaaCredentials = !!(authConfig?.uaaUrl &&
|
|
517
|
+
authConfig?.uaaClientId &&
|
|
518
|
+
authConfig?.uaaClientSecret);
|
|
519
|
+
this.logger?.debug(`Session check for ${destination}: hasToken(${hasToken}), hasUaaCredentials(${hasUaaCredentials}), serviceUrl(${serviceUrl ? 'yes' : 'no'})`);
|
|
520
|
+
// Step 0: Initialize Session with Token (if needed)
|
|
521
|
+
if (!hasToken && !hasUaaCredentials) {
|
|
522
|
+
try {
|
|
523
|
+
return await this.initializeSessionFromServiceKey(destination, serviceUrl);
|
|
468
524
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
refreshToken: tokenResult.refreshToken,
|
|
474
|
-
});
|
|
525
|
+
catch (error) {
|
|
526
|
+
// Re-throw BROWSER_AUTH_REQUIRED error without wrapping
|
|
527
|
+
if (hasErrorCode(error) && error.code === 'BROWSER_AUTH_REQUIRED') {
|
|
528
|
+
throw error;
|
|
475
529
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
530
|
+
// Handle typed store errors
|
|
531
|
+
if (hasErrorCode(error)) {
|
|
532
|
+
if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
533
|
+
throw new Error(`Cannot initialize session for destination "${destination}": service key file not found`);
|
|
534
|
+
}
|
|
535
|
+
else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
536
|
+
throw new Error(`Cannot initialize session for destination "${destination}": service key parsing failed - ${getErrorMessage(error)}`);
|
|
537
|
+
}
|
|
538
|
+
else if (error.code === interfaces_1.STORE_ERROR_CODES.INVALID_CONFIG) {
|
|
539
|
+
throw new Error(`Cannot initialize session for destination "${destination}": invalid service key - missing ${error.missingFields?.join(', ') || 'required fields'}`);
|
|
540
|
+
}
|
|
479
541
|
}
|
|
542
|
+
throw error;
|
|
480
543
|
}
|
|
481
|
-
return tokenResult.connectionConfig.authorizationToken;
|
|
482
544
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
|
|
545
|
+
// Step 1: Validate existing token
|
|
546
|
+
if (hasToken && connConfig?.authorizationToken) {
|
|
547
|
+
const isValid = await this.validateExistingToken(destination, connConfig.authorizationToken, serviceUrl);
|
|
548
|
+
if (isValid) {
|
|
549
|
+
return connConfig.authorizationToken;
|
|
488
550
|
}
|
|
489
|
-
|
|
490
|
-
|
|
551
|
+
// If no validation or validation failed, continue to refresh
|
|
552
|
+
if (!this.tokenProvider?.validateToken) {
|
|
553
|
+
this.logger?.info(`Token found for ${destination} (no validation): token(${connConfig.authorizationToken.length} chars)`);
|
|
554
|
+
return connConfig.authorizationToken;
|
|
491
555
|
}
|
|
492
|
-
|
|
493
|
-
|
|
556
|
+
}
|
|
557
|
+
// Step 2: Refresh Token Flow
|
|
558
|
+
this.logger?.debug(`Step 2: Attempting token refresh for ${destination}`);
|
|
559
|
+
const uaaCredentials = await this.getUaaCredentials(destination, authConfig);
|
|
560
|
+
// Step 2a: Try refresh from session (if refresh token exists)
|
|
561
|
+
const refreshToken = authConfig?.refreshToken;
|
|
562
|
+
if (refreshToken) {
|
|
563
|
+
try {
|
|
564
|
+
return await this.refreshTokenFromSession(destination, uaaCredentials, refreshToken, serviceUrl);
|
|
494
565
|
}
|
|
495
|
-
|
|
496
|
-
|
|
566
|
+
catch (error) {
|
|
567
|
+
this.logger?.debug(`Step 2a: refreshTokenFromSession failed for ${destination}: ${getErrorMessage(error)}, trying refreshTokenFromServiceKey`);
|
|
568
|
+
// Continue to try service key refresh
|
|
497
569
|
}
|
|
498
|
-
// Generic error
|
|
499
|
-
throw new Error(`Token refresh failed for ${destination}: ${error.message}`);
|
|
500
570
|
}
|
|
571
|
+
else {
|
|
572
|
+
this.logger?.debug(`Step 2a: No refresh token in session for ${destination}, skipping to service key refresh`);
|
|
573
|
+
}
|
|
574
|
+
// Step 2b: Try refresh from service key (browser authentication)
|
|
575
|
+
return await this.refreshTokenFromServiceKey(destination, uaaCredentials, serviceUrl);
|
|
501
576
|
}
|
|
502
577
|
/**
|
|
503
578
|
* Force refresh token for destination.
|
|
@@ -521,10 +596,11 @@ class AuthBroker {
|
|
|
521
596
|
this.logger?.debug(`Checking session store for authorization config: ${destination}`);
|
|
522
597
|
let sessionAuthConfig = null;
|
|
523
598
|
try {
|
|
524
|
-
sessionAuthConfig =
|
|
599
|
+
sessionAuthConfig =
|
|
600
|
+
await this.sessionStore.getAuthorizationConfig(destination);
|
|
525
601
|
}
|
|
526
602
|
catch (error) {
|
|
527
|
-
this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${error
|
|
603
|
+
this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
528
604
|
}
|
|
529
605
|
if (sessionAuthConfig) {
|
|
530
606
|
this.logger?.debug(`Authorization config from session for ${destination}: hasUaaUrl(${!!sessionAuthConfig.uaaUrl}), hasRefreshToken(${!!sessionAuthConfig.refreshToken})`);
|
|
@@ -535,18 +611,24 @@ class AuthBroker {
|
|
|
535
611
|
this.logger?.debug(`Checking service key store for authorization config: ${destination}`);
|
|
536
612
|
let serviceKeyAuthConfig = null;
|
|
537
613
|
try {
|
|
538
|
-
serviceKeyAuthConfig =
|
|
614
|
+
serviceKeyAuthConfig =
|
|
615
|
+
await this.serviceKeyStore.getAuthorizationConfig(destination);
|
|
539
616
|
}
|
|
540
617
|
catch (error) {
|
|
541
618
|
// Handle typed store errors
|
|
542
|
-
if (error
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
619
|
+
if (hasErrorCode(error)) {
|
|
620
|
+
if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
621
|
+
this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
622
|
+
}
|
|
623
|
+
else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
624
|
+
this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
628
|
+
}
|
|
547
629
|
}
|
|
548
630
|
else {
|
|
549
|
-
this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${error
|
|
631
|
+
this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
550
632
|
}
|
|
551
633
|
}
|
|
552
634
|
if (serviceKeyAuthConfig) {
|
|
@@ -570,10 +652,11 @@ class AuthBroker {
|
|
|
570
652
|
// Try session store first (has tokens and URLs)
|
|
571
653
|
let sessionConnConfig = null;
|
|
572
654
|
try {
|
|
573
|
-
sessionConnConfig =
|
|
655
|
+
sessionConnConfig =
|
|
656
|
+
await this.sessionStore.getConnectionConfig(destination);
|
|
574
657
|
}
|
|
575
658
|
catch (error) {
|
|
576
|
-
this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${error
|
|
659
|
+
this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
577
660
|
}
|
|
578
661
|
if (sessionConnConfig) {
|
|
579
662
|
this.logger?.debug(`Connection config from session for ${destination}: token(${sessionConnConfig.authorizationToken?.length || 0} chars), serviceUrl(${sessionConnConfig.serviceUrl ? 'yes' : 'no'})`);
|
|
@@ -583,18 +666,24 @@ class AuthBroker {
|
|
|
583
666
|
if (this.serviceKeyStore) {
|
|
584
667
|
let serviceKeyConnConfig = null;
|
|
585
668
|
try {
|
|
586
|
-
serviceKeyConnConfig =
|
|
669
|
+
serviceKeyConnConfig =
|
|
670
|
+
await this.serviceKeyStore.getConnectionConfig(destination);
|
|
587
671
|
}
|
|
588
672
|
catch (error) {
|
|
589
673
|
// Handle typed store errors
|
|
590
|
-
if (error
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
674
|
+
if (hasErrorCode(error)) {
|
|
675
|
+
if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
676
|
+
this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
677
|
+
}
|
|
678
|
+
else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
679
|
+
this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
this.logger?.warn(`Failed to get connection config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
683
|
+
}
|
|
595
684
|
}
|
|
596
685
|
else {
|
|
597
|
-
this.logger?.warn(`Failed to get connection config from service key store for ${destination}: ${error
|
|
686
|
+
this.logger?.warn(`Failed to get connection config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
598
687
|
}
|
|
599
688
|
}
|
|
600
689
|
if (serviceKeyConnConfig) {
|
|
@@ -608,5 +697,41 @@ class AuthBroker {
|
|
|
608
697
|
this.logger?.debug(`No connection config found for ${destination}`);
|
|
609
698
|
return null;
|
|
610
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Create a token refresher for a specific destination.
|
|
702
|
+
*
|
|
703
|
+
* The token refresher is designed to be injected into JwtAbapConnection via DI,
|
|
704
|
+
* allowing the connection to handle token refresh transparently without knowing
|
|
705
|
+
* about authentication internals.
|
|
706
|
+
*
|
|
707
|
+
* **Usage:**
|
|
708
|
+
* ```typescript
|
|
709
|
+
* const broker = new AuthBroker(config);
|
|
710
|
+
* const tokenRefresher = broker.createTokenRefresher('TRIAL');
|
|
711
|
+
* const connection = new JwtAbapConnection(config, tokenRefresher);
|
|
712
|
+
* ```
|
|
713
|
+
*
|
|
714
|
+
* @param destination Destination name (e.g., "TRIAL")
|
|
715
|
+
* @returns ITokenRefresher implementation for the given destination
|
|
716
|
+
*/
|
|
717
|
+
createTokenRefresher(destination) {
|
|
718
|
+
const broker = this;
|
|
719
|
+
return {
|
|
720
|
+
/**
|
|
721
|
+
* Get current valid token.
|
|
722
|
+
* Returns cached token if valid, otherwise refreshes and returns new token.
|
|
723
|
+
*/
|
|
724
|
+
async getToken() {
|
|
725
|
+
return broker.getToken(destination);
|
|
726
|
+
},
|
|
727
|
+
/**
|
|
728
|
+
* Force refresh token and save to session store.
|
|
729
|
+
* Always performs refresh, ignoring cached token validity.
|
|
730
|
+
*/
|
|
731
|
+
async refreshToken() {
|
|
732
|
+
return broker.refreshToken(destination);
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
}
|
|
611
736
|
}
|
|
612
737
|
exports.AuthBroker = AuthBroker;
|