@mcp-abap-adt/auth-providers 0.2.7 → 0.2.9
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 +55 -0
- package/README.md +37 -22
- package/dist/__tests__/helpers/configHelpers.d.ts +24 -29
- package/dist/__tests__/helpers/configHelpers.d.ts.map +1 -1
- package/dist/__tests__/helpers/configHelpers.js +96 -50
- package/dist/auth/browserAuth.d.ts.map +1 -1
- package/dist/auth/browserAuth.js +74 -32
- package/dist/providers/AuthorizationCodeProvider.d.ts +2 -1
- package/dist/providers/AuthorizationCodeProvider.d.ts.map +1 -1
- package/dist/providers/AuthorizationCodeProvider.js +63 -3
- package/dist/providers/BaseTokenProvider.d.ts +12 -1
- package/dist/providers/BaseTokenProvider.d.ts.map +1 -1
- package/dist/providers/BaseTokenProvider.js +89 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.9] - 2025-12-25
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Logging Improvements**: Enhanced logging for better debugging and readability
|
|
14
|
+
- **Date Formatting**: Expiration dates now displayed in readable format (`YYYY-MM-DD HH:MM:SS UTC`) instead of ISO format (`2025-12-25T11:08:15.000Z`)
|
|
15
|
+
- **Token Formatting**: Tokens are logged in truncated format (start...end) for security and readability
|
|
16
|
+
- **Browser Information**: Added logging of browser type and authorization URL before starting browser authentication
|
|
17
|
+
- **Token Lifecycle**: Improved logging of token acquisition, validation, and refresh operations with formatted dates
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Browser Auth Timeout**: Added 30-second timeout for browser-based authentication
|
|
21
|
+
- Prevents provider from blocking consumer indefinitely when user doesn't complete authentication
|
|
22
|
+
- Timeout error is thrown if authentication is not completed within 30 seconds
|
|
23
|
+
- Helps prevent hanging in automated tests and CI/CD environments
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **Test Hanging Issues**: Fixed Jest tests hanging after completion
|
|
27
|
+
- Added `forceExit: true` to Jest configuration to force exit after tests complete
|
|
28
|
+
- Improved cleanup of HTTP server and timers to prevent open handles
|
|
29
|
+
- Added protection against double execution of server close handlers
|
|
30
|
+
- Proper cleanup of `finishTimeoutId` timer in all scenarios (success, error, timeout)
|
|
31
|
+
- Server now resolves promise only after fully closing to ensure Jest can exit cleanly
|
|
32
|
+
|
|
33
|
+
## [0.2.8] - 2025-12-24
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- **Integration Tests**: Replaced mock-based unit tests with comprehensive integration tests using real YAML configuration
|
|
37
|
+
- Tests now use `test-config.yaml` for loading real service keys and session files
|
|
38
|
+
- Simplified test configuration structure: only `destination` and optional `destination_dir` (removed `abap`/`xsuaa` sections)
|
|
39
|
+
- Default paths: `~/.config/mcp-abap-adt` (Unix) or `%USERPROFILE%\Documents\mcp-abap-adt` (Windows)
|
|
40
|
+
- **Logging**: Migrated from `console.log` to structured logging using `@mcp-abap-adt/logger`
|
|
41
|
+
- All providers and browser auth functions now use `ILogger` interface
|
|
42
|
+
- Consistent structured logging with log levels (debug, info, warn, error)
|
|
43
|
+
- Better debugging with token validation details and execution flow
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- **Test Scenarios**: Comprehensive integration test coverage for token lifecycle
|
|
47
|
+
- Scenario 1 & 2: Token lifecycle - login via browser and reuse token from previous scenario
|
|
48
|
+
- Scenario 3: Expired session + expired refresh token - provider should re-authenticate via browser
|
|
49
|
+
- Token validation: Explicit validation of token expiration in all scenarios
|
|
50
|
+
- **Test Configuration**: Simplified `test-config.yaml.template` with detailed comments
|
|
51
|
+
- Only requires `destination` name (service key and session files are auto-resolved)
|
|
52
|
+
- Optional `destination_dir` (commented out by default, can be uncommented for custom paths)
|
|
53
|
+
- Clear documentation of test scenarios and file formats
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
- **Test Hanging Issues**: Fixed tests hanging due to browser and port conflicts
|
|
57
|
+
- Changed `browser: 'none'` to `browser: 'system'` for interactive authentication
|
|
58
|
+
- Each test scenario uses unique ports (3101, 3102, 3103) to avoid conflicts
|
|
59
|
+
- Improved server cleanup: `resolve(tokens)` called immediately after token exchange, server closes asynchronously
|
|
60
|
+
- Added delays after browser-based tests to allow ports to free up
|
|
61
|
+
- **Token Validation**: Added explicit token validation in all test scenarios
|
|
62
|
+
- Tests verify that returned tokens are valid and not expired
|
|
63
|
+
- Uses JWT `exp` claim validation with 60-second buffer (matching `BaseTokenProvider`)
|
|
64
|
+
|
|
10
65
|
## [0.2.7] - 2025-12-24
|
|
11
66
|
|
|
12
67
|
### Changed
|
package/README.md
CHANGED
|
@@ -207,7 +207,9 @@ const result = await provider.getTokens();
|
|
|
207
207
|
// result.refreshToken is undefined (client_credentials doesn't provide refresh tokens)
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
**Note**: The `browserAuthPort` parameter (default: 3001) configures the OAuth callback server port. If the requested port is already in use, an error will be thrown. You must specify a different port or free the port before starting authentication. The server properly closes all connections and frees the port after authentication completes, ensuring no lingering port occupation.
|
|
210
|
+
**Note**: The `browserAuthPort` parameter (default: 3001) configures the OAuth callback server port. If the requested port is already in use, an error will be thrown. You must specify a different port or free the port before starting authentication. The server properly closes all connections and frees the port after authentication completes, ensuring no lingering port occupation.
|
|
211
|
+
|
|
212
|
+
**Timeout**: Browser authentication has a 30-second timeout to prevent blocking the consumer. If authentication is not completed within 30 seconds, the operation will fail with a timeout error. This prevents the provider from hanging indefinitely when the user doesn't complete authentication.
|
|
211
213
|
|
|
212
214
|
**Process Termination Handling**: The OAuth callback server registers cleanup handlers for `SIGTERM`, `SIGINT`, `SIGHUP`, and `exit` signals. This ensures ports are properly freed even when MCP clients (like Cline) terminate the process before authentication completes. This is especially important for stdio servers where the client may kill the process at any time. On Windows, the `SIGBREAK` signal (Ctrl+Break) is also handled.
|
|
213
215
|
|
|
@@ -335,27 +337,33 @@ npm test
|
|
|
335
337
|
Integration tests work with real files from `tests/test-config.yaml`:
|
|
336
338
|
|
|
337
339
|
1. Copy `tests/test-config.yaml.template` to `tests/test-config.yaml`
|
|
338
|
-
2. Fill in real
|
|
340
|
+
2. Fill in real destination name
|
|
339
341
|
3. Run tests - integration tests will use real services if configured
|
|
340
342
|
|
|
341
343
|
```yaml
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
344
|
+
# Destination name (used for service key file: <destination>.json and session file: <destination>.env)
|
|
345
|
+
destination: "trial" # Example: "trial" -> looks for trial.json and trial.env
|
|
346
|
+
|
|
347
|
+
# Optional: Destination directory (base directory for service keys and sessions)
|
|
348
|
+
# If not specified, uses default platform paths:
|
|
349
|
+
# Unix: ~/.config/mcp-abap-adt
|
|
350
|
+
# Windows: %USERPROFILE%\Documents\mcp-abap-adt
|
|
351
|
+
# Uncomment and set if you need a custom path:
|
|
352
|
+
# destination_dir: ~/.config/mcp-abap-adt
|
|
351
353
|
```
|
|
352
354
|
|
|
353
355
|
Integration tests will skip if `test-config.yaml` is not configured or contains placeholder values.
|
|
354
356
|
|
|
357
|
+
**Test Scenarios**:
|
|
358
|
+
- **Scenario 1 & 2**: Token lifecycle - login via browser and reuse token from previous scenario
|
|
359
|
+
- **Scenario 3**: Expired session + expired refresh token - provider should re-authenticate via browser
|
|
360
|
+
- **Token validation**: Explicit validation of token expiration in all scenarios
|
|
361
|
+
|
|
355
362
|
**Note**:
|
|
356
|
-
-
|
|
357
|
-
-
|
|
358
|
-
-
|
|
363
|
+
- Integration tests use `AbapServiceKeyStore` and `AbapSessionStore` for loading service keys and sessions
|
|
364
|
+
- Tests may open a browser for authentication if no refresh token is available. This is expected behavior.
|
|
365
|
+
- Each test scenario uses a unique port (3101, 3102, 3103) to avoid port conflicts
|
|
366
|
+
- Tests use `browser: 'system'` for interactive authentication (not `'none'`)
|
|
359
367
|
|
|
360
368
|
### Debug Logging
|
|
361
369
|
|
|
@@ -363,26 +371,33 @@ To enable detailed logging during tests or runtime, set environment variables:
|
|
|
363
371
|
|
|
364
372
|
```bash
|
|
365
373
|
# Enable logging for auth providers
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
# Or enable browser auth specific logging
|
|
369
|
-
DEBUG_BROWSER_AUTH=true npm test
|
|
374
|
+
DEBUG_PROVIDER=true npm test
|
|
370
375
|
|
|
371
376
|
# Set log level (debug, info, warn, error)
|
|
372
377
|
LOG_LEVEL=debug npm test
|
|
373
378
|
```
|
|
374
379
|
|
|
375
|
-
Logging
|
|
380
|
+
Logging uses `@mcp-abap-adt/logger` package with structured logging:
|
|
376
381
|
- Token exchange stages (what we send, what we receive)
|
|
377
|
-
- Token information (lengths, previews)
|
|
382
|
+
- Token information (lengths, previews, expiration)
|
|
383
|
+
- Token validation checks (expiration, validity)
|
|
378
384
|
- Errors with details
|
|
379
385
|
|
|
380
386
|
Example output:
|
|
381
387
|
```
|
|
382
|
-
[
|
|
383
|
-
[
|
|
388
|
+
[INFO] ℹ️ [browserAuth] Exchanging code for token...
|
|
389
|
+
[INFO] ℹ️ Tokens received: accessToken(2263 chars), refreshToken(34 chars)
|
|
390
|
+
[DEBUG] 🐛 [BaseTokenProvider] Token validation check {"expiresAt":"2025-12-25 11:08:15 UTC","isValid":true}
|
|
391
|
+
[INFO] ℹ️ [browserAuth] Authorization URL: https://.../oauth/authorize?...
|
|
392
|
+
[INFO] ℹ️ [browserAuth] Browser: system
|
|
384
393
|
```
|
|
385
394
|
|
|
395
|
+
**Logging Features**:
|
|
396
|
+
- **Token Formatting**: Tokens are logged in truncated format (start...end) for security
|
|
397
|
+
- **Date Formatting**: Expiration dates are displayed in readable format (YYYY-MM-DD HH:MM:SS UTC) instead of ISO format
|
|
398
|
+
- **Browser Information**: Logs browser type and authorization URL for debugging
|
|
399
|
+
- **Token Lifecycle**: Detailed logging of token acquisition, validation, and refresh operations
|
|
400
|
+
|
|
386
401
|
## Dependencies
|
|
387
402
|
|
|
388
403
|
- `@mcp-abap-adt/interfaces` (^0.2.2) - Interface definitions and error code constants
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Configuration helpers for auth-providers tests
|
|
3
|
-
* Loads test configuration from test-config.yaml
|
|
3
|
+
* Loads test configuration from test-config.yaml
|
|
4
4
|
*/
|
|
5
5
|
export interface TestConfig {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
11
|
-
abap?: {
|
|
12
|
-
destination?: string;
|
|
13
|
-
};
|
|
14
|
-
xsuaa?: {
|
|
15
|
-
btp_destination?: string;
|
|
16
|
-
mcp_destination?: string;
|
|
17
|
-
mcp_url?: string;
|
|
18
|
-
};
|
|
19
|
-
};
|
|
6
|
+
destination?: string;
|
|
7
|
+
destination_dir?: string;
|
|
8
|
+
service_key_path?: string;
|
|
9
|
+
session_path?: string;
|
|
20
10
|
}
|
|
21
11
|
/**
|
|
22
12
|
* Load test configuration from YAML
|
|
@@ -26,26 +16,31 @@ export declare function loadTestConfig(): TestConfig;
|
|
|
26
16
|
/**
|
|
27
17
|
* Check if test config has real values (not placeholders)
|
|
28
18
|
*/
|
|
29
|
-
export declare function
|
|
19
|
+
export declare function hasRealConfigValue(config?: TestConfig): boolean;
|
|
30
20
|
/**
|
|
31
|
-
* Get
|
|
21
|
+
* Get destination from config
|
|
32
22
|
*/
|
|
33
|
-
export declare function
|
|
34
|
-
/**
|
|
35
|
-
* Get XSUAA destinations from config
|
|
36
|
-
*/
|
|
37
|
-
export declare function getXsuaaDestinations(config?: TestConfig): {
|
|
38
|
-
btp_destination: string | null;
|
|
39
|
-
mcp_url: string | null;
|
|
40
|
-
};
|
|
23
|
+
export declare function getDestination(config?: TestConfig): string | null;
|
|
41
24
|
/**
|
|
42
25
|
* Get service keys directory from config
|
|
43
|
-
*
|
|
26
|
+
* Uses base_dir/service-keys or default platform path
|
|
44
27
|
*/
|
|
45
|
-
export declare function getServiceKeysDir(config?: TestConfig): string
|
|
28
|
+
export declare function getServiceKeysDir(config?: TestConfig): string;
|
|
46
29
|
/**
|
|
47
30
|
* Get sessions directory from config
|
|
48
|
-
*
|
|
31
|
+
* Uses base_dir/sessions or default platform path
|
|
32
|
+
*/
|
|
33
|
+
export declare function getSessionsDir(config?: TestConfig): string;
|
|
34
|
+
/**
|
|
35
|
+
* Get service key file path
|
|
36
|
+
* Returns full path to service key file
|
|
37
|
+
*/
|
|
38
|
+
export declare function getServiceKeyPath(config?: TestConfig): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Get session file path
|
|
41
|
+
* Returns full path to session file
|
|
49
42
|
*/
|
|
50
|
-
export declare function
|
|
43
|
+
export declare function getSessionPath(config?: TestConfig): string | null;
|
|
44
|
+
export declare function getAbapDestination(config?: TestConfig): string | null;
|
|
45
|
+
export declare function hasRealConfig(config?: TestConfig, _section?: 'abap' | 'xsuaa'): boolean;
|
|
51
46
|
//# sourceMappingURL=configHelpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configHelpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/configHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE
|
|
1
|
+
{"version":3,"file":"configHelpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/configHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAkBD;;;GAGG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAyD3C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAO/D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAGjE;AA+BD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAa7D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAa1D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAcpE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAcjE;AAGD,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAErE;AAED,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,UAAU,EACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAC1B,OAAO,CAET"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Configuration helpers for auth-providers tests
|
|
4
|
-
* Loads test configuration from test-config.yaml
|
|
4
|
+
* Loads test configuration from test-config.yaml
|
|
5
5
|
*/
|
|
6
6
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
7
|
if (k2 === undefined) k2 = k;
|
|
@@ -38,11 +38,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.loadTestConfig = loadTestConfig;
|
|
41
|
-
exports.
|
|
42
|
-
exports.
|
|
43
|
-
exports.getXsuaaDestinations = getXsuaaDestinations;
|
|
41
|
+
exports.hasRealConfigValue = hasRealConfigValue;
|
|
42
|
+
exports.getDestination = getDestination;
|
|
44
43
|
exports.getServiceKeysDir = getServiceKeysDir;
|
|
45
44
|
exports.getSessionsDir = getSessionsDir;
|
|
45
|
+
exports.getServiceKeyPath = getServiceKeyPath;
|
|
46
|
+
exports.getSessionPath = getSessionPath;
|
|
47
|
+
exports.getAbapDestination = getAbapDestination;
|
|
48
|
+
exports.hasRealConfig = hasRealConfig;
|
|
46
49
|
const fs = __importStar(require("node:fs"));
|
|
47
50
|
const path = __importStar(require("node:path"));
|
|
48
51
|
const yaml = __importStar(require("js-yaml"));
|
|
@@ -112,75 +115,118 @@ function loadTestConfig() {
|
|
|
112
115
|
/**
|
|
113
116
|
* Check if test config has real values (not placeholders)
|
|
114
117
|
*/
|
|
115
|
-
function
|
|
116
|
-
|
|
118
|
+
function hasRealConfigValue(config) {
|
|
119
|
+
const cfg = config || loadTestConfig();
|
|
120
|
+
if (!cfg.destination) {
|
|
117
121
|
return false;
|
|
118
122
|
}
|
|
119
|
-
if
|
|
120
|
-
|
|
121
|
-
if (!abap?.destination) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
// Check if destination is not a placeholder
|
|
125
|
-
return !abap.destination.includes('<') && !abap.destination.includes('>');
|
|
126
|
-
}
|
|
127
|
-
if (section === 'xsuaa') {
|
|
128
|
-
const xsuaa = config.auth_broker.xsuaa;
|
|
129
|
-
if (!xsuaa?.btp_destination) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
// Check if values are not placeholders
|
|
133
|
-
return !xsuaa.btp_destination.includes('<');
|
|
134
|
-
}
|
|
135
|
-
return false;
|
|
123
|
+
// Check if destination is not a placeholder
|
|
124
|
+
return !cfg.destination.includes('<') && !cfg.destination.includes('>');
|
|
136
125
|
}
|
|
137
126
|
/**
|
|
138
|
-
* Get
|
|
127
|
+
* Get destination from config
|
|
139
128
|
*/
|
|
140
|
-
function
|
|
129
|
+
function getDestination(config) {
|
|
141
130
|
const cfg = config || loadTestConfig();
|
|
142
|
-
return cfg.
|
|
131
|
+
return cfg.destination || null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get default destination directory based on platform
|
|
135
|
+
*/
|
|
136
|
+
function getDefaultDestinationDir() {
|
|
137
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
138
|
+
if (process.platform === 'win32') {
|
|
139
|
+
return path.join(homeDir, 'Documents', 'mcp-abap-adt');
|
|
140
|
+
}
|
|
141
|
+
return path.join(homeDir, '.config', 'mcp-abap-adt');
|
|
143
142
|
}
|
|
144
143
|
/**
|
|
145
|
-
* Get
|
|
144
|
+
* Get destination directory from config or use default
|
|
146
145
|
*/
|
|
147
|
-
function
|
|
146
|
+
function getDestinationDir(config) {
|
|
148
147
|
const cfg = config || loadTestConfig();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
if (cfg.destination_dir) {
|
|
149
|
+
// Expand ~ to home directory
|
|
150
|
+
if (cfg.destination_dir.startsWith('~')) {
|
|
151
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
152
|
+
return cfg.destination_dir.replace('~', homeDir);
|
|
153
|
+
}
|
|
154
|
+
return cfg.destination_dir;
|
|
155
|
+
}
|
|
156
|
+
return getDefaultDestinationDir();
|
|
154
157
|
}
|
|
155
158
|
/**
|
|
156
159
|
* Get service keys directory from config
|
|
157
|
-
*
|
|
160
|
+
* Uses base_dir/service-keys or default platform path
|
|
158
161
|
*/
|
|
159
162
|
function getServiceKeysDir(config) {
|
|
160
163
|
const cfg = config || loadTestConfig();
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
167
|
-
return dir.replace('~', homeDir);
|
|
164
|
+
// If service_key_path is specified, return its directory
|
|
165
|
+
if (cfg.service_key_path) {
|
|
166
|
+
const projectRoot = findProjectRoot();
|
|
167
|
+
const fullPath = path.resolve(projectRoot, cfg.service_key_path);
|
|
168
|
+
return path.dirname(fullPath);
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
+
// Use destination_dir/service-keys
|
|
171
|
+
const destinationDir = getDestinationDir(cfg);
|
|
172
|
+
return path.join(destinationDir, 'service-keys');
|
|
170
173
|
}
|
|
171
174
|
/**
|
|
172
175
|
* Get sessions directory from config
|
|
173
|
-
*
|
|
176
|
+
* Uses base_dir/sessions or default platform path
|
|
174
177
|
*/
|
|
175
178
|
function getSessionsDir(config) {
|
|
176
179
|
const cfg = config || loadTestConfig();
|
|
177
|
-
|
|
178
|
-
if (
|
|
180
|
+
// If session_path is specified, return its directory
|
|
181
|
+
if (cfg.session_path) {
|
|
182
|
+
const projectRoot = findProjectRoot();
|
|
183
|
+
const fullPath = path.resolve(projectRoot, cfg.session_path);
|
|
184
|
+
return path.dirname(fullPath);
|
|
185
|
+
}
|
|
186
|
+
// Use destination_dir/sessions
|
|
187
|
+
const destinationDir = getDestinationDir(cfg);
|
|
188
|
+
return path.join(destinationDir, 'sessions');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get service key file path
|
|
192
|
+
* Returns full path to service key file
|
|
193
|
+
*/
|
|
194
|
+
function getServiceKeyPath(config) {
|
|
195
|
+
const cfg = config || loadTestConfig();
|
|
196
|
+
const destination = cfg.destination;
|
|
197
|
+
if (!destination)
|
|
198
|
+
return null;
|
|
199
|
+
// If service_key_path is specified, use it
|
|
200
|
+
if (cfg.service_key_path) {
|
|
201
|
+
const projectRoot = findProjectRoot();
|
|
202
|
+
return path.resolve(projectRoot, cfg.service_key_path);
|
|
203
|
+
}
|
|
204
|
+
// Construct from directory + destination
|
|
205
|
+
const serviceKeysDir = getServiceKeysDir(cfg);
|
|
206
|
+
return path.join(serviceKeysDir, `${destination}.json`);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get session file path
|
|
210
|
+
* Returns full path to session file
|
|
211
|
+
*/
|
|
212
|
+
function getSessionPath(config) {
|
|
213
|
+
const cfg = config || loadTestConfig();
|
|
214
|
+
const destination = cfg.destination;
|
|
215
|
+
if (!destination)
|
|
179
216
|
return null;
|
|
180
|
-
//
|
|
181
|
-
if (
|
|
182
|
-
const
|
|
183
|
-
return
|
|
217
|
+
// If session_path is specified, use it
|
|
218
|
+
if (cfg.session_path) {
|
|
219
|
+
const projectRoot = findProjectRoot();
|
|
220
|
+
return path.resolve(projectRoot, cfg.session_path);
|
|
184
221
|
}
|
|
185
|
-
|
|
222
|
+
// Construct from directory + destination
|
|
223
|
+
const sessionsDir = getSessionsDir(cfg);
|
|
224
|
+
return path.join(sessionsDir, `${destination}.env`);
|
|
225
|
+
}
|
|
226
|
+
// Legacy functions for backward compatibility
|
|
227
|
+
function getAbapDestination(config) {
|
|
228
|
+
return getDestination(config);
|
|
229
|
+
}
|
|
230
|
+
function hasRealConfig(config, _section) {
|
|
231
|
+
return hasRealConfigValue(config);
|
|
186
232
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/browserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAI9E,KAAK,iBAAiB,GAAG,oBAAoB,GAAG;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AA8BF;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAa,EACnB,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,GACnB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgDzD;AA6BD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,MAAiB,EAC1B,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,GAClB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"browserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/browserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAI9E,KAAK,iBAAiB,GAAG,oBAAoB,GAAG;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AA8BF;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAa,EACnB,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,GACnB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgDzD;AA6BD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,MAAiB,EAC1B,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,GAClB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAqjBzD"}
|
package/dist/auth/browserAuth.js
CHANGED
|
@@ -146,7 +146,9 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
|
|
|
146
146
|
}
|
|
147
147
|
return new Promise((originalResolve, originalReject) => {
|
|
148
148
|
let timeoutId = null;
|
|
149
|
+
let finishTimeoutId = null;
|
|
149
150
|
let cleanupDone = false;
|
|
151
|
+
let resolved = false;
|
|
150
152
|
const app = (0, express_1.default)();
|
|
151
153
|
const server = http.createServer(app);
|
|
152
154
|
// Disable keep-alive to ensure connections close immediately
|
|
@@ -164,6 +166,10 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
|
|
|
164
166
|
clearTimeout(timeoutId);
|
|
165
167
|
timeoutId = null;
|
|
166
168
|
}
|
|
169
|
+
if (finishTimeoutId) {
|
|
170
|
+
clearTimeout(finishTimeoutId);
|
|
171
|
+
finishTimeoutId = null;
|
|
172
|
+
}
|
|
167
173
|
if (server) {
|
|
168
174
|
try {
|
|
169
175
|
if (typeof server.closeAllConnections === 'function') {
|
|
@@ -189,8 +195,17 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
|
|
|
189
195
|
}
|
|
190
196
|
};
|
|
191
197
|
const resolve = (value) => {
|
|
192
|
-
if (
|
|
198
|
+
if (resolved)
|
|
199
|
+
return; // Prevent double resolution
|
|
200
|
+
resolved = true;
|
|
201
|
+
if (timeoutId) {
|
|
193
202
|
clearTimeout(timeoutId);
|
|
203
|
+
timeoutId = null;
|
|
204
|
+
}
|
|
205
|
+
if (finishTimeoutId) {
|
|
206
|
+
clearTimeout(finishTimeoutId);
|
|
207
|
+
finishTimeoutId = null;
|
|
208
|
+
}
|
|
194
209
|
removeCleanupListeners();
|
|
195
210
|
originalResolve(value);
|
|
196
211
|
};
|
|
@@ -212,10 +227,27 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
|
|
|
212
227
|
}
|
|
213
228
|
// Use provided authorization URL or build from authConfig
|
|
214
229
|
const authorizationUrl = authConfig.authorizationUrl ?? getJwtAuthorizationUrl(authConfig, PORT);
|
|
230
|
+
log?.info(`[browserAuth] Authorization URL: ${authorizationUrl}`);
|
|
231
|
+
log?.info(`[browserAuth] Server listening on port: ${PORT}`);
|
|
232
|
+
// Verify port in redirect_uri matches server port
|
|
233
|
+
const redirectUriMatch = authorizationUrl.match(/redirect_uri=([^&]+)/);
|
|
234
|
+
if (redirectUriMatch) {
|
|
235
|
+
const redirectUri = decodeURIComponent(redirectUriMatch[1]);
|
|
236
|
+
const urlPortMatch = redirectUri.match(/localhost:(\d+)/);
|
|
237
|
+
if (urlPortMatch) {
|
|
238
|
+
const urlPort = parseInt(urlPortMatch[1], 10);
|
|
239
|
+
if (urlPort !== PORT) {
|
|
240
|
+
log?.warn(`[browserAuth] WARNING: Port mismatch! URL has port ${urlPort}, but server listens on ${PORT}`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
log?.info(`[browserAuth] Port match: URL and server both use port ${PORT}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
215
247
|
// OAuth2 callback handler
|
|
216
248
|
app.get('/callback', async (req, res) => {
|
|
217
249
|
try {
|
|
218
|
-
log?.info(`Callback received: ${req.url}`);
|
|
250
|
+
log?.info(`[browserAuth] Callback received: ${req.url}`);
|
|
219
251
|
log?.debug(`Callback query: ${JSON.stringify(req.query)}`);
|
|
220
252
|
// Check for OAuth2 error parameters
|
|
221
253
|
const { error, error_description, error_uri } = req.query;
|
|
@@ -291,13 +323,14 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
|
|
|
291
323
|
return reject(new Error(`OAuth2 authentication failed: ${errorMsg}${error_uri ? ` (${error_uri})` : ''}`));
|
|
292
324
|
}
|
|
293
325
|
const { code } = req.query;
|
|
326
|
+
log?.info(`[browserAuth] Callback code received: ${code ? 'yes' : 'no'}`);
|
|
294
327
|
log?.debug(`Callback code received: ${code ? 'yes' : 'no'}`);
|
|
295
328
|
if (!code || typeof code !== 'string') {
|
|
296
|
-
log?.error(`Callback code missing`);
|
|
329
|
+
log?.error(`[browserAuth] Callback code missing`);
|
|
297
330
|
res.status(400).send('Error: Authorization code missing');
|
|
298
331
|
return reject(new Error('Authorization code missing'));
|
|
299
332
|
}
|
|
300
|
-
log?.
|
|
333
|
+
log?.info(`[browserAuth] Exchanging code for token...`);
|
|
301
334
|
// Send success page
|
|
302
335
|
const html = `<!DOCTYPE html>
|
|
303
336
|
<html lang="en">
|
|
@@ -356,37 +389,45 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
|
|
|
356
389
|
</div>
|
|
357
390
|
</body>
|
|
358
391
|
</html>`;
|
|
359
|
-
//
|
|
360
|
-
res.send(html);
|
|
361
|
-
// Wait for response to finish before closing server
|
|
362
|
-
res.on('finish', () => {
|
|
363
|
-
// Response finished, now we can safely close server
|
|
364
|
-
});
|
|
365
|
-
// Exchange code for tokens and close server
|
|
392
|
+
// Exchange code for tokens first
|
|
366
393
|
try {
|
|
394
|
+
log?.info(`[browserAuth] Starting token exchange...`);
|
|
367
395
|
const tokens = await exchangeCodeForToken(authConfig, code, PORT, log);
|
|
368
|
-
log?.info(`Tokens received: accessToken(${tokens.accessToken?.length || 0} chars), refreshToken(${tokens.refreshToken?.length || 0} chars)`);
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
const
|
|
396
|
+
log?.info(`[browserAuth] Tokens received: accessToken(${tokens.accessToken?.length || 0} chars), refreshToken(${tokens.refreshToken?.length || 0} chars)`);
|
|
397
|
+
// Send success page (non-blocking, doesn't affect promise)
|
|
398
|
+
res.send(html);
|
|
399
|
+
log?.info(`[browserAuth] Response sent, waiting for finish...`);
|
|
400
|
+
// Close all connections and server after response is sent
|
|
401
|
+
// Resolve promise AFTER server is closed to prevent Jest from hanging
|
|
402
|
+
let serverClosing = false;
|
|
403
|
+
const closeServerAndResolve = () => {
|
|
404
|
+
if (serverClosing)
|
|
405
|
+
return; // Prevent double execution
|
|
406
|
+
serverClosing = true;
|
|
407
|
+
if (finishTimeoutId) {
|
|
408
|
+
clearTimeout(finishTimeoutId);
|
|
409
|
+
finishTimeoutId = null;
|
|
410
|
+
}
|
|
411
|
+
log?.info(`[browserAuth] Response finished, closing server...`);
|
|
412
|
+
if (typeof server.closeAllConnections === 'function') {
|
|
413
|
+
server.closeAllConnections();
|
|
414
|
+
}
|
|
415
|
+
// Wait for server to close before resolving to prevent Jest from hanging
|
|
376
416
|
server.close(() => {
|
|
377
417
|
// Server closed - port should be freed
|
|
378
|
-
log?.
|
|
418
|
+
log?.info(`[browserAuth] Server closed, port ${PORT} should be freed`);
|
|
419
|
+
// Resolve after server is fully closed
|
|
420
|
+
log?.info(`[browserAuth] Resolving promise with tokens...`);
|
|
421
|
+
resolve({
|
|
422
|
+
accessToken: tokens.accessToken,
|
|
423
|
+
refreshToken: tokens.refreshToken,
|
|
424
|
+
});
|
|
379
425
|
});
|
|
380
426
|
};
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
else {
|
|
386
|
-
// Wait for response to finish
|
|
387
|
-
res.once('finish', closeServer);
|
|
388
|
-
}
|
|
389
|
-
resolve(tokens);
|
|
427
|
+
// Wait for response to finish, but add timeout to prevent hanging
|
|
428
|
+
res.once('finish', closeServerAndResolve);
|
|
429
|
+
// Fallback: if finish event doesn't fire within 1 second, close anyway
|
|
430
|
+
finishTimeoutId = setTimeout(closeServerAndResolve, 1000);
|
|
390
431
|
}
|
|
391
432
|
catch (error) {
|
|
392
433
|
if (typeof server.closeAllConnections === 'function') {
|
|
@@ -429,6 +470,7 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
|
|
|
429
470
|
}
|
|
430
471
|
});
|
|
431
472
|
serverInstance = server.listen(PORT, async () => {
|
|
473
|
+
log?.info(`[browserAuth] Server started on port ${PORT}`);
|
|
432
474
|
const browserApp = BROWSER_MAP[browser];
|
|
433
475
|
// Handle 'none' and 'headless' modes - log URL and wait for callback
|
|
434
476
|
// (for SSH/remote sessions or when browser should not be opened)
|
|
@@ -554,7 +596,7 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
|
|
|
554
596
|
}
|
|
555
597
|
}
|
|
556
598
|
});
|
|
557
|
-
// Timeout after
|
|
599
|
+
// Timeout after 30 seconds to prevent blocking consumer
|
|
558
600
|
timeoutId = setTimeout(() => {
|
|
559
601
|
if (serverInstance) {
|
|
560
602
|
if (typeof server.closeAllConnections === 'function') {
|
|
@@ -567,8 +609,8 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
|
|
|
567
609
|
log?.debug(`Server closed on timeout, port ${PORT} should be freed`);
|
|
568
610
|
});
|
|
569
611
|
}, 100);
|
|
570
|
-
reject(new Error('Authentication timeout.
|
|
612
|
+
reject(new Error('Authentication timeout after 30 seconds. Please try again.'));
|
|
571
613
|
}
|
|
572
|
-
},
|
|
614
|
+
}, 30 * 1000);
|
|
573
615
|
});
|
|
574
616
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses authorization_code grant type with browser-based OAuth2 flow.
|
|
5
5
|
* Supports pre-built authorization URLs and automatic refresh.
|
|
6
6
|
*/
|
|
7
|
-
import type { ITokenResult, OAuth2GrantType } from '@mcp-abap-adt/interfaces';
|
|
7
|
+
import type { ILogger, ITokenResult, OAuth2GrantType } from '@mcp-abap-adt/interfaces';
|
|
8
8
|
import { BaseTokenProvider } from './BaseTokenProvider';
|
|
9
9
|
export interface AuthorizationCodeProviderConfig {
|
|
10
10
|
uaaUrl: string;
|
|
@@ -15,6 +15,7 @@ export interface AuthorizationCodeProviderConfig {
|
|
|
15
15
|
redirectPort?: number;
|
|
16
16
|
accessToken?: string;
|
|
17
17
|
refreshToken?: string;
|
|
18
|
+
logger?: ILogger;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Authorization Code token provider
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthorizationCodeProvider.d.ts","sourceRoot":"","sources":["../../src/providers/AuthorizationCodeProvider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAEV,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,WAAW,+BAA+B;IAE9C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IAGrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"AuthorizationCodeProvider.d.ts","sourceRoot":"","sources":["../../src/providers/AuthorizationCodeProvider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAEV,OAAO,EACP,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,WAAW,+BAA+B;IAE9C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IAGrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,iBAAiB;IAC9D,OAAO,CAAC,MAAM,CAAkC;gBAEpC,MAAM,EAAE,+BAA+B;IA8D7C,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAIxC,SAAS,CAAC,WAAW,IAAI,eAAe;cAIxB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;cA2ErC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CA0CxD"}
|
|
@@ -22,6 +22,17 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
|
|
|
22
22
|
constructor(config) {
|
|
23
23
|
super();
|
|
24
24
|
this.config = config;
|
|
25
|
+
this.logger = config.logger;
|
|
26
|
+
this.logger?.info('[AuthorizationCodeProvider] Provider created', {
|
|
27
|
+
uaaUrl: config.uaaUrl,
|
|
28
|
+
clientId: config.clientId,
|
|
29
|
+
hasAccessToken: !!config.accessToken,
|
|
30
|
+
hasRefreshToken: !!config.refreshToken,
|
|
31
|
+
accessToken: this.formatToken(config.accessToken),
|
|
32
|
+
refreshToken: this.formatToken(config.refreshToken),
|
|
33
|
+
browser: config.browser || 'none',
|
|
34
|
+
redirectPort: config.redirectPort || 3001,
|
|
35
|
+
});
|
|
25
36
|
const missingFields = [];
|
|
26
37
|
if (!config.uaaUrl) {
|
|
27
38
|
missingFields.push('uaaUrl');
|
|
@@ -43,9 +54,19 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
|
|
|
43
54
|
this.authorizationToken = config.accessToken;
|
|
44
55
|
// Parse expiration from JWT
|
|
45
56
|
this.expiresAt = this.parseExpirationFromJWT(config.accessToken);
|
|
57
|
+
this.logger?.info('[AuthorizationCodeProvider] Initialized with access token', {
|
|
58
|
+
accessToken: this.formatToken(config.accessToken),
|
|
59
|
+
hasExpiresAt: !!this.expiresAt,
|
|
60
|
+
expiresAt: this.expiresAt
|
|
61
|
+
? this.formatExpirationDate(this.expiresAt)
|
|
62
|
+
: undefined,
|
|
63
|
+
});
|
|
46
64
|
}
|
|
47
65
|
if (config.refreshToken) {
|
|
48
66
|
this.refreshToken = config.refreshToken;
|
|
67
|
+
this.logger?.info('[AuthorizationCodeProvider] Initialized with refresh token', {
|
|
68
|
+
refreshToken: this.formatToken(config.refreshToken),
|
|
69
|
+
});
|
|
49
70
|
}
|
|
50
71
|
}
|
|
51
72
|
async getTokens() {
|
|
@@ -68,8 +89,36 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
|
|
|
68
89
|
}
|
|
69
90
|
// Use provided browser or default to 'none' (prints URL to console)
|
|
70
91
|
const browser = this.config.browser || 'none';
|
|
71
|
-
const
|
|
72
|
-
|
|
92
|
+
const redirectPort = this.config.redirectPort || 3001;
|
|
93
|
+
// Build authorization URL for logging (same logic as in startBrowserAuth)
|
|
94
|
+
const authorizationUrl = authConfig.authorizationUrl ??
|
|
95
|
+
`${authConfig.uaaUrl}/oauth/authorize?client_id=${encodeURIComponent(authConfig.uaaClientId)}&redirect_uri=${encodeURIComponent(`http://localhost:${redirectPort}/callback`)}&response_type=code`;
|
|
96
|
+
this.logger?.info('[AuthorizationCodeProvider] Performing login via browser', {
|
|
97
|
+
browser,
|
|
98
|
+
redirectPort,
|
|
99
|
+
authorizationUrl,
|
|
100
|
+
uaaUrl: authConfig.uaaUrl,
|
|
101
|
+
clientId: authConfig.uaaClientId,
|
|
102
|
+
});
|
|
103
|
+
// Wrap startBrowserAuth with timeout
|
|
104
|
+
const timeoutMs = 30 * 1000; // 30 seconds
|
|
105
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
reject(new Error(`Authentication timeout after ${timeoutMs / 1000} seconds. Please try again.`));
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
});
|
|
110
|
+
const result = await Promise.race([
|
|
111
|
+
(0, browserAuth_1.startBrowserAuth)(authConfig, browser, this.logger || undefined, // Pass logger to browserAuth
|
|
112
|
+
redirectPort),
|
|
113
|
+
timeoutPromise,
|
|
114
|
+
]);
|
|
115
|
+
this.logger?.info('[AuthorizationCodeProvider] Login completed', {
|
|
116
|
+
hasAccessToken: !!result.accessToken,
|
|
117
|
+
hasRefreshToken: !!result.refreshToken,
|
|
118
|
+
accessToken: this.formatToken(result.accessToken),
|
|
119
|
+
refreshToken: this.formatToken(result.refreshToken),
|
|
120
|
+
accessTokenLength: result.accessToken?.length || 0,
|
|
121
|
+
});
|
|
73
122
|
// Parse expiration from JWT
|
|
74
123
|
const expiresIn = this.calculateExpiresIn(result.accessToken);
|
|
75
124
|
return {
|
|
@@ -83,9 +132,17 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
|
|
|
83
132
|
if (!this.refreshToken) {
|
|
84
133
|
throw new Error('Refresh token is required for refresh');
|
|
85
134
|
}
|
|
135
|
+
this.logger?.info('[AuthorizationCodeProvider] Refreshing token');
|
|
86
136
|
// Try refresh first
|
|
87
137
|
try {
|
|
88
138
|
const result = await (0, tokenRefresher_1.refreshJwtToken)(this.refreshToken, this.config.uaaUrl, this.config.clientId, this.config.clientSecret);
|
|
139
|
+
this.logger?.info('[AuthorizationCodeProvider] Token refresh completed', {
|
|
140
|
+
hasAccessToken: !!result.accessToken,
|
|
141
|
+
hasRefreshToken: !!result.refreshToken,
|
|
142
|
+
newAccessToken: this.formatToken(result.accessToken),
|
|
143
|
+
newRefreshToken: this.formatToken(result.refreshToken),
|
|
144
|
+
oldRefreshToken: this.formatToken(this.refreshToken),
|
|
145
|
+
});
|
|
89
146
|
const expiresIn = this.calculateExpiresIn(result.accessToken);
|
|
90
147
|
return {
|
|
91
148
|
authorizationToken: result.accessToken,
|
|
@@ -94,7 +151,10 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
|
|
|
94
151
|
expiresIn,
|
|
95
152
|
};
|
|
96
153
|
}
|
|
97
|
-
catch (
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.logger?.warn('[AuthorizationCodeProvider] Token refresh failed, falling back to login', {
|
|
156
|
+
error: error instanceof Error ? error.message : String(error),
|
|
157
|
+
});
|
|
98
158
|
// Refresh failed - try login (will use uaaUrl + clientId to build URL)
|
|
99
159
|
return await this.performLogin();
|
|
100
160
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Expiration checking
|
|
8
8
|
* - Automatic refresh/relogin
|
|
9
9
|
*/
|
|
10
|
-
import type { ITokenProvider, ITokenResult, OAuth2GrantType } from '@mcp-abap-adt/interfaces';
|
|
10
|
+
import type { ILogger, ITokenProvider, ITokenResult, OAuth2GrantType } from '@mcp-abap-adt/interfaces';
|
|
11
11
|
/**
|
|
12
12
|
* Abstract base class for token providers
|
|
13
13
|
*
|
|
@@ -21,6 +21,17 @@ export declare abstract class BaseTokenProvider implements ITokenProvider {
|
|
|
21
21
|
protected authorizationToken?: string;
|
|
22
22
|
protected refreshToken?: string;
|
|
23
23
|
protected expiresAt?: number;
|
|
24
|
+
protected logger?: ILogger;
|
|
25
|
+
/**
|
|
26
|
+
* Format timestamp to readable date/time string
|
|
27
|
+
* @param timestamp Timestamp in milliseconds
|
|
28
|
+
* @returns Formatted date string (e.g., "2025-12-25 19:21:27 UTC")
|
|
29
|
+
*/
|
|
30
|
+
protected formatExpirationDate(timestamp: number): string;
|
|
31
|
+
/**
|
|
32
|
+
* Format token for logging (start...end)
|
|
33
|
+
*/
|
|
34
|
+
protected formatToken(token?: string): string | undefined;
|
|
24
35
|
/**
|
|
25
36
|
* Check if current token is valid (not expired)
|
|
26
37
|
* @returns true if token exists and is not expired, false otherwise
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseTokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/BaseTokenProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;GAQG;AACH,8BAAsB,iBAAkB,YAAW,cAAc;IAC/D,SAAS,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACtC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"BaseTokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/BaseTokenProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;GAQG;AACH,8BAAsB,iBAAkB,YAAW,cAAc;IAC/D,SAAS,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACtC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAWzD;;OAEG;IACH,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMzD;;;OAGG;IACH,SAAS,CAAC,YAAY,IAAI,OAAO;IAyBjC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAExD;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;IAE1D;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,IAAI,eAAe;IAEjD;;;;;;;;OAQG;IACG,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAuElC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmB3E;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAoBlD;;;;OAIG;IACH,SAAS,CAAC,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IA0BnE;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAShE"}
|
|
@@ -23,17 +23,56 @@ class BaseTokenProvider {
|
|
|
23
23
|
authorizationToken;
|
|
24
24
|
refreshToken;
|
|
25
25
|
expiresAt; // timestamp in milliseconds
|
|
26
|
+
logger;
|
|
27
|
+
/**
|
|
28
|
+
* Format timestamp to readable date/time string
|
|
29
|
+
* @param timestamp Timestamp in milliseconds
|
|
30
|
+
* @returns Formatted date string (e.g., "2025-12-25 19:21:27 UTC")
|
|
31
|
+
*/
|
|
32
|
+
formatExpirationDate(timestamp) {
|
|
33
|
+
const date = new Date(timestamp);
|
|
34
|
+
const year = date.getUTCFullYear();
|
|
35
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
36
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
37
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
38
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
39
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
|
40
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Format token for logging (start...end)
|
|
44
|
+
*/
|
|
45
|
+
formatToken(token) {
|
|
46
|
+
if (!token)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (token.length <= 50)
|
|
49
|
+
return token;
|
|
50
|
+
return `${token.substring(0, 25)}...${token.substring(token.length - 25)}`;
|
|
51
|
+
}
|
|
26
52
|
/**
|
|
27
53
|
* Check if current token is valid (not expired)
|
|
28
54
|
* @returns true if token exists and is not expired, false otherwise
|
|
29
55
|
*/
|
|
30
56
|
isTokenValid() {
|
|
31
57
|
if (!this.authorizationToken || !this.expiresAt) {
|
|
58
|
+
this.logger?.debug('[BaseTokenProvider] Token invalid: missing token or expiration', {
|
|
59
|
+
hasToken: !!this.authorizationToken,
|
|
60
|
+
hasExpiresAt: !!this.expiresAt,
|
|
61
|
+
});
|
|
32
62
|
return false;
|
|
33
63
|
}
|
|
34
64
|
// Add 60 second buffer to account for clock skew and network latency
|
|
35
65
|
const bufferMs = 60 * 1000;
|
|
36
|
-
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const isValid = now < this.expiresAt - bufferMs;
|
|
68
|
+
this.logger?.debug('[BaseTokenProvider] Token validation check', {
|
|
69
|
+
now: this.formatExpirationDate(now),
|
|
70
|
+
expiresAt: this.formatExpirationDate(this.expiresAt),
|
|
71
|
+
expiresIn: Math.floor((this.expiresAt - now) / 1000),
|
|
72
|
+
isValid,
|
|
73
|
+
bufferMs,
|
|
74
|
+
});
|
|
75
|
+
return isValid;
|
|
37
76
|
}
|
|
38
77
|
/**
|
|
39
78
|
* Main method - handles token lifecycle
|
|
@@ -45,12 +84,25 @@ class BaseTokenProvider {
|
|
|
45
84
|
* @returns Promise that resolves to token result
|
|
46
85
|
*/
|
|
47
86
|
async getTokens() {
|
|
87
|
+
this.logger?.debug('[BaseTokenProvider] getTokens called', {
|
|
88
|
+
hasToken: !!this.authorizationToken,
|
|
89
|
+
hasExpiresAt: !!this.expiresAt,
|
|
90
|
+
hasRefreshToken: !!this.refreshToken,
|
|
91
|
+
currentToken: this.formatToken(this.authorizationToken),
|
|
92
|
+
});
|
|
48
93
|
// If token is valid, return cached
|
|
49
|
-
|
|
94
|
+
const isValid = this.isTokenValid();
|
|
95
|
+
if (isValid) {
|
|
50
96
|
const authorizationToken = this.authorizationToken;
|
|
51
97
|
if (!authorizationToken) {
|
|
52
98
|
throw new Error('Authorization token is missing.');
|
|
53
99
|
}
|
|
100
|
+
this.logger?.info('[BaseTokenProvider] Returning cached valid token', {
|
|
101
|
+
token: this.formatToken(authorizationToken),
|
|
102
|
+
expiresIn: this.expiresAt
|
|
103
|
+
? Math.floor((this.expiresAt - Date.now()) / 1000)
|
|
104
|
+
: undefined,
|
|
105
|
+
});
|
|
54
106
|
return {
|
|
55
107
|
authorizationToken,
|
|
56
108
|
refreshToken: this.refreshToken,
|
|
@@ -62,12 +114,23 @@ class BaseTokenProvider {
|
|
|
62
114
|
}
|
|
63
115
|
// Try refresh if we have refresh token
|
|
64
116
|
if (this.refreshToken) {
|
|
117
|
+
this.logger?.info('[BaseTokenProvider] Token invalid, attempting refresh', {
|
|
118
|
+
oldToken: this.formatToken(this.authorizationToken),
|
|
119
|
+
refreshToken: this.formatToken(this.refreshToken),
|
|
120
|
+
});
|
|
65
121
|
try {
|
|
66
122
|
const result = await this.performRefresh();
|
|
67
123
|
this.updateTokens(result);
|
|
124
|
+
this.logger?.info('[BaseTokenProvider] Token refreshed successfully', {
|
|
125
|
+
newToken: this.formatToken(result.authorizationToken),
|
|
126
|
+
newRefreshToken: this.formatToken(result.refreshToken),
|
|
127
|
+
});
|
|
68
128
|
return result;
|
|
69
129
|
}
|
|
70
|
-
catch (
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.logger?.warn('[BaseTokenProvider] Refresh failed', {
|
|
132
|
+
error: error instanceof Error ? error.message : String(error),
|
|
133
|
+
});
|
|
71
134
|
// Refresh failed - need to login
|
|
72
135
|
// Clear refresh token as it's invalid
|
|
73
136
|
this.refreshToken = undefined;
|
|
@@ -75,23 +138,37 @@ class BaseTokenProvider {
|
|
|
75
138
|
}
|
|
76
139
|
}
|
|
77
140
|
// Perform login
|
|
141
|
+
this.logger?.info('[BaseTokenProvider] Token invalid and no refresh token, performing login');
|
|
78
142
|
const result = await this.performLogin();
|
|
79
143
|
this.updateTokens(result);
|
|
144
|
+
this.logger?.info('[BaseTokenProvider] Login completed', {
|
|
145
|
+
newToken: this.formatToken(result.authorizationToken),
|
|
146
|
+
newRefreshToken: this.formatToken(result.refreshToken),
|
|
147
|
+
});
|
|
80
148
|
return result;
|
|
81
149
|
}
|
|
82
150
|
async validateToken(_token, _serviceUrl) {
|
|
151
|
+
this.logger?.debug('[BaseTokenProvider] Validating token');
|
|
83
152
|
const expiresAt = this.parseExpirationFromJWT(_token);
|
|
84
153
|
if (!expiresAt) {
|
|
154
|
+
this.logger?.warn('[BaseTokenProvider] Token validation failed: cannot parse expiration');
|
|
85
155
|
return false;
|
|
86
156
|
}
|
|
87
157
|
const bufferMs = 60 * 1000;
|
|
88
|
-
|
|
158
|
+
const isValid = Date.now() < expiresAt - bufferMs;
|
|
159
|
+
this.logger?.info('[BaseTokenProvider] Token validation result', {
|
|
160
|
+
isValid,
|
|
161
|
+
expiresAt: this.formatExpirationDate(expiresAt),
|
|
162
|
+
expiresIn: Math.floor((expiresAt - Date.now()) / 1000),
|
|
163
|
+
});
|
|
164
|
+
return isValid;
|
|
89
165
|
}
|
|
90
166
|
/**
|
|
91
167
|
* Update internal token cache from result
|
|
92
168
|
* @param result Token result to cache
|
|
93
169
|
*/
|
|
94
170
|
updateTokens(result) {
|
|
171
|
+
const oldToken = this.formatToken(this.authorizationToken);
|
|
95
172
|
this.authorizationToken = result.authorizationToken;
|
|
96
173
|
this.refreshToken = result.refreshToken;
|
|
97
174
|
if (result.expiresIn) {
|
|
@@ -101,6 +178,14 @@ class BaseTokenProvider {
|
|
|
101
178
|
// Try to parse expiration from JWT if expiresIn not provided
|
|
102
179
|
this.expiresAt = this.parseExpirationFromJWT(result.authorizationToken);
|
|
103
180
|
}
|
|
181
|
+
this.logger?.info('[BaseTokenProvider] Tokens updated', {
|
|
182
|
+
oldToken,
|
|
183
|
+
newToken: this.formatToken(result.authorizationToken),
|
|
184
|
+
newRefreshToken: this.formatToken(result.refreshToken),
|
|
185
|
+
expiresAt: this.expiresAt
|
|
186
|
+
? this.formatExpirationDate(this.expiresAt)
|
|
187
|
+
: undefined,
|
|
188
|
+
});
|
|
104
189
|
}
|
|
105
190
|
/**
|
|
106
191
|
* Parse expiration time from JWT token
|