@mcp-abap-adt/auth-broker 2.2.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +235 -0
- package/README.md +324 -284
- package/dist/AuthBroker.d.ts +81 -106
- package/dist/AuthBroker.d.ts.map +1 -1
- package/dist/AuthBroker.js +209 -567
- package/dist/bin/mcp-auth.js +54 -31
- package/dist/bin/mcp-sso.js +52 -44
- package/dist/bin/mcpSsoConfig.js +169 -1
- package/dist/bin/samlMetadata.js +175 -0
- package/dist/bin/workDir.js +79 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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/package.json +12 -8
- package/dist/utils/formatting.d.ts +0 -16
- package/dist/utils/formatting.d.ts.map +0 -1
- package/dist/utils/formatting.js +0 -34
package/dist/AuthBroker.js
CHANGED
|
@@ -1,653 +1,295 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* AuthBroker: tokens for a destination, from a provider, kept in a session store.
|
|
4
|
+
*
|
|
5
|
+
* The broker orchestrates and nothing more. It resolves what the stores know
|
|
6
|
+
* about a destination, hands it to the provider, asks the provider for a token
|
|
7
|
+
* and writes the answer back. Whether a token is still valid, whether to use the
|
|
8
|
+
* refresh token or log in, and how a login is conducted (browser, headless,
|
|
9
|
+
* pasted code) are the provider's decisions — made by its strategy — and the
|
|
10
|
+
* broker does not repeat or override any of them.
|
|
4
11
|
*/
|
|
5
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
13
|
exports.AuthBroker = void 0;
|
|
7
14
|
const interfaces_auth_1 = require("@mcp-abap-adt/interfaces-auth");
|
|
8
|
-
const formatting_1 = require("./utils/formatting");
|
|
9
|
-
/**
|
|
10
|
-
* No-op logger implementation for default fallback when logger is not provided
|
|
11
|
-
*/
|
|
12
15
|
const noOpLogger = {
|
|
13
16
|
info: () => { },
|
|
14
17
|
error: () => { },
|
|
15
18
|
warn: () => { },
|
|
16
19
|
debug: () => { },
|
|
17
20
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
function hasErrorCode(error) {
|
|
23
|
-
return (error !== null &&
|
|
24
|
-
typeof error === 'object' &&
|
|
25
|
-
'code' in error &&
|
|
26
|
-
typeof error.code === 'string');
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Helper function to get error message safely
|
|
30
|
-
*/
|
|
31
|
-
// biome-ignore lint/suspicious/noExplicitAny: Helper function needs to accept any error type
|
|
32
|
-
function getErrorMessage(error) {
|
|
33
|
-
return error instanceof Error ? error.message : String(error);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Whether an error represents a failed *interactive* browser login (the user
|
|
37
|
-
* did not complete the OAuth flow / it timed out), as opposed to a transient or
|
|
38
|
-
* configuration error. Such failures must not be retried via a second
|
|
39
|
-
* provider.getTokens() — that would start a duplicate browser login on the same
|
|
40
|
-
* redirect port and mask the real cause with a "Port in use" error.
|
|
41
|
-
*/
|
|
42
|
-
function isInteractiveAuthFailure(error) {
|
|
43
|
-
if (hasErrorCode(error) && error.code === 'BROWSER_AUTH_ERROR') {
|
|
44
|
-
return true;
|
|
21
|
+
function errorCode(error) {
|
|
22
|
+
if (error !== null && typeof error === 'object' && 'code' in error) {
|
|
23
|
+
const code = error.code;
|
|
24
|
+
return typeof code === 'string' ? code : undefined;
|
|
45
25
|
}
|
|
46
|
-
|
|
47
|
-
return /authentication timeout|browser authentication|already in use/i.test(message);
|
|
26
|
+
return undefined;
|
|
48
27
|
}
|
|
49
28
|
/**
|
|
50
|
-
* AuthBroker manages
|
|
29
|
+
* AuthBroker manages authentication tokens for destinations
|
|
51
30
|
*/
|
|
52
31
|
class AuthBroker {
|
|
53
|
-
browser;
|
|
54
32
|
logger;
|
|
55
33
|
serviceKeyStore;
|
|
56
34
|
sessionStore;
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
provider;
|
|
36
|
+
providers = new Map();
|
|
59
37
|
/**
|
|
60
|
-
*
|
|
61
|
-
* @param
|
|
62
|
-
* - sessionStore: Store for session data (required)
|
|
63
|
-
* - serviceKeyStore: Store for service keys (optional)
|
|
64
|
-
* - tokenProvider: Token provider implementing ITokenProvider interface (required) - handles browser-based authorization
|
|
65
|
-
* @param browser Optional browser name for authentication (chrome, edge, firefox, system, headless, none).
|
|
66
|
-
* Default: 'system' (system default browser).
|
|
67
|
-
* Use 'headless' for SSH/remote sessions - logs URL and waits for manual callback.
|
|
68
|
-
* Use 'none' for automated tests - logs URL and rejects immediately.
|
|
69
|
-
* @param logger Optional logger instance implementing ILogger interface. If not provided, uses no-op logger.
|
|
38
|
+
* @param config Stores and the provider (instance or factory)
|
|
39
|
+
* @param logger Optional logger. Nothing the broker logs contains a token.
|
|
70
40
|
*/
|
|
71
|
-
constructor(config,
|
|
72
|
-
// Validate that config is provided
|
|
41
|
+
constructor(config, logger) {
|
|
73
42
|
if (!config) {
|
|
74
43
|
throw new Error('AuthBroker: config parameter is required');
|
|
75
44
|
}
|
|
76
|
-
|
|
77
|
-
if (!
|
|
45
|
+
const { sessionStore, serviceKeyStore, provider } = config;
|
|
46
|
+
if (!sessionStore) {
|
|
78
47
|
throw new Error('AuthBroker: sessionStore is required');
|
|
79
48
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
throw new Error('AuthBroker: sessionStore.getConnectionConfig must be a function');
|
|
94
|
-
}
|
|
95
|
-
if (typeof sessionStore.setAuthorizationConfig !== 'function') {
|
|
96
|
-
throw new Error('AuthBroker: sessionStore.setAuthorizationConfig must be a function');
|
|
97
|
-
}
|
|
98
|
-
if (typeof sessionStore.setConnectionConfig !== 'function') {
|
|
99
|
-
throw new Error('AuthBroker: sessionStore.setConnectionConfig must be a function');
|
|
100
|
-
}
|
|
101
|
-
// Check tokenProvider methods (required)
|
|
102
|
-
if (typeof tokenProvider.getTokens !== 'function') {
|
|
103
|
-
throw new Error('AuthBroker: tokenProvider.getTokens must be a function');
|
|
104
|
-
}
|
|
105
|
-
// validateToken is optional, so we don't check it
|
|
106
|
-
// Check serviceKeyStore methods (if provided)
|
|
107
|
-
if (serviceKeyStore) {
|
|
108
|
-
if (typeof serviceKeyStore.getServiceKey !== 'function') {
|
|
109
|
-
throw new Error('AuthBroker: serviceKeyStore.getServiceKey must be a function');
|
|
110
|
-
}
|
|
111
|
-
if (typeof serviceKeyStore.getAuthorizationConfig !== 'function') {
|
|
112
|
-
throw new Error('AuthBroker: serviceKeyStore.getAuthorizationConfig must be a function');
|
|
113
|
-
}
|
|
114
|
-
if (typeof serviceKeyStore.getConnectionConfig !== 'function') {
|
|
115
|
-
throw new Error('AuthBroker: serviceKeyStore.getConnectionConfig must be a function');
|
|
49
|
+
if (!provider) {
|
|
50
|
+
throw new Error('AuthBroker: provider is required');
|
|
51
|
+
}
|
|
52
|
+
for (const method of [
|
|
53
|
+
'getAuthorizationConfig',
|
|
54
|
+
'getConnectionConfig',
|
|
55
|
+
'setAuthorizationConfig',
|
|
56
|
+
'setConnectionConfig',
|
|
57
|
+
'loadSession',
|
|
58
|
+
'saveSession',
|
|
59
|
+
]) {
|
|
60
|
+
if (typeof sessionStore[method] !== 'function') {
|
|
61
|
+
throw new Error(`AuthBroker: sessionStore.${method} must be a function`);
|
|
116
62
|
}
|
|
117
63
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
this.browser = browser || 'system';
|
|
122
|
-
this.logger = logger || noOpLogger;
|
|
123
|
-
this.allowBrowserAuth = config.allowBrowserAuth ?? true;
|
|
124
|
-
// Log successful initialization
|
|
125
|
-
const hasServiceKeyStore = !!this.serviceKeyStore;
|
|
126
|
-
this.logger?.info('[AuthBroker] Broker initialized', {
|
|
127
|
-
hasServiceKeyStore,
|
|
128
|
-
hasSessionStore: true,
|
|
129
|
-
hasTokenProvider: true,
|
|
130
|
-
browser: this.browser,
|
|
131
|
-
allowBrowserAuth: this.allowBrowserAuth,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Load session data (connection and authorization configs)
|
|
136
|
-
*/
|
|
137
|
-
async loadSessionData(destination) {
|
|
138
|
-
let connConfig = null;
|
|
139
|
-
let authConfig = null;
|
|
140
|
-
try {
|
|
141
|
-
connConfig = await this.sessionStore.getConnectionConfig(destination);
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
if (hasErrorCode(error)) {
|
|
145
|
-
if (error.code === interfaces_auth_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
146
|
-
this.logger?.debug(`Session file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
147
|
-
}
|
|
148
|
-
else if (error.code === interfaces_auth_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
149
|
-
this.logger?.warn(`Failed to parse session file for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
153
|
-
}
|
|
64
|
+
if (typeof provider !== 'function') {
|
|
65
|
+
if (typeof provider.getTokens !== 'function') {
|
|
66
|
+
throw new Error('AuthBroker: provider.getTokens must be a function');
|
|
154
67
|
}
|
|
155
|
-
|
|
156
|
-
|
|
68
|
+
if (typeof provider.refreshTokens !== 'function') {
|
|
69
|
+
throw new Error('AuthBroker: provider.refreshTokens must be a function');
|
|
157
70
|
}
|
|
158
71
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
else if (error.code === interfaces_auth_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
168
|
-
this.logger?.warn(`Failed to parse session file for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
72
|
+
if (serviceKeyStore) {
|
|
73
|
+
for (const method of [
|
|
74
|
+
'getServiceKey',
|
|
75
|
+
'getAuthorizationConfig',
|
|
76
|
+
'getConnectionConfig',
|
|
77
|
+
]) {
|
|
78
|
+
if (typeof serviceKeyStore[method] !== 'function') {
|
|
79
|
+
throw new Error(`AuthBroker: serviceKeyStore.${method} must be a function`);
|
|
172
80
|
}
|
|
173
81
|
}
|
|
174
|
-
else {
|
|
175
|
-
this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
176
|
-
}
|
|
177
82
|
}
|
|
178
|
-
|
|
83
|
+
this.sessionStore = sessionStore;
|
|
84
|
+
this.serviceKeyStore = serviceKeyStore;
|
|
85
|
+
this.provider = provider;
|
|
86
|
+
this.logger = logger ?? noOpLogger;
|
|
87
|
+
this.logger.debug('[AuthBroker] Broker initialized', {
|
|
88
|
+
hasServiceKeyStore: !!serviceKeyStore,
|
|
89
|
+
providerForm: typeof provider === 'function' ? 'factory' : 'instance',
|
|
90
|
+
});
|
|
179
91
|
}
|
|
180
92
|
/**
|
|
181
|
-
*
|
|
93
|
+
* A token for the destination: the provider's current one, which it refreshes
|
|
94
|
+
* or obtains by login when it judges the cached one unusable.
|
|
95
|
+
*
|
|
96
|
+
* The result is written to the session store. Errors from the provider
|
|
97
|
+
* (its typed errors included) propagate unchanged.
|
|
182
98
|
*/
|
|
183
|
-
async
|
|
184
|
-
|
|
185
|
-
if (!serviceUrl && this.serviceKeyStore) {
|
|
186
|
-
try {
|
|
187
|
-
const serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
|
|
188
|
-
serviceUrl = serviceKeyConnConfig?.serviceUrl;
|
|
189
|
-
if (serviceUrl) {
|
|
190
|
-
this.logger?.debug(`serviceUrl not in session for ${destination}, found in serviceKeyStore`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
if (hasErrorCode(error)) {
|
|
195
|
-
if (error.code === interfaces_auth_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
196
|
-
this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
197
|
-
}
|
|
198
|
-
else if (error.code === interfaces_auth_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
199
|
-
this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
this.logger?.warn(`Failed to get serviceUrl from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
this.logger?.warn(`Failed to get serviceUrl from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (!serviceUrl) {
|
|
211
|
-
this.logger?.error(`Session for destination "${destination}" is missing required field 'serviceUrl'. SessionStore must contain initial session with serviceUrl${this.serviceKeyStore ? ' or serviceKeyStore must contain serviceUrl' : ''}.`);
|
|
212
|
-
throw new Error(`Session for destination "${destination}" is missing required field 'serviceUrl'. ` +
|
|
213
|
-
`SessionStore must contain initial session with serviceUrl${this.serviceKeyStore ? ' or serviceKeyStore must contain serviceUrl' : ''}.`);
|
|
214
|
-
}
|
|
215
|
-
return serviceUrl;
|
|
99
|
+
async getToken(destination) {
|
|
100
|
+
return this.obtain(destination, 'getTokens');
|
|
216
101
|
}
|
|
217
102
|
/**
|
|
218
|
-
*
|
|
103
|
+
* A new token for the destination, never the cached one — for a caller whose
|
|
104
|
+
* token the server has just refused. Calls the provider's `refreshTokens()`,
|
|
105
|
+
* writes the result to the session store and returns it.
|
|
219
106
|
*/
|
|
220
|
-
async
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
107
|
+
async refreshToken(destination) {
|
|
108
|
+
return this.obtain(destination, 'refreshTokens');
|
|
109
|
+
}
|
|
110
|
+
async obtain(destination, method) {
|
|
111
|
+
const connConfig = await this.read(destination, 'session connection config', () => this.sessionStore.getConnectionConfig(destination));
|
|
112
|
+
const serviceUrl = await this.resolveServiceUrl(destination, connConfig);
|
|
113
|
+
const provider = await this.providerFor(destination, serviceUrl, connConfig);
|
|
114
|
+
this.logger.debug(`[AuthBroker] ${method} for ${destination}`);
|
|
115
|
+
const result = await provider[method]();
|
|
116
|
+
if (!result?.authorizationToken) {
|
|
117
|
+
throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
|
|
228
118
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
243
|
-
}
|
|
119
|
+
await this.persist(destination, serviceUrl, connConfig, result);
|
|
120
|
+
return result.authorizationToken;
|
|
121
|
+
}
|
|
122
|
+
async providerFor(destination, serviceUrl, connConfig) {
|
|
123
|
+
if (typeof this.provider !== 'function') {
|
|
124
|
+
return this.provider;
|
|
244
125
|
}
|
|
245
|
-
|
|
246
|
-
|
|
126
|
+
const existing = this.providers.get(destination);
|
|
127
|
+
if (existing) {
|
|
128
|
+
return existing;
|
|
247
129
|
}
|
|
248
|
-
|
|
130
|
+
const authConfig = await this.resolveAuthorizationConfig(destination);
|
|
131
|
+
const built = this.provider(destination, authConfig, {
|
|
132
|
+
...(connConfig ?? {}),
|
|
133
|
+
serviceUrl,
|
|
134
|
+
});
|
|
135
|
+
this.providers.set(destination, built);
|
|
136
|
+
this.logger.debug(`[AuthBroker] Provider built for ${destination}`, {
|
|
137
|
+
hasCredentials: !!authConfig,
|
|
138
|
+
hasRefreshToken: !!authConfig?.refreshToken,
|
|
139
|
+
hasStoredToken: !!(connConfig?.authorizationToken || connConfig?.sessionCookies),
|
|
140
|
+
});
|
|
141
|
+
return built;
|
|
249
142
|
}
|
|
250
143
|
/**
|
|
251
|
-
*
|
|
144
|
+
* The credentials the provider is built with: the session's own when it holds
|
|
145
|
+
* them, else the service key's, carrying the refresh token the session
|
|
146
|
+
* stored. The session keeps a refresh token without credentials when the
|
|
147
|
+
* credentials came from the service key, since the broker does not copy the
|
|
148
|
+
* client secret into it; `loadSession` is where such a token is read.
|
|
252
149
|
*/
|
|
253
|
-
async
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
this.logger?.error(`Failed to save authorization config to session for ${destination}: ${getErrorMessage(error)}`);
|
|
269
|
-
throw new Error(`Failed to save authorization config for destination "${destination}": ${getErrorMessage(error)}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
this.logger?.debug(`Skipping authorization config save for ${destination}: missing UAA fields`);
|
|
150
|
+
async resolveAuthorizationConfig(destination) {
|
|
151
|
+
const sessionAuth = await this.read(destination, 'session authorization config', () => this.sessionStore.getAuthorizationConfig(destination));
|
|
152
|
+
if (sessionAuth) {
|
|
153
|
+
return sessionAuth;
|
|
154
|
+
}
|
|
155
|
+
const session = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
156
|
+
const storedRefreshToken = typeof session?.refreshToken === 'string'
|
|
157
|
+
? session.refreshToken
|
|
158
|
+
: undefined;
|
|
159
|
+
const serviceKeyStore = this.serviceKeyStore;
|
|
160
|
+
const keyAuth = serviceKeyStore
|
|
161
|
+
? await this.read(destination, 'service key authorization config', () => serviceKeyStore.getAuthorizationConfig(destination))
|
|
162
|
+
: null;
|
|
163
|
+
if (!keyAuth) {
|
|
164
|
+
return null;
|
|
274
165
|
}
|
|
166
|
+
return {
|
|
167
|
+
...keyAuth,
|
|
168
|
+
refreshToken: storedRefreshToken ?? keyAuth.refreshToken,
|
|
169
|
+
};
|
|
275
170
|
}
|
|
276
|
-
async
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
const tokenResult = await getTokens.call(this.tokenProvider);
|
|
284
|
-
const expiresAt = tokenResult.expiresIn
|
|
285
|
-
? Date.now() + tokenResult.expiresIn * 1000
|
|
286
|
-
: undefined;
|
|
287
|
-
this.logger?.info(`[AuthBroker] Tokens received for ${destination}`, {
|
|
288
|
-
authorizationToken: (0, formatting_1.formatToken)(tokenResult.authorizationToken),
|
|
289
|
-
hasRefreshToken: !!tokenResult.refreshToken,
|
|
290
|
-
refreshToken: (0, formatting_1.formatToken)(tokenResult.refreshToken),
|
|
291
|
-
authType: tokenResult.authType,
|
|
292
|
-
expiresIn: tokenResult.expiresIn,
|
|
293
|
-
expiresAt: expiresAt ? (0, formatting_1.formatExpirationDate)(expiresAt) : undefined,
|
|
294
|
-
});
|
|
295
|
-
return tokenResult;
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
if (hasErrorCode(error)) {
|
|
299
|
-
if (error.code === 'VALIDATION_ERROR') {
|
|
300
|
-
throw new Error(`Token provider validation failed for ${destination}: missing ${error.missingFields?.join(', ') || 'required fields'}`);
|
|
301
|
-
}
|
|
302
|
-
if (error.code === 'BROWSER_AUTH_ERROR') {
|
|
303
|
-
throw new Error(`Token provider browser authentication failed for ${destination}: ${getErrorMessage(error)}`);
|
|
304
|
-
}
|
|
305
|
-
if (error.code === 'ECONNREFUSED' ||
|
|
306
|
-
error.code === 'ETIMEDOUT' ||
|
|
307
|
-
error.code === 'ENOTFOUND') {
|
|
308
|
-
throw new Error(`Token provider network error for ${destination}: ${error.code}`);
|
|
309
|
-
}
|
|
310
|
-
if (error.code === 'SERVICE_KEY_ERROR') {
|
|
311
|
-
throw new Error(`Token provider service key error for ${destination}: ${getErrorMessage(error)}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
throw new Error(`Token provider error for ${destination}: ${getErrorMessage(error)}`);
|
|
171
|
+
async resolveServiceUrl(destination, connConfig) {
|
|
172
|
+
let serviceUrl = connConfig?.serviceUrl;
|
|
173
|
+
const serviceKeyStore = this.serviceKeyStore;
|
|
174
|
+
if (!serviceUrl && serviceKeyStore) {
|
|
175
|
+
const keyConn = await this.read(destination, 'service key connection config', () => serviceKeyStore.getConnectionConfig(destination));
|
|
176
|
+
serviceUrl = keyConn?.serviceUrl;
|
|
315
177
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (!token) {
|
|
320
|
-
throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
|
|
178
|
+
if (!serviceUrl) {
|
|
179
|
+
throw new Error(`Session for destination "${destination}" is missing required field 'serviceUrl'. ` +
|
|
180
|
+
`SessionStore must contain initial session with serviceUrl${this.serviceKeyStore ? ' or serviceKeyStore must contain serviceUrl' : ''}.`);
|
|
321
181
|
}
|
|
322
|
-
|
|
323
|
-
const connectionConfigWithServiceUrl = {
|
|
324
|
-
...baseConnConfig,
|
|
325
|
-
serviceUrl,
|
|
326
|
-
authorizationToken: isSaml ? undefined : token,
|
|
327
|
-
sessionCookies: isSaml ? token : undefined,
|
|
328
|
-
authType: isSaml ? 'saml' : 'jwt',
|
|
329
|
-
};
|
|
330
|
-
const authorizationConfig = {
|
|
331
|
-
...authConfig,
|
|
332
|
-
refreshToken: tokenResult.refreshToken ?? authConfig.refreshToken,
|
|
333
|
-
};
|
|
334
|
-
const expiresAt = tokenResult.expiresIn
|
|
335
|
-
? Date.now() + tokenResult.expiresIn * 1000
|
|
336
|
-
: undefined;
|
|
337
|
-
this.logger?.info(`[AuthBroker] Saving tokens to session for ${destination}`, {
|
|
338
|
-
serviceUrl,
|
|
339
|
-
authorizationToken: (0, formatting_1.formatToken)(token),
|
|
340
|
-
hasRefreshToken: !!authorizationConfig.refreshToken,
|
|
341
|
-
refreshToken: (0, formatting_1.formatToken)(authorizationConfig.refreshToken),
|
|
342
|
-
expiresIn: tokenResult.expiresIn,
|
|
343
|
-
expiresAt: expiresAt ? (0, formatting_1.formatExpirationDate)(expiresAt) : undefined,
|
|
344
|
-
});
|
|
345
|
-
await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, authorizationConfig);
|
|
182
|
+
return serviceUrl;
|
|
346
183
|
}
|
|
347
184
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
* **Flow:**
|
|
352
|
-
* **Step 0: Initialize Session with Token (if needed)**
|
|
353
|
-
* - Check if session has `authorizationToken` AND UAA credentials
|
|
354
|
-
* - If both are empty AND serviceKeyStore is available:
|
|
355
|
-
* - Get UAA credentials from service key
|
|
356
|
-
* - Use tokenProvider for browser-based authentication
|
|
357
|
-
* - Save token and refresh token to session
|
|
185
|
+
* Writes the result by its type: a SAML result is session cookies, anything
|
|
186
|
+
* else a bearer token. The refresh token is written only when the result has
|
|
187
|
+
* one, so a provider that returns none does not erase the stored one.
|
|
358
188
|
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* - If valid → return token
|
|
362
|
-
* - If invalid or no token → continue to refresh
|
|
363
|
-
*
|
|
364
|
-
* **Step 2: Refresh Token Flow**
|
|
365
|
-
* - Check if refresh token exists in session
|
|
366
|
-
* - If refresh token exists:
|
|
367
|
-
* - Use tokenProvider to refresh token (browser-based or refresh grant)
|
|
368
|
-
* - Save new token to session
|
|
369
|
-
* - Return new token
|
|
370
|
-
* - Otherwise → proceed to Step 3
|
|
371
|
-
*
|
|
372
|
-
* **Step 3: New Token Flow**
|
|
373
|
-
* - Get UAA credentials from session or service key
|
|
374
|
-
* - Use tokenProvider for browser-based authentication
|
|
375
|
-
* - Save new token to session
|
|
376
|
-
* - Return new token
|
|
377
|
-
*
|
|
378
|
-
* **Important Notes:**
|
|
379
|
-
* - All authentication is handled by tokenProvider (e.g., XSUAA provider)
|
|
380
|
-
* - Provider uses browser-based authorization to ensure proper role assignment
|
|
381
|
-
* - Direct UAA HTTP requests are not used to avoid role assignment issues
|
|
382
|
-
*
|
|
383
|
-
* @param destination Destination name (e.g., "TRIAL")
|
|
384
|
-
* @returns Promise that resolves to JWT token string
|
|
385
|
-
* @throws Error if session initialization fails or authentication failed
|
|
189
|
+
* `ITokenResult.expiresAt` has no field in `IConnectionConfig` to go to; the
|
|
190
|
+
* provider seeded with the stored token reads the expiry from the JWT itself.
|
|
386
191
|
*/
|
|
387
|
-
async
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
// Get serviceUrl (required)
|
|
392
|
-
const serviceUrl = await this.getServiceUrl(destination, connConfig);
|
|
393
|
-
// Check if we have token or UAA credentials
|
|
394
|
-
const sessionToken = connConfig?.authorizationToken || connConfig?.sessionCookies;
|
|
395
|
-
const hasToken = !!sessionToken;
|
|
396
|
-
const hasAuthConfig = !!authConfig;
|
|
397
|
-
this.logger?.info(`[AuthBroker] Session check for ${destination}`, {
|
|
398
|
-
hasToken,
|
|
399
|
-
hasAuthConfig,
|
|
400
|
-
hasServiceUrl: !!serviceUrl,
|
|
192
|
+
async persist(destination, serviceUrl, connConfig, result) {
|
|
193
|
+
const isSaml = result.tokenType === 'saml';
|
|
194
|
+
await this.sessionStore.setConnectionConfig(destination, {
|
|
195
|
+
...(connConfig ?? {}),
|
|
401
196
|
serviceUrl,
|
|
402
|
-
authorizationToken:
|
|
403
|
-
|
|
404
|
-
|
|
197
|
+
authorizationToken: isSaml ? undefined : result.authorizationToken,
|
|
198
|
+
sessionCookies: isSaml ? result.authorizationToken : undefined,
|
|
199
|
+
authType: isSaml ? 'saml' : 'jwt',
|
|
405
200
|
});
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
throw error;
|
|
414
|
-
}
|
|
415
|
-
const serviceKeyAuthConfig = await this.getAuthorizationConfigFromServiceKey(destination);
|
|
416
|
-
const tokenResult = await this.requestTokens(destination, 'serviceKey');
|
|
417
|
-
await this.persistTokenResult(destination, serviceUrl, connConfig, serviceKeyAuthConfig, tokenResult);
|
|
418
|
-
this.logger?.info(`[AuthBroker] Token retrieved for ${destination} (initialized from service key)`, {
|
|
419
|
-
authorizationToken: (0, formatting_1.formatToken)(tokenResult.authorizationToken),
|
|
420
|
-
});
|
|
421
|
-
return tokenResult.authorizationToken;
|
|
422
|
-
}
|
|
423
|
-
// Step 1: Request tokens via provider (provider handles token lifecycle internally)
|
|
424
|
-
// Broker always calls provider.getTokens() - provider decides whether to return cached token,
|
|
425
|
-
// refresh, or perform login. Consumer doesn't need to know about token issues.
|
|
426
|
-
this.logger?.debug(`Step 1: Requesting tokens via provider for ${destination}`);
|
|
427
|
-
let lastError = null;
|
|
428
|
-
if (authConfig) {
|
|
429
|
-
if (!this.allowBrowserAuth && !authConfig.refreshToken) {
|
|
430
|
-
const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Session has no refresh token.`);
|
|
431
|
-
error.code = 'BROWSER_AUTH_REQUIRED';
|
|
432
|
-
error.destination = destination;
|
|
433
|
-
this.logger?.error(`Step 2: Browser auth required but disabled for ${destination}`);
|
|
434
|
-
throw error;
|
|
435
|
-
}
|
|
436
|
-
try {
|
|
437
|
-
const tokenResult = await this.requestTokens(destination, 'session');
|
|
438
|
-
await this.persistTokenResult(destination, serviceUrl, connConfig, authConfig, tokenResult);
|
|
439
|
-
this.logger?.info(`[AuthBroker] Token retrieved for ${destination} (via session)`, {
|
|
440
|
-
authorizationToken: (0, formatting_1.formatToken)(tokenResult.authorizationToken),
|
|
201
|
+
if (result.refreshToken) {
|
|
202
|
+
const sessionAuth = await this.read(destination, 'session authorization config', () => this.sessionStore.getAuthorizationConfig(destination));
|
|
203
|
+
if (sessionAuth) {
|
|
204
|
+
// The session holds its own credentials: only the refresh token changes.
|
|
205
|
+
await this.sessionStore.setAuthorizationConfig(destination, {
|
|
206
|
+
...sessionAuth,
|
|
207
|
+
refreshToken: result.refreshToken,
|
|
441
208
|
});
|
|
442
|
-
return tokenResult.authorizationToken;
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
446
|
-
this.logger?.debug(`Step 2: Token request via session failed for ${destination}: ${getErrorMessage(error)}, trying service key`);
|
|
447
209
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (!this.serviceKeyStore) {
|
|
457
|
-
const tokenType = this.tokenProvider?.tokenType;
|
|
458
|
-
if (tokenType === 'saml') {
|
|
459
|
-
const tokenResult = await this.requestTokens(destination, 'session');
|
|
460
|
-
await this.persistTokenResult(destination, serviceUrl, connConfig, authConfig || {}, tokenResult);
|
|
461
|
-
this.logger?.info(`[AuthBroker] Token retrieved for ${destination} (SAML without auth config)`, {
|
|
462
|
-
authorizationToken: (0, formatting_1.formatToken)(tokenResult.authorizationToken),
|
|
210
|
+
else {
|
|
211
|
+
// The credentials live in the service key and stay there. The session
|
|
212
|
+
// gets the refresh token alone — never the client secret.
|
|
213
|
+
const session = await this.read(destination, 'session', () => this.sessionStore.loadSession(destination));
|
|
214
|
+
await this.sessionStore.saveSession(destination, {
|
|
215
|
+
...(session ?? {}),
|
|
216
|
+
serviceUrl: session?.serviceUrl ?? serviceUrl,
|
|
217
|
+
refreshToken: result.refreshToken,
|
|
463
218
|
});
|
|
464
|
-
return tokenResult.authorizationToken;
|
|
465
|
-
}
|
|
466
|
-
if (lastError) {
|
|
467
|
-
throw lastError;
|
|
468
219
|
}
|
|
469
|
-
throw new Error(`Authorization config not found for ${destination}. Session has no auth config and serviceKeyStore is not available.`);
|
|
470
|
-
}
|
|
471
|
-
// If the session attempt already performed an *interactive* browser login
|
|
472
|
-
// and it failed (the user didn't complete it / it timed out), the serviceKey
|
|
473
|
-
// strategy would call the SAME provider.getTokens() again and start a
|
|
474
|
-
// duplicate browser login on the same redirect port — surfacing a misleading
|
|
475
|
-
// "Port in use" instead of the real cause. Don't retry interactive failures;
|
|
476
|
-
// propagate the original error. Transient/non-interactive session failures
|
|
477
|
-
// still fall through to the serviceKey attempt below.
|
|
478
|
-
if (lastError && isInteractiveAuthFailure(lastError)) {
|
|
479
|
-
this.logger?.debug(`Step 2: session login failed interactively for ${destination}; not retrying via service key (${getErrorMessage(lastError)})`);
|
|
480
|
-
throw lastError;
|
|
481
220
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
221
|
+
this.logger.info(`[AuthBroker] Token saved for ${destination}`, {
|
|
222
|
+
tokenType: result.tokenType ?? 'jwt',
|
|
223
|
+
authType: result.authType,
|
|
224
|
+
hasRefreshToken: !!result.refreshToken,
|
|
225
|
+
expiresAt: result.expiresAt
|
|
226
|
+
? new Date(result.expiresAt).toISOString()
|
|
227
|
+
: undefined,
|
|
228
|
+
expiresIn: result.expiresIn,
|
|
487
229
|
});
|
|
488
|
-
return tokenResult.authorizationToken;
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Force refresh token for destination.
|
|
492
|
-
* Uses refresh token from session if available, otherwise uses UAA credentials from session or service key.
|
|
493
|
-
* @param destination Destination name (e.g., "TRIAL")
|
|
494
|
-
* @returns Promise that resolves to new JWT token string
|
|
495
|
-
*/
|
|
496
|
-
async refreshToken(destination) {
|
|
497
|
-
this.logger?.debug(`Force refreshing token for destination: ${destination}`);
|
|
498
|
-
// Call getToken to trigger full refresh flow
|
|
499
|
-
return this.getToken(destination);
|
|
500
230
|
}
|
|
501
231
|
/**
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
232
|
+
* A store read where absence is an answer and anything else is not.
|
|
233
|
+
*
|
|
234
|
+
* A store says "nothing here" with `null`, or with `FILE_NOT_FOUND`, and the
|
|
235
|
+
* flow goes on to the next source. Any other failure — a service key that is
|
|
236
|
+
* not valid JSON, a file the process may not read — is a different problem
|
|
237
|
+
* with a different fix, and reaches the caller as the store raised it. It
|
|
238
|
+
* used to be logged and answered as absent, so the caller saw only the
|
|
239
|
+
* consequence ("missing required field 'serviceUrl'") and went looking for a
|
|
240
|
+
* file that was there.
|
|
505
241
|
*/
|
|
506
|
-
async
|
|
507
|
-
this.logger?.debug(`Getting authorization config for ${destination}`);
|
|
508
|
-
// Try session store first (has tokens)
|
|
509
|
-
this.logger?.debug(`Checking session store for authorization config: ${destination}`);
|
|
510
|
-
let sessionAuthConfig = null;
|
|
242
|
+
async read(destination, what, fn) {
|
|
511
243
|
try {
|
|
512
|
-
|
|
513
|
-
await this.sessionStore.getAuthorizationConfig(destination);
|
|
244
|
+
return await fn();
|
|
514
245
|
}
|
|
515
246
|
catch (error) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
this.logger?.debug(`Authorization config from session for ${destination}: hasUaaUrl(${!!sessionAuthConfig.uaaUrl}), hasRefreshToken(${!!sessionAuthConfig.refreshToken})`);
|
|
520
|
-
return sessionAuthConfig;
|
|
521
|
-
}
|
|
522
|
-
// Fall back to service key store (has UAA credentials) if available
|
|
523
|
-
if (this.serviceKeyStore) {
|
|
524
|
-
this.logger?.debug(`Checking service key store for authorization config: ${destination}`);
|
|
525
|
-
let serviceKeyAuthConfig = null;
|
|
526
|
-
try {
|
|
527
|
-
serviceKeyAuthConfig =
|
|
528
|
-
await this.serviceKeyStore.getAuthorizationConfig(destination);
|
|
529
|
-
}
|
|
530
|
-
catch (error) {
|
|
531
|
-
// Handle typed store errors
|
|
532
|
-
if (hasErrorCode(error)) {
|
|
533
|
-
if (error.code === interfaces_auth_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
534
|
-
this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
535
|
-
}
|
|
536
|
-
else if (error.code === interfaces_auth_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
537
|
-
this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
else {
|
|
544
|
-
this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
if (serviceKeyAuthConfig) {
|
|
548
|
-
this.logger?.debug(`Authorization config from service key for ${destination}: hasUaaUrl(${!!serviceKeyAuthConfig.uaaUrl})`);
|
|
549
|
-
return serviceKeyAuthConfig;
|
|
247
|
+
if (errorCode(error) === interfaces_auth_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
248
|
+
this.logger.debug(`No ${what} for ${destination}: file not found`);
|
|
249
|
+
return null;
|
|
550
250
|
}
|
|
251
|
+
throw error;
|
|
551
252
|
}
|
|
552
|
-
else {
|
|
553
|
-
this.logger?.debug(`Service key store not available for ${destination}`);
|
|
554
|
-
}
|
|
555
|
-
this.logger?.debug(`No authorization config found for ${destination}`);
|
|
556
|
-
return null;
|
|
557
253
|
}
|
|
558
254
|
/**
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
* @returns Promise that resolves to IConnectionConfig or null if not found
|
|
255
|
+
* Authorization configuration for the destination: the session's, else the
|
|
256
|
+
* service key's, else null.
|
|
562
257
|
*/
|
|
563
|
-
async
|
|
564
|
-
this.
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
try {
|
|
568
|
-
sessionConnConfig =
|
|
569
|
-
await this.sessionStore.getConnectionConfig(destination);
|
|
570
|
-
}
|
|
571
|
-
catch (error) {
|
|
572
|
-
this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${getErrorMessage(error)}`);
|
|
258
|
+
async getAuthorizationConfig(destination) {
|
|
259
|
+
const sessionAuth = await this.read(destination, 'session authorization config', () => this.sessionStore.getAuthorizationConfig(destination));
|
|
260
|
+
if (sessionAuth) {
|
|
261
|
+
return sessionAuth;
|
|
573
262
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const formattedToken = (0, formatting_1.formatToken)(sessionConnConfig.authorizationToken ||
|
|
578
|
-
sessionConnConfig.sessionCookies);
|
|
579
|
-
this.logger?.debug(`Connection config from session for ${destination}: token(${tokenLength} chars${formattedToken ? `, ${formattedToken}` : ''}), serviceUrl(${sessionConnConfig.serviceUrl ? 'yes' : 'no'})`);
|
|
580
|
-
return sessionConnConfig;
|
|
263
|
+
const serviceKeyStore = this.serviceKeyStore;
|
|
264
|
+
if (!serviceKeyStore) {
|
|
265
|
+
return null;
|
|
581
266
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (error.code === interfaces_auth_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
|
|
593
|
-
this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
|
|
594
|
-
}
|
|
595
|
-
else if (error.code === interfaces_auth_1.STORE_ERROR_CODES.PARSE_ERROR) {
|
|
596
|
-
this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
this.logger?.warn(`Failed to get connection config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
603
|
-
this.logger?.warn(`Failed to get connection config from service key store for ${destination}: ${getErrorMessage(error)}`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (serviceKeyConnConfig) {
|
|
607
|
-
this.logger?.debug(`Connection config from service key for ${destination}: serviceUrl(${serviceKeyConnConfig.serviceUrl ? 'yes' : 'no'}), token(none)`);
|
|
608
|
-
return serviceKeyConnConfig;
|
|
609
|
-
}
|
|
267
|
+
return this.read(destination, 'service key authorization config', () => serviceKeyStore.getAuthorizationConfig(destination));
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Connection configuration for the destination: the session's, else the
|
|
271
|
+
* service key's (which has URLs but no token), else null.
|
|
272
|
+
*/
|
|
273
|
+
async getConnectionConfig(destination) {
|
|
274
|
+
const sessionConn = await this.read(destination, 'session connection config', () => this.sessionStore.getConnectionConfig(destination));
|
|
275
|
+
if (sessionConn) {
|
|
276
|
+
return sessionConn;
|
|
610
277
|
}
|
|
611
|
-
|
|
612
|
-
|
|
278
|
+
const serviceKeyStore = this.serviceKeyStore;
|
|
279
|
+
if (!serviceKeyStore) {
|
|
280
|
+
return null;
|
|
613
281
|
}
|
|
614
|
-
this.
|
|
615
|
-
return null;
|
|
282
|
+
return this.read(destination, 'service key connection config', () => serviceKeyStore.getConnectionConfig(destination));
|
|
616
283
|
}
|
|
617
284
|
/**
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
* allowing the connection to handle token refresh transparently without knowing
|
|
622
|
-
* about authentication internals.
|
|
623
|
-
*
|
|
624
|
-
* **Usage:**
|
|
625
|
-
* ```typescript
|
|
626
|
-
* const broker = new AuthBroker(config);
|
|
627
|
-
* const tokenRefresher = broker.createTokenRefresher('TRIAL');
|
|
628
|
-
* const connection = new JwtAbapConnection(config, tokenRefresher);
|
|
629
|
-
* ```
|
|
630
|
-
*
|
|
631
|
-
* @param destination Destination name (e.g., "TRIAL")
|
|
632
|
-
* @returns ITokenRefresher implementation for the given destination
|
|
285
|
+
* An `ITokenRefresher` for one destination, for injection into a connection:
|
|
286
|
+
* `getToken()` is the broker's `getToken`, `refreshToken()` its forced
|
|
287
|
+
* `refreshToken`.
|
|
633
288
|
*/
|
|
634
289
|
createTokenRefresher(destination) {
|
|
635
|
-
const broker = this;
|
|
636
290
|
return {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
* Returns cached token if valid, otherwise refreshes and returns new token.
|
|
640
|
-
*/
|
|
641
|
-
async getToken() {
|
|
642
|
-
return broker.getToken(destination);
|
|
643
|
-
},
|
|
644
|
-
/**
|
|
645
|
-
* Force refresh token and save to session store.
|
|
646
|
-
* Always performs refresh, ignoring cached token validity.
|
|
647
|
-
*/
|
|
648
|
-
async refreshToken() {
|
|
649
|
-
return broker.refreshToken(destination);
|
|
650
|
-
},
|
|
291
|
+
getToken: () => this.getToken(destination),
|
|
292
|
+
refreshToken: () => this.refreshToken(destination),
|
|
651
293
|
};
|
|
652
294
|
}
|
|
653
295
|
}
|