@mcp-abap-adt/auth-broker 0.2.11 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,13 @@ Thank you to all contributors! See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the co
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.2.12] - 2025-12-25
15
+
16
+ ### Changed
17
+ - **Auth flow**: Broker now relies on `ITokenProvider.getTokens()` with no parameters and expects providers to manage refresh/re-auth internally.
18
+ - **Constructor**: `tokenProvider` is required and `allowBrowserAuth` is supported for non-interactive flows.
19
+ - **Docs**: Updated usage/architecture/export docs to reflect provider injection, new flow, and CLI usage.
20
+
14
21
  ## [0.2.11] - 2025-12-23
15
22
 
16
23
  ### Changed
package/README.md CHANGED
@@ -7,7 +7,7 @@ JWT authentication broker for MCP ABAP ADT server. Manages authentication tokens
7
7
  - 🔐 **Destination-based Authentication**: Load tokens based on `x-mcp-destination` header
8
8
  - 📁 **Environment File Support**: Automatically loads tokens from `{destination}.env` files
9
9
  - 🔄 **Automatic Token Refresh**: Refreshes expired tokens using service keys from `{destination}.json` files
10
- - ✅ **Token Validation**: Validates tokens by testing connection to SAP system
10
+ - ✅ **Token Validation**: Validates tokens via provider (if `validateToken` is implemented)
11
11
  - 💾 **Token Caching**: In-memory caching for improved performance
12
12
  - 🔧 **Configurable Base Path**: Customize where `.env` and `.json` files are stored
13
13
 
@@ -19,19 +19,26 @@ npm install @mcp-abap-adt/auth-broker
19
19
 
20
20
  ## Usage
21
21
 
22
- ### Basic Usage (Session Only)
22
+ ### Basic Usage (Provider Required)
23
23
 
24
- If your sessionStore contains valid UAA credentials, you only need to provide `sessionStore`:
24
+ AuthBroker requires a token provider configured for the destination:
25
25
 
26
26
  ```typescript
27
27
  import { AuthBroker, AbapSessionStore } from '@mcp-abap-adt/auth-broker';
28
+ import { AuthorizationCodeProvider } from '@mcp-abap-adt/auth-providers';
29
+
30
+ const tokenProvider = new AuthorizationCodeProvider({
31
+ uaaUrl: 'https://...authentication...hana.ondemand.com',
32
+ clientId: '...',
33
+ clientSecret: '...',
34
+ browser: 'system',
35
+ });
28
36
 
29
- // Session-only mode - works if session has UAA credentials
30
37
  const broker = new AuthBroker({
31
38
  sessionStore: new AbapSessionStore('/path/to/destinations'),
39
+ tokenProvider,
32
40
  });
33
41
 
34
- // Get token - uses direct UAA HTTP requests automatically
35
42
  const token = await broker.getToken('TRIAL');
36
43
  ```
37
44
 
@@ -40,37 +47,52 @@ const token = await broker.getToken('TRIAL');
40
47
  For maximum flexibility, provide all three dependencies:
41
48
 
42
49
  ```typescript
43
- import {
44
- AuthBroker,
45
- AbapServiceKeyStore,
46
- AbapSessionStore,
47
- BtpTokenProvider
50
+ import {
51
+ AuthBroker,
52
+ AbapServiceKeyStore,
53
+ AbapSessionStore,
48
54
  } from '@mcp-abap-adt/auth-broker';
55
+ import { AuthorizationCodeProvider } from '@mcp-abap-adt/auth-providers';
49
56
 
50
57
  const broker = new AuthBroker({
51
58
  sessionStore: new AbapSessionStore('/path/to/destinations'),
52
59
  serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'), // optional
53
- tokenProvider: new BtpTokenProvider(), // optional
60
+ tokenProvider: new AuthorizationCodeProvider({
61
+ uaaUrl: 'https://...authentication...hana.ondemand.com',
62
+ clientId: '...',
63
+ clientSecret: '...',
64
+ browser: 'system',
65
+ }),
54
66
  }, 'chrome', logger);
55
67
 
56
68
  // Disable browser authentication for headless/stdio environments (e.g., MCP with Cline)
57
69
  const brokerNoBrowser = new AuthBroker({
58
70
  sessionStore: new AbapSessionStore('/path/to/destinations'),
59
71
  serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'),
60
- tokenProvider: new BtpTokenProvider(),
72
+ tokenProvider: new AuthorizationCodeProvider({
73
+ uaaUrl: 'https://...authentication...hana.ondemand.com',
74
+ clientId: '...',
75
+ clientSecret: '...',
76
+ browser: 'none',
77
+ }),
61
78
  allowBrowserAuth: false, // Throws BROWSER_AUTH_REQUIRED if browser auth needed
62
79
  }, 'chrome', logger);
63
80
  ```
64
81
 
65
82
  ### Session + Service Key (For Initialization)
66
83
 
67
- If you need to initialize sessions from service keys:
84
+ If you need to initialize sessions from service keys, create the provider from service key auth config:
68
85
 
69
86
  ```typescript
70
87
  const broker = new AuthBroker({
71
88
  sessionStore: new AbapSessionStore('/path/to/destinations'),
72
89
  serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'),
73
- // tokenProvider optional - direct UAA requests will be used from service key
90
+ tokenProvider: new AuthorizationCodeProvider({
91
+ uaaUrl: 'https://...authentication...hana.ondemand.com',
92
+ clientId: '...',
93
+ clientSecret: '...',
94
+ browser: 'system',
95
+ }),
74
96
  });
75
97
  ```
76
98
 
@@ -94,16 +116,19 @@ To avoid port conflicts with browser authentication:
94
116
  const broker = new AuthBroker({
95
117
  sessionStore: new AbapSessionStore('/path/to/destinations'),
96
118
  serviceKeyStore: new AbapServiceKeyStore('/path/to/destinations'),
97
- tokenProvider: new BtpTokenProvider(4001), // Custom port for OAuth callback server
119
+ tokenProvider: new AuthorizationCodeProvider({
120
+ uaaUrl: 'https://...authentication...hana.ondemand.com',
121
+ clientId: '...',
122
+ clientSecret: '...',
123
+ browser: 'system',
124
+ redirectPort: 4001,
125
+ }),
98
126
  }, 'chrome');
99
127
  ```
100
128
 
101
- **Note**: The `BtpTokenProvider` automatically finds an available port if the requested port is in use. This prevents `EADDRINUSE` errors when multiple stdio servers run simultaneously. The server properly closes all connections and frees the port after authentication completes, ensuring no lingering port occupation.
102
-
103
129
  ### Getting Tokens
104
130
 
105
131
  ```typescript
106
- // Get token - automatically uses direct UAA requests if UAA credentials available
107
132
  const token = await broker.getToken('TRIAL');
108
133
 
109
134
  // Force refresh token
@@ -282,7 +307,7 @@ This package supports two types of BTP authentication:
282
307
  **Interface-Only Communication**: This package follows a fundamental development principle: **all interactions with external dependencies happen ONLY through interfaces**. The code knows **NOTHING beyond what is defined in the interfaces**.
283
308
 
284
309
  This means:
285
- - Does not know about concrete implementation classes (e.g., `AbapSessionStore`, `BtpTokenProvider`)
310
+ - Does not know about concrete implementation classes (e.g., `AbapSessionStore`, `AuthorizationCodeProvider`)
286
311
  - Does not know about internal data structures or methods not defined in interfaces
287
312
  - Does not make assumptions about implementation behavior beyond interface contracts
288
313
  - Does not access properties or methods not explicitly defined in interfaces
@@ -302,13 +327,11 @@ The `@mcp-abap-adt/auth-broker` package defines **interfaces** and provides **or
302
327
  - **Orchestrates authentication flows**: Coordinates token retrieval, validation, and refresh using provided stores and providers
303
328
  - **Manages token lifecycle**: Handles token caching, validation, and automatic refresh
304
329
  - **Works with interfaces only**: Uses `IServiceKeyStore`, `ISessionStore`, and `ITokenProvider` interfaces without knowing concrete implementations
305
- - **Delegates to providers**: Calls `tokenProvider.getConnectionConfig()` to obtain tokens and connection configuration
306
- - **Delegates to stores**: Uses `sessionStore.setConnectionConfig()` to save tokens and connection configuration
330
+ - **Delegates to providers**: Calls `tokenProvider.getTokens()` to obtain tokens
331
+ - **Delegates to stores**: Saves tokens and connection configuration to `sessionStore`
307
332
 
308
333
  #### What AuthBroker Does NOT Do
309
334
 
310
- - **Does NOT know about `serviceUrl`**: `AuthBroker` does not know whether a specific `ISessionStore` implementation requires `serviceUrl` or not. It simply passes the `IConnectionConfig` returned by `tokenProvider` to `sessionStore.setConnectionConfig()`
311
- - **Does NOT merge configurations**: `AuthBroker` does not merge `serviceUrl` from service keys with connection config from token providers. This is the responsibility of the consumer or the session store implementation
312
335
  - **Does NOT implement storage**: File I/O, parsing, and storage logic are handled by concrete store implementations from `@mcp-abap-adt/auth-stores`
313
336
  - **Does NOT implement token acquisition**: OAuth2 flows, refresh token logic, and client credentials are handled by concrete provider implementations from `@mcp-abap-adt/auth-providers`
314
337
 
@@ -317,9 +340,9 @@ The `@mcp-abap-adt/auth-broker` package defines **interfaces** and provides **or
317
340
  The **consumer** (application using `AuthBroker`) is responsible for:
318
341
 
319
342
  1. **Selecting appropriate implementations**: Choose the correct `IServiceKeyStore`, `ISessionStore`, and `ITokenProvider` implementations based on the use case:
320
- - **ABAP systems**: Use `AbapServiceKeyStore`, `AbapSessionStore` (or `SafeAbapSessionStore`), and `BtpTokenProvider`
321
- - **BTP systems**: Use `AbapServiceKeyStore`, `BtpSessionStore` (or `SafeBtpSessionStore`), and `BtpTokenProvider`
322
- - **XSUAA services**: Use `XsuaaServiceKeyStore`, `XsuaaSessionStore` (or `SafeXsuaaSessionStore`), and `XsuaaTokenProvider`
343
+ - **ABAP systems**: Use `AbapServiceKeyStore`, `AbapSessionStore` (or `SafeAbapSessionStore`), and `AuthorizationCodeProvider`
344
+ - **BTP systems**: Use `AbapServiceKeyStore`, `BtpSessionStore` (or `SafeBtpSessionStore`), and `AuthorizationCodeProvider`
345
+ - **XSUAA services**: Use `XsuaaServiceKeyStore`, `XsuaaSessionStore` (or `SafeXsuaaSessionStore`), and `ClientCredentialsProvider`
323
346
 
324
347
  2. **Ensuring complete configuration**: If a session store requires `serviceUrl` (e.g., `AbapSessionStore` requires `sapUrl`), the consumer must ensure that:
325
348
  - The session is created with `serviceUrl` before calling `AuthBroker.getToken()`, OR
@@ -346,8 +369,7 @@ Concrete `ISessionStore` implementations are responsible for:
346
369
  Concrete `ITokenProvider` implementations are responsible for:
347
370
 
348
371
  - **Obtaining tokens**: Using OAuth2 flows, refresh tokens, or client credentials to obtain JWT tokens
349
- - **Returning connection config**: Returning `IConnectionConfig` with `authorizationToken` and optionally `serviceUrl` (if known)
350
- - **Not returning `serviceUrl` if unknown**: Providers like `BtpTokenProvider` may not return `serviceUrl` because they only handle token acquisition, not connection configuration
372
+ - **Managing token lifecycle**: Caching, validating, refreshing, and re-authenticating as needed
351
373
 
352
374
  ### Design Principles
353
375
 
@@ -361,22 +383,6 @@ Concrete `ITokenProvider` implementations are responsible for:
361
383
  4. **Interface Segregation**: Interfaces are focused and minimal, containing only what's necessary for their specific purpose
362
384
  5. **Open/Closed Principle**: New store and provider implementations can be added without modifying `AuthBroker`
363
385
 
364
- ### Example: Why AuthBroker Doesn't Handle `serviceUrl`
365
-
366
- Consider this scenario:
367
- - `BtpTokenProvider.getConnectionConfig()` returns `IConnectionConfig` with `authorizationToken` but **without** `serviceUrl` (because it only handles token acquisition)
368
- - `AbapSessionStore.setConnectionConfig()` requires `sapUrl` (which maps to `serviceUrl`)
369
-
370
- If `AuthBroker` tried to merge `serviceUrl` from `serviceKeyStore`, it would:
371
- 1. Violate the DIP by knowing about specific store requirements
372
- 2. Break the abstraction - `AuthBroker` shouldn't know that `AbapSessionStore` needs `serviceUrl`
373
- 3. Create coupling between `AuthBroker` and concrete implementations
374
-
375
- Instead, the consumer or `AbapSessionStore` itself should handle this:
376
- - **Option 1**: Consumer retrieves `serviceUrl` from `serviceKeyStore` and ensures it's in the session before calling `AuthBroker.getToken()`
377
- - **Option 2**: `AbapSessionStore.setConnectionConfig()` retrieves `serviceUrl` from `serviceKeyStore` internally if not provided
378
- - **Option 3**: `AbapSessionStore.setConnectionConfig()` uses existing `sapUrl` from current session if available
379
-
380
386
  ## API
381
387
 
382
388
  ### `AuthBroker`
@@ -388,7 +394,8 @@ new AuthBroker(
388
394
  config: {
389
395
  sessionStore: ISessionStore; // required
390
396
  serviceKeyStore?: IServiceKeyStore; // optional
391
- tokenProvider?: ITokenProvider; // optional
397
+ tokenProvider: ITokenProvider; // required
398
+ allowBrowserAuth?: boolean; // optional
392
399
  },
393
400
  browser?: string,
394
401
  logger?: ILogger
@@ -399,7 +406,8 @@ new AuthBroker(
399
406
  - `config` - Configuration object:
400
407
  - `sessionStore` - **Required** - Store for session data. Must contain initial session with `serviceUrl`
401
408
  - `serviceKeyStore` - **Optional** - Store for service keys. Only needed for initializing sessions from service keys
402
- - `tokenProvider` - **Optional** - Token provider for token acquisition. Only needed for browser authentication or when direct UAA requests fail
409
+ - `tokenProvider` - **Required** - Token provider for token acquisition and refresh
410
+ - `allowBrowserAuth` - **Optional** - When `false`, throws `BROWSER_AUTH_REQUIRED` instead of launching browser auth
403
411
  - `browser` - Optional browser name for authentication (`chrome`, `edge`, `firefox`, `system`, `headless`, `none`). Default: `system`
404
412
  - Use `'headless'` for SSH/remote sessions - logs URL and waits for manual callback
405
413
  - Use `'none'` for automated tests - logs URL and rejects immediately
@@ -411,16 +419,15 @@ new AuthBroker(
411
419
  - **`sessionStore` (required)**: Always required. Must contain initial session with `serviceUrl`
412
420
  - **`serviceKeyStore` (optional)**:
413
421
  - Required if you need to initialize sessions from service keys (Step 0)
414
- - Not needed if session already contains UAA credentials
415
- - **`tokenProvider` (optional)**:
416
- - Required for browser authentication when initializing from service key (Step 0)
417
- - Optional but recommended as fallback when direct UAA requests fail
418
- - Not needed if session contains valid UAA credentials (direct UAA HTTP requests will be used)
422
+ - Not needed if session already contains authorization config and tokens
423
+ - **`tokenProvider` (required)**:
424
+ - Used for all token acquisition and refresh flows
425
+ - Must be configured with the destination's auth parameters (e.g., UAA credentials)
419
426
 
420
427
  **Available Implementations:**
421
- - **ABAP**: `AbapServiceKeyStore(directory, defaultServiceUrl?, logger?)`, `AbapSessionStore(directory, defaultServiceUrl?, logger?)`, `SafeAbapSessionStore(defaultServiceUrl?, logger?)`, `BtpTokenProvider(browserAuthPort?)`
422
- - **XSUAA** (reduced scope): `XsuaaServiceKeyStore(directory, logger?)`, `XsuaaSessionStore(directory, defaultServiceUrl, logger?)`, `SafeXsuaaSessionStore(defaultServiceUrl, logger?)`, `XsuaaTokenProvider()`
423
- - **BTP** (full scope for ABAP): `AbapServiceKeyStore(directory, defaultServiceUrl?, logger?)`, `BtpSessionStore(directory, defaultServiceUrl, logger?)`, `SafeBtpSessionStore(defaultServiceUrl, logger?)`, `BtpTokenProvider(browserAuthPort?)`
428
+ - **ABAP**: `AbapServiceKeyStore(directory, defaultServiceUrl?, logger?)`, `AbapSessionStore(directory, defaultServiceUrl?, logger?)`, `SafeAbapSessionStore(defaultServiceUrl?, logger?)`, `AuthorizationCodeProvider(...)`
429
+ - **XSUAA** (reduced scope): `XsuaaServiceKeyStore(directory, logger?)`, `XsuaaSessionStore(directory, defaultServiceUrl, logger?)`, `SafeXsuaaSessionStore(defaultServiceUrl, logger?)`, `ClientCredentialsProvider(...)`
430
+ - **BTP** (full scope for ABAP): `AbapServiceKeyStore(directory, defaultServiceUrl?, logger?)`, `BtpSessionStore(directory, defaultServiceUrl, logger?)`, `SafeBtpSessionStore(defaultServiceUrl, logger?)`, `AuthorizationCodeProvider(...)`
424
431
 
425
432
  #### Methods
426
433
 
@@ -429,31 +436,34 @@ new AuthBroker(
429
436
  Gets authentication token for destination. Implements a three-step flow:
430
437
 
431
438
  **Step 0: Initialize Session with Token (if needed)**
432
- - Checks if session has `authorizationToken` and UAA credentials
433
- - If both are empty and `serviceKeyStore` is available:
434
- - Tries direct UAA request from service key (if UAA credentials available)
435
- - If failed and `tokenProvider` available uses provider for authentication
436
- - If session has token OR UAA credentials → proceeds to Step 1
437
-
438
- **Step 1: Refresh Token Flow**
439
- - Checks if refresh token exists in session
440
- - If refresh token exists:
441
- - Tries direct UAA refresh (if UAA credentials in session)
442
- - If failed and `tokenProvider` availableuses provider
443
- - If successful → returns new token
439
+ - Checks if session has `authorizationToken` and authorization config
440
+ - If both are missing and `serviceKeyStore` is available:
441
+ - Loads authorization config from service key
442
+ - Uses `tokenProvider.getTokens()` to obtain tokens
443
+ - Persists tokens to session
444
+ - Otherwise → proceeds to Step 1
445
+
446
+ **Step 1: Token Validation**
447
+ - If token exists in session and provider supports `validateToken`, validate it
448
+ - If valid returns token
449
+ - If provider does not support validation returns token as-is
444
450
  - Otherwise → proceeds to Step 2
445
451
 
446
- **Step 2: UAA Credentials Flow**
447
- - Checks if UAA credentials exist in session or service key
448
- - Tries direct UAA client_credentials request (if UAA credentials available)
449
- - If failed and `tokenProvider` available → uses provider
450
- - If successful → returns new token
452
+ **Step 2: Token Refresh / Re-Auth**
453
+ - If session has authorization config:
454
+ - Uses `tokenProvider.getTokens()` to refresh or re-authenticate
455
+ - Persists tokens to session
456
+ - Returns new token
457
+ - If that fails (or no session auth config) and `serviceKeyStore` is available:
458
+ - Loads authorization config from service key
459
+ - Uses `tokenProvider.getTokens()` to obtain tokens
460
+ - Persists tokens to session
451
461
  - If all failed → throws error
452
462
 
453
463
  **Important Notes:**
454
- - If `sessionStore` contains valid UAA credentials, neither `serviceKeyStore` nor `tokenProvider` are required. Direct UAA HTTP requests will be used automatically.
455
- - `tokenProvider` is only needed for browser authentication or when direct UAA requests fail.
456
- - Token validation is performed only when checking existing session. Tokens obtained through refresh/UAA/browser authentication are not validated before being saved.
464
+ - All authentication is handled by the injected provider (authorization_code or client_credentials).
465
+ - `tokenProvider` is required for all token acquisition and refresh flows.
466
+ - Token validation is performed only when checking existing session. Tokens obtained through refresh are not validated before being saved.
457
467
  - **Store errors are handled gracefully**: If service key files are missing or malformed, the broker logs the error and continues with fallback mechanisms (session store data or provider-based auth)
458
468
 
459
469
  ##### Error Handling
@@ -512,13 +522,7 @@ Example error scenarios handled:
512
522
 
513
523
  ##### `refreshToken(destination: string): Promise<string>`
514
524
 
515
- Force refresh token for destination. Uses refresh token from session if available, otherwise uses UAA credentials from session or service key.
516
-
517
- **Flow:**
518
- - If refresh token exists and UAA credentials available → tries direct UAA refresh
519
- - If direct UAA fails and `tokenProvider` available → uses provider
520
- - If no refresh token but UAA credentials available → tries direct UAA client_credentials
521
- - If all failed → throws error
525
+ Force refresh token for destination. Calls `getToken()` to run the full refresh flow and persist updated tokens.
522
526
 
523
527
  ##### `clearCache(destination: string): void`
524
528
 
@@ -530,14 +534,14 @@ Clear all cached tokens.
530
534
 
531
535
  ### Token Providers
532
536
 
533
- The package uses `ITokenProvider` interface for token acquisition. Two implementations are available:
537
+ The package uses the `ITokenProvider` interface for token acquisition. Provider implementations live in `@mcp-abap-adt/auth-providers`:
534
538
 
535
- - **`XsuaaTokenProvider`** - For XSUAA authentication (reduced scope)
539
+ - **`ClientCredentialsProvider`** - For XSUAA authentication (reduced scope)
536
540
  - Uses client_credentials grant type
537
541
  - No browser interaction required
538
542
  - No refresh token provided
539
543
 
540
- - **`BtpTokenProvider`** - For BTP/ABAP authentication (full scope)
544
+ - **`AuthorizationCodeProvider`** - For BTP/ABAP authentication (full scope)
541
545
  - Constructor accepts optional `browserAuthPort?: number` parameter (default: 3001)
542
546
  - Automatically finds an available port if the requested port is in use (prevents `EADDRINUSE` errors)
543
547
  - Server properly closes all connections and frees the port after authentication completes
@@ -553,55 +557,59 @@ import {
553
557
  AuthBroker,
554
558
  XsuaaServiceKeyStore,
555
559
  XsuaaSessionStore,
556
- XsuaaTokenProvider,
557
- BtpTokenProvider,
558
560
  AbapServiceKeyStore,
559
561
  BtpSessionStore
560
562
  } from '@mcp-abap-adt/auth-broker';
563
+ import {
564
+ ClientCredentialsProvider,
565
+ AuthorizationCodeProvider,
566
+ } from '@mcp-abap-adt/auth-providers';
561
567
 
562
- // XSUAA authentication - session only (direct UAA requests)
568
+ // XSUAA authentication
563
569
  const xsuaaBroker = new AuthBroker({
564
570
  sessionStore: new XsuaaSessionStore('/path/to/sessions', 'https://mcp.example.com'),
565
- // serviceKeyStore and tokenProvider not needed if session has UAA credentials
571
+ tokenProvider: new ClientCredentialsProvider({
572
+ uaaUrl: 'https://auth.example.com',
573
+ clientId: '...',
574
+ clientSecret: '...',
575
+ }),
566
576
  });
567
577
 
568
578
  // XSUAA authentication - with service key initialization
569
579
  const xsuaaBrokerWithServiceKey = new AuthBroker({
570
580
  sessionStore: new XsuaaSessionStore('/path/to/sessions', 'https://mcp.example.com'),
571
581
  serviceKeyStore: new XsuaaServiceKeyStore('/path/to/keys'),
572
- // tokenProvider optional - direct UAA requests will be used
582
+ tokenProvider: new ClientCredentialsProvider({
583
+ uaaUrl: 'https://auth.example.com',
584
+ clientId: '...',
585
+ clientSecret: '...',
586
+ }),
573
587
  }, 'none');
574
588
 
575
- // BTP authentication - session only (direct UAA requests)
589
+ // BTP authentication
576
590
  const btpBroker = new AuthBroker({
577
591
  sessionStore: new BtpSessionStore('/path/to/sessions', 'https://abap.example.com'),
578
- // serviceKeyStore and tokenProvider not needed if session has UAA credentials
592
+ tokenProvider: new AuthorizationCodeProvider({
593
+ uaaUrl: 'https://auth.example.com',
594
+ clientId: '...',
595
+ clientSecret: '...',
596
+ browser: 'system',
597
+ }),
579
598
  });
580
599
 
581
600
  // BTP authentication - with service key and provider (for browser auth)
582
601
  const btpBrokerFull = new AuthBroker({
583
602
  sessionStore: new BtpSessionStore('/path/to/sessions', 'https://abap.example.com'),
584
603
  serviceKeyStore: new AbapServiceKeyStore('/path/to/keys'),
585
- tokenProvider: new BtpTokenProvider(), // needed for browser authentication
604
+ tokenProvider: new AuthorizationCodeProvider({
605
+ uaaUrl: 'https://auth.example.com',
606
+ clientId: '...',
607
+ clientSecret: '...',
608
+ browser: 'system',
609
+ }),
586
610
  });
587
611
  ```
588
612
 
589
- ### Direct UAA HTTP Requests
590
-
591
- When UAA credentials are available in session, `AuthBroker` automatically uses direct HTTP requests to UAA without requiring `tokenProvider`:
592
-
593
- - **Refresh Token Grant**: Direct HTTP POST to `{uaaUrl}/oauth/token` with `grant_type=refresh_token`
594
- - **Client Credentials Grant**: Direct HTTP POST to `{uaaUrl}/oauth/token` with `grant_type=client_credentials`
595
-
596
- **Benefits:**
597
- - No dependency on `tokenProvider` when session has UAA credentials
598
- - Faster token refresh (no provider overhead)
599
- - Simpler configuration (only `sessionStore` needed)
600
-
601
- **Fallback to Provider:**
602
- - If direct UAA request fails and `tokenProvider` is available, broker automatically falls back to provider
603
- - Provider is useful for browser authentication or alternative authentication flows
604
-
605
613
  ### CLI: mcp-auth
606
614
 
607
615
  Generate or refresh `.env`/JSON output using AuthBroker + stores:
@@ -16,8 +16,16 @@
16
16
  import * as path from 'path';
17
17
  import * as fs from 'fs';
18
18
  import { AuthBroker } from '../src/AuthBroker';
19
- import { AbapServiceKeyStore, AbapSessionStore, XsuaaServiceKeyStore, XsuaaSessionStore } from '@mcp-abap-adt/auth-stores';
20
- import { BtpTokenProvider, XsuaaTokenProvider } from '@mcp-abap-adt/auth-providers';
19
+ import {
20
+ AbapServiceKeyStore,
21
+ AbapSessionStore,
22
+ XsuaaServiceKeyStore,
23
+ XsuaaSessionStore,
24
+ } from '@mcp-abap-adt/auth-stores';
25
+ import {
26
+ AuthorizationCodeProvider,
27
+ ClientCredentialsProvider,
28
+ } from '@mcp-abap-adt/auth-providers';
21
29
 
22
30
  async function main() {
23
31
  const args = process.argv.slice(2);
@@ -65,10 +73,24 @@ async function main() {
65
73
  ? new XsuaaSessionStore(sessionDir)
66
74
  : new AbapSessionStore(sessionDir);
67
75
 
76
+ const authConfig = await serviceKeyStore.getAuthorizationConfig(destination);
77
+ if (!authConfig) {
78
+ throw new Error(`Missing authorization config for ${destination}`);
79
+ }
80
+
68
81
  // Create token provider
69
82
  const tokenProvider = isXsuaa
70
- ? new XsuaaTokenProvider()
71
- : new BtpTokenProvider();
83
+ ? new ClientCredentialsProvider({
84
+ uaaUrl: authConfig.uaaUrl,
85
+ clientId: authConfig.uaaClientId,
86
+ clientSecret: authConfig.uaaClientSecret,
87
+ })
88
+ : new AuthorizationCodeProvider({
89
+ uaaUrl: authConfig.uaaUrl,
90
+ clientId: authConfig.uaaClientId,
91
+ clientSecret: authConfig.uaaClientSecret,
92
+ browser: 'system',
93
+ });
72
94
 
73
95
  // Create AuthBroker
74
96
  // For ABAP, use 'system' browser (will open browser for auth)
@@ -124,4 +146,3 @@ main().catch((error) => {
124
146
  console.error('Fatal error:', error);
125
147
  process.exit(1);
126
148
  });
127
-
package/bin/mcp-auth.ts CHANGED
@@ -32,12 +32,6 @@ import {
32
32
  AuthorizationCodeProvider,
33
33
  ClientCredentialsProvider,
34
34
  } from '@mcp-abap-adt/auth-providers';
35
- import type {
36
- IAuthorizationConfig,
37
- ITokenProvider,
38
- ITokenProviderOptions,
39
- ITokenProviderResult,
40
- } from '@mcp-abap-adt/interfaces';
41
35
  import {
42
36
  ABAP_CONNECTION_VARS,
43
37
  ABAP_AUTHORIZATION_VARS,
@@ -241,86 +235,6 @@ function parseArgs(): McpAuthOptions | null {
241
235
  };
242
236
  }
243
237
 
244
- type ProviderMode = 'authorization_code' | 'client_credentials';
245
-
246
- class BrokerTokenProvider implements ITokenProvider {
247
- private mode: ProviderMode;
248
- private browser?: string;
249
- private redirectPort?: number;
250
-
251
- constructor(mode: ProviderMode, browser?: string, redirectPort?: number) {
252
- this.mode = mode;
253
- this.browser = browser;
254
- this.redirectPort = redirectPort;
255
- }
256
-
257
- async getConnectionConfig(
258
- authConfig: IAuthorizationConfig,
259
- options?: ITokenProviderOptions,
260
- ): Promise<ITokenProviderResult> {
261
- return this.getTokenResult(authConfig, options);
262
- }
263
-
264
- async refreshTokenFromSession(
265
- authConfig: IAuthorizationConfig,
266
- options?: ITokenProviderOptions,
267
- ): Promise<ITokenProviderResult> {
268
- return this.getTokenResult(authConfig, options);
269
- }
270
-
271
- async refreshTokenFromServiceKey(
272
- authConfig: IAuthorizationConfig,
273
- options?: ITokenProviderOptions,
274
- ): Promise<ITokenProviderResult> {
275
- return this.getTokenResult(authConfig, options);
276
- }
277
-
278
- private async getTokenResult(
279
- authConfig: IAuthorizationConfig,
280
- options?: ITokenProviderOptions,
281
- ): Promise<ITokenProviderResult> {
282
- const uaaUrl = authConfig.uaaUrl;
283
- const uaaClientId = authConfig.uaaClientId;
284
- const uaaClientSecret = authConfig.uaaClientSecret;
285
-
286
- if (!uaaUrl || !uaaClientId || !uaaClientSecret) {
287
- throw new Error('Auth config missing required UAA credentials');
288
- }
289
-
290
- if (this.mode === 'client_credentials') {
291
- const provider = new ClientCredentialsProvider({
292
- uaaUrl,
293
- clientId: uaaClientId,
294
- clientSecret: uaaClientSecret,
295
- });
296
- const result = await provider.getTokens();
297
- return {
298
- connectionConfig: {
299
- authorizationToken: result.authorizationToken,
300
- },
301
- refreshToken: result.refreshToken,
302
- };
303
- }
304
-
305
- const browserValue = options?.browser ?? this.browser ?? 'system';
306
- const provider = new AuthorizationCodeProvider({
307
- uaaUrl,
308
- clientId: uaaClientId,
309
- clientSecret: uaaClientSecret,
310
- refreshToken: authConfig.refreshToken,
311
- browser: browserValue,
312
- redirectPort: this.redirectPort,
313
- });
314
- const result = await provider.getTokens();
315
- return {
316
- connectionConfig: {
317
- authorizationToken: result.authorizationToken,
318
- },
319
- refreshToken: result.refreshToken,
320
- };
321
- }
322
- }
323
-
324
238
  function writeEnvFile(
325
239
  outputPath: string,
326
240
  authType: 'abap' | 'xsuaa',
@@ -531,15 +445,32 @@ async function main() {
531
445
  ? new XsuaaSessionStore(tempSessionDir, brokerServiceUrl)
532
446
  : new AbapSessionStore(tempSessionDir);
533
447
 
448
+ const sessionAuthConfig =
449
+ await sessionStore.getAuthorizationConfig(destination);
450
+ const serviceKeyAuthConfig =
451
+ serviceKeyStore?.getAuthorizationConfig
452
+ ? await serviceKeyStore.getAuthorizationConfig(destination)
453
+ : null;
454
+ const authConfig = sessionAuthConfig || serviceKeyAuthConfig;
455
+ if (!authConfig) {
456
+ throw new Error(`Authorization config not found for ${destination}`);
457
+ }
458
+
534
459
  const useBrowserAuth = options.browser !== undefined;
535
- const providerMode: ProviderMode = useBrowserAuth
536
- ? 'authorization_code'
537
- : 'client_credentials';
538
- const tokenProvider = new BrokerTokenProvider(
539
- providerMode,
540
- options.browser,
541
- options.redirectPort,
542
- );
460
+ const tokenProvider = useBrowserAuth
461
+ ? new AuthorizationCodeProvider({
462
+ uaaUrl: authConfig.uaaUrl,
463
+ clientId: authConfig.uaaClientId,
464
+ clientSecret: authConfig.uaaClientSecret,
465
+ refreshToken: authConfig.refreshToken,
466
+ browser: options.browser,
467
+ redirectPort: options.redirectPort,
468
+ })
469
+ : new ClientCredentialsProvider({
470
+ uaaUrl: authConfig.uaaUrl,
471
+ clientId: authConfig.uaaClientId,
472
+ clientSecret: authConfig.uaaClientSecret,
473
+ });
543
474
 
544
475
  const broker = new AuthBroker(
545
476
  {
@@ -55,27 +55,17 @@ export declare class AuthBroker {
55
55
  /**
56
56
  * Get UAA credentials from session or service key
57
57
  */
58
- private getUaaCredentials;
58
+ private getAuthorizationConfigFromServiceKey;
59
59
  /**
60
60
  * Save token and config to session
61
61
  */
62
62
  private saveTokenToSession;
63
- /**
64
- * Initialize session from service key (Step 0)
65
- */
66
- private initializeSessionFromServiceKey;
63
+ private requestTokens;
64
+ private persistTokenResult;
67
65
  /**
68
66
  * Validate existing token (Step 1)
69
67
  */
70
68
  private validateExistingToken;
71
- /**
72
- * Refresh token from session (Step 2a)
73
- */
74
- private refreshTokenFromSession;
75
- /**
76
- * Refresh token from service key (Step 2b)
77
- */
78
- private refreshTokenFromServiceKey;
79
69
  /**
80
70
  * Get authentication token for destination.
81
71
  * Uses tokenProvider for all authentication operations (browser-based authorization).
@@ -1 +1 @@
1
- {"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACd,MAAM,qBAAqB,CAAC;AA4C7B;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,YAAY,EAAE,aAAa,CAAC;IAC5B,uEAAuE;IACvE,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,4IAA4I;IAC5I,aAAa,EAAE,cAAc,CAAC;IAC9B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAiB;IACtC,OAAO,CAAC,gBAAgB,CAAU;IAElC;;;;;;;;;;;OAWG;gBACS,MAAM,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO;IAoFxE;;OAEG;YACW,eAAe;IA0D7B;;OAEG;YACW,aAAa;IAoD3B;;OAEG;YACW,iBAAiB;IA2D/B;;OAEG;YACW,kBAAkB;IAkChC;;OAEG;YACW,+BAA+B;IA4G7C;;OAEG;YACW,qBAAqB;IA8BnC;;OAEG;YACW,uBAAuB;IAyFrC;;OAEG;YACW,0BAA0B;IAgHxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACG,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA8GpD;;;;;OAKG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASxD;;;;OAIG;IACG,sBAAsB,CAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAoEvC;;;;OAIG;IACG,mBAAmB,CACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IA8DpC;;;;;;;;;;;;;;;;OAgBG;IACH,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe;CAqB3D"}
1
+ {"version":3,"file":"AuthBroker.d.ts","sourceRoot":"","sources":["../src/AuthBroker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,eAAe,EAGrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACd,MAAM,qBAAqB,CAAC;AA4C7B;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,YAAY,EAAE,aAAa,CAAC;IAC5B,uEAAuE;IACvE,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,4IAA4I;IAC5I,aAAa,EAAE,cAAc,CAAC;IAC9B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAiB;IACtC,OAAO,CAAC,gBAAgB,CAAU;IAElC;;;;;;;;;;;OAWG;gBACS,MAAM,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO;IAkFxE;;OAEG;YACW,eAAe;IA0D7B;;OAEG;YACW,aAAa;IAoD3B;;OAEG;YACW,oCAAoC;IA4ClD;;OAEG;YACW,kBAAkB;YAkClB,aAAa;YA8Cb,kBAAkB;IAiChC;;OAEG;YACW,qBAAqB;IA8BnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACG,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAqIpD;;;;;OAKG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASxD;;;;OAIG;IACG,sBAAsB,CAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAoEvC;;;;OAIG;IACG,mBAAmB,CACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IA8DpC;;;;;;;;;;;;;;;;OAgBG;IACH,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe;CAqB3D"}
@@ -84,8 +84,8 @@ class AuthBroker {
84
84
  throw new Error('AuthBroker: sessionStore.setConnectionConfig must be a function');
85
85
  }
86
86
  // Check tokenProvider methods (required)
87
- if (typeof tokenProvider.getConnectionConfig !== 'function') {
88
- throw new Error('AuthBroker: tokenProvider.getConnectionConfig must be a function');
87
+ if (typeof tokenProvider.getTokens !== 'function') {
88
+ throw new Error('AuthBroker: tokenProvider.getTokens must be a function');
89
89
  }
90
90
  // validateToken is optional, so we don't check it
91
91
  // Check serviceKeyStore methods (if provided)
@@ -196,14 +196,9 @@ class AuthBroker {
196
196
  /**
197
197
  * Get UAA credentials from session or service key
198
198
  */
199
- async getUaaCredentials(destination, authConfig) {
200
- if (authConfig?.uaaUrl &&
201
- authConfig?.uaaClientId &&
202
- authConfig?.uaaClientSecret) {
203
- return authConfig;
204
- }
199
+ async getAuthorizationConfigFromServiceKey(destination) {
205
200
  if (!this.serviceKeyStore) {
206
- throw new Error(`UAA credentials not found for ${destination}. Session has no UAA credentials and serviceKeyStore is not available.`);
201
+ throw new Error(`Authorization config not found for ${destination}. Session has no auth config and serviceKeyStore is not available.`);
207
202
  }
208
203
  let serviceKeyAuthConfig = null;
209
204
  try {
@@ -219,21 +214,17 @@ class AuthBroker {
219
214
  this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
220
215
  }
221
216
  else {
222
- this.logger?.warn(`Failed to get UAA credentials from service key store for ${destination}: ${getErrorMessage(error)}`);
217
+ this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
223
218
  }
224
219
  }
225
220
  else {
226
- this.logger?.warn(`Failed to get UAA credentials from service key store for ${destination}: ${getErrorMessage(error)}`);
221
+ this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
227
222
  }
228
223
  }
229
- const uaaCredentials = authConfig || serviceKeyAuthConfig;
230
- if (!uaaCredentials ||
231
- !uaaCredentials.uaaUrl ||
232
- !uaaCredentials.uaaClientId ||
233
- !uaaCredentials.uaaClientSecret) {
234
- throw new Error(`UAA credentials not found for ${destination}. Session has no UAA credentials${this.serviceKeyStore ? ' and serviceKeyStore has no UAA credentials' : ' and serviceKeyStore is not available'}.`);
224
+ if (!serviceKeyAuthConfig) {
225
+ throw new Error(`Authorization config not found for ${destination}. Session has no auth config${this.serviceKeyStore ? ' and serviceKeyStore has no auth config' : ' and serviceKeyStore is not available'}.`);
235
226
  }
236
- return uaaCredentials;
227
+ return serviceKeyAuthConfig;
237
228
  }
238
229
  /**
239
230
  * Save token and config to session
@@ -254,74 +245,51 @@ class AuthBroker {
254
245
  throw new Error(`Failed to save authorization config for destination "${destination}": ${getErrorMessage(error)}`);
255
246
  }
256
247
  }
257
- /**
258
- * Initialize session from service key (Step 0)
259
- */
260
- async initializeSessionFromServiceKey(destination, serviceUrl) {
261
- if (!this.serviceKeyStore) {
262
- throw new Error(`Cannot initialize session for destination "${destination}": authorizationToken is empty, UAA credentials are empty, and serviceKeyStore is not available. Provide serviceKeyStore to initialize from service key.`);
263
- }
264
- const serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
265
- if (!serviceKeyAuthConfig ||
266
- !serviceKeyAuthConfig.uaaUrl ||
267
- !serviceKeyAuthConfig.uaaClientId ||
268
- !serviceKeyAuthConfig.uaaClientSecret) {
269
- throw new Error(`Service key for destination "${destination}" does not contain UAA credentials`);
270
- }
271
- if (!this.allowBrowserAuth) {
272
- const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Either enable browser auth or provide a valid session with token.`);
273
- error.code = 'BROWSER_AUTH_REQUIRED';
274
- error.destination = destination;
275
- this.logger?.error(`Step 0: Browser auth required but disabled for ${destination}`);
276
- throw error;
277
- }
278
- this.logger?.debug(`Step 0: Authenticating via provider (browser) for ${destination} using service key UAA credentials`);
279
- const getConnectionConfig = this.tokenProvider.getConnectionConfig;
280
- if (!getConnectionConfig) {
281
- throw new Error('AuthBroker: tokenProvider.getConnectionConfig is required');
282
- }
283
- let tokenResult;
248
+ async requestTokens(destination, sourceLabel) {
249
+ this.logger?.debug(`Requesting tokens for ${destination} via ${sourceLabel}`);
284
250
  try {
285
- tokenResult = await getConnectionConfig(serviceKeyAuthConfig, {
286
- browser: this.browser,
287
- logger: this.logger,
288
- });
251
+ const getTokens = this.tokenProvider.getTokens;
252
+ if (!getTokens) {
253
+ throw new Error('AuthBroker: tokenProvider.getTokens is required');
254
+ }
255
+ return await getTokens.call(this.tokenProvider);
289
256
  }
290
257
  catch (error) {
291
258
  if (hasErrorCode(error)) {
292
259
  if (error.code === 'VALIDATION_ERROR') {
293
- throw new Error(`Cannot initialize session for destination "${destination}": provider validation failed - missing ${error.missingFields?.join(', ') || 'required fields'}`);
260
+ throw new Error(`Token provider validation failed for ${destination}: missing ${error.missingFields?.join(', ') || 'required fields'}`);
294
261
  }
295
- else if (error.code === 'BROWSER_AUTH_ERROR') {
296
- throw new Error(`Cannot initialize session for destination "${destination}": browser authentication failed - ${getErrorMessage(error)}`);
262
+ if (error.code === 'BROWSER_AUTH_ERROR') {
263
+ throw new Error(`Token provider browser authentication failed for ${destination}: ${getErrorMessage(error)}`);
297
264
  }
298
- else if (error.code === 'ECONNREFUSED' ||
265
+ if (error.code === 'ECONNREFUSED' ||
299
266
  error.code === 'ETIMEDOUT' ||
300
267
  error.code === 'ENOTFOUND') {
301
- throw new Error(`Cannot initialize session for destination "${destination}": network error - cannot reach authentication server (${error.code})`);
268
+ throw new Error(`Token provider network error for ${destination}: ${error.code}`);
269
+ }
270
+ if (error.code === 'SERVICE_KEY_ERROR') {
271
+ throw new Error(`Token provider service key error for ${destination}: ${getErrorMessage(error)}`);
302
272
  }
303
273
  }
304
- throw new Error(`Cannot initialize session for destination "${destination}": provider error - ${getErrorMessage(error)}`);
274
+ throw new Error(`Token provider error for ${destination}: ${getErrorMessage(error)}`);
305
275
  }
306
- const token = tokenResult.connectionConfig.authorizationToken;
276
+ }
277
+ async persistTokenResult(destination, serviceUrl, baseConnConfig, authConfig, tokenResult) {
278
+ const token = tokenResult.authorizationToken;
307
279
  if (!token) {
308
280
  throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
309
281
  }
310
- const tokenLength = token.length;
311
- this.logger?.info(`Step 0: Token initialized for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
312
- // Get serviceUrl from service key store if not in connectionConfig
313
- const serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
314
282
  const connectionConfigWithServiceUrl = {
315
- ...tokenResult.connectionConfig,
316
- serviceUrl: tokenResult.connectionConfig.serviceUrl ||
317
- serviceKeyConnConfig?.serviceUrl ||
318
- serviceUrl,
283
+ ...baseConnConfig,
284
+ serviceUrl,
285
+ authorizationToken: token,
286
+ authType: 'jwt',
319
287
  };
320
- await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, {
321
- ...serviceKeyAuthConfig,
322
- refreshToken: tokenResult.refreshToken || serviceKeyAuthConfig.refreshToken,
323
- });
324
- return token;
288
+ const authorizationConfig = {
289
+ ...authConfig,
290
+ refreshToken: tokenResult.refreshToken ?? authConfig.refreshToken,
291
+ };
292
+ await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, authorizationConfig);
325
293
  }
326
294
  /**
327
295
  * Validate existing token (Step 1)
@@ -345,138 +313,6 @@ class AuthBroker {
345
313
  return false;
346
314
  }
347
315
  }
348
- /**
349
- * Refresh token from session (Step 2a)
350
- */
351
- async refreshTokenFromSession(destination, uaaCredentials, refreshToken, serviceUrl) {
352
- this.logger?.debug(`Step 2a: Trying refreshTokenFromSession for ${destination}`);
353
- const authConfigWithRefresh = { ...uaaCredentials, refreshToken };
354
- const refreshTokenFromSession = this.tokenProvider.refreshTokenFromSession;
355
- if (!refreshTokenFromSession) {
356
- throw new Error('AuthBroker: tokenProvider.refreshTokenFromSession is required');
357
- }
358
- let tokenResult;
359
- try {
360
- tokenResult = await refreshTokenFromSession(authConfigWithRefresh, {
361
- browser: this.browser,
362
- logger: this.logger,
363
- });
364
- }
365
- catch (error) {
366
- if (hasErrorCode(error)) {
367
- if (error.code === 'ECONNREFUSED' ||
368
- error.code === 'ETIMEDOUT' ||
369
- error.code === 'ENOTFOUND') {
370
- this.logger?.debug(`Step 2a: Network error during refreshTokenFromSession for ${destination}: ${error.code}. Trying refreshTokenFromServiceKey`);
371
- throw error; // Re-throw to trigger fallback to Step 2b
372
- }
373
- }
374
- throw error; // Re-throw other errors
375
- }
376
- const token = tokenResult.connectionConfig.authorizationToken;
377
- if (!token) {
378
- throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
379
- }
380
- const tokenLength = token.length;
381
- this.logger?.info(`Step 2a: Token refreshed from session for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
382
- // Get serviceUrl from session or service key
383
- let serviceKeyServiceUrl;
384
- if (this.serviceKeyStore) {
385
- try {
386
- const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
387
- serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
388
- }
389
- catch (error) {
390
- this.logger?.debug(`Could not get serviceUrl from service key store: ${getErrorMessage(error)}`);
391
- }
392
- }
393
- const finalServiceUrl = tokenResult.connectionConfig.serviceUrl ||
394
- serviceUrl ||
395
- serviceKeyServiceUrl;
396
- const connectionConfigWithServiceUrl = {
397
- ...tokenResult.connectionConfig,
398
- serviceUrl: finalServiceUrl,
399
- };
400
- const authorizationConfig = {
401
- ...uaaCredentials,
402
- refreshToken: tokenResult.refreshToken || refreshToken,
403
- };
404
- await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, authorizationConfig);
405
- return token;
406
- }
407
- /**
408
- * Refresh token from service key (Step 2b)
409
- */
410
- async refreshTokenFromServiceKey(destination, uaaCredentials, serviceUrl) {
411
- if (!this.allowBrowserAuth) {
412
- const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Token refresh via session failed and browser auth is not allowed. Either enable browser auth or ensure a valid refresh token exists in session.`);
413
- error.code = 'BROWSER_AUTH_REQUIRED';
414
- error.destination = destination;
415
- this.logger?.error(`Step 2b: Browser auth required but disabled for ${destination}`);
416
- throw error;
417
- }
418
- this.logger?.debug(`Step 2b: Trying refreshTokenFromServiceKey for ${destination}`);
419
- const refreshTokenFromServiceKey = this.tokenProvider.refreshTokenFromServiceKey;
420
- if (!refreshTokenFromServiceKey) {
421
- throw new Error('AuthBroker: tokenProvider.refreshTokenFromServiceKey is required');
422
- }
423
- let tokenResult;
424
- try {
425
- tokenResult = await refreshTokenFromServiceKey(uaaCredentials, {
426
- browser: this.browser,
427
- logger: this.logger,
428
- });
429
- }
430
- catch (error) {
431
- if (hasErrorCode(error)) {
432
- if (error.code === 'VALIDATION_ERROR') {
433
- throw new Error(`Token refresh failed: Missing required fields in authConfig - ${error.missingFields?.join(', ')}`);
434
- }
435
- else if (error.code === 'BROWSER_AUTH_ERROR') {
436
- throw new Error(`Token refresh failed: Browser authentication failed or was cancelled - ${getErrorMessage(error)}`);
437
- }
438
- else if (error.code === 'ECONNREFUSED' ||
439
- error.code === 'ETIMEDOUT' ||
440
- error.code === 'ENOTFOUND') {
441
- throw new Error(`Token refresh failed: Network error - ${error.code}: Cannot reach authentication server`);
442
- }
443
- else if (error.code === 'SERVICE_KEY_ERROR') {
444
- throw new Error(`Token refresh failed: Service key not found or invalid for ${destination}`);
445
- }
446
- }
447
- throw new Error(`Token refresh failed for ${destination}: ${getErrorMessage(error)}`);
448
- }
449
- const token = tokenResult.connectionConfig.authorizationToken;
450
- if (!token) {
451
- throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
452
- }
453
- const tokenLength = token.length;
454
- this.logger?.info(`Step 2b: Token refreshed from service key for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
455
- // Get serviceUrl from session or service key
456
- let serviceKeyServiceUrl;
457
- if (this.serviceKeyStore) {
458
- try {
459
- const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
460
- serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
461
- }
462
- catch (error) {
463
- this.logger?.debug(`Could not get serviceUrl from service key store: ${getErrorMessage(error)}`);
464
- }
465
- }
466
- const finalServiceUrl = tokenResult.connectionConfig.serviceUrl ||
467
- serviceUrl ||
468
- serviceKeyServiceUrl;
469
- const connectionConfigWithServiceUrl = {
470
- ...tokenResult.connectionConfig,
471
- serviceUrl: finalServiceUrl,
472
- };
473
- const authorizationConfig = {
474
- ...uaaCredentials,
475
- refreshToken: tokenResult.refreshToken || uaaCredentials.refreshToken,
476
- };
477
- await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, authorizationConfig);
478
- return token;
479
- }
480
316
  /**
481
317
  * Get authentication token for destination.
482
318
  * Uses tokenProvider for all authentication operations (browser-based authorization).
@@ -525,34 +361,21 @@ class AuthBroker {
525
361
  const serviceUrl = await this.getServiceUrl(destination, connConfig);
526
362
  // Check if we have token or UAA credentials
527
363
  const hasToken = !!connConfig?.authorizationToken;
528
- const hasUaaCredentials = !!(authConfig?.uaaUrl &&
529
- authConfig?.uaaClientId &&
530
- authConfig?.uaaClientSecret);
531
- this.logger?.debug(`Session check for ${destination}: hasToken(${hasToken}), hasUaaCredentials(${hasUaaCredentials}), serviceUrl(${serviceUrl ? 'yes' : 'no'})`);
364
+ const hasAuthConfig = !!authConfig;
365
+ this.logger?.debug(`Session check for ${destination}: hasToken(${hasToken}), hasAuthConfig(${hasAuthConfig}), serviceUrl(${serviceUrl ? 'yes' : 'no'})`);
532
366
  // Step 0: Initialize Session with Token (if needed)
533
- if (!hasToken && !hasUaaCredentials) {
534
- try {
535
- return await this.initializeSessionFromServiceKey(destination, serviceUrl);
536
- }
537
- catch (error) {
538
- // Re-throw BROWSER_AUTH_REQUIRED error without wrapping
539
- if (hasErrorCode(error) && error.code === 'BROWSER_AUTH_REQUIRED') {
540
- throw error;
541
- }
542
- // Handle typed store errors
543
- if (hasErrorCode(error)) {
544
- if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
545
- throw new Error(`Cannot initialize session for destination "${destination}": service key file not found`);
546
- }
547
- else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
548
- throw new Error(`Cannot initialize session for destination "${destination}": service key parsing failed - ${getErrorMessage(error)}`);
549
- }
550
- else if (error.code === interfaces_1.STORE_ERROR_CODES.INVALID_CONFIG) {
551
- throw new Error(`Cannot initialize session for destination "${destination}": invalid service key - missing ${error.missingFields?.join(', ') || 'required fields'}`);
552
- }
553
- }
367
+ if (!hasToken && !hasAuthConfig) {
368
+ if (!this.allowBrowserAuth) {
369
+ const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Either enable browser auth or provide a valid session with token.`);
370
+ error.code = 'BROWSER_AUTH_REQUIRED';
371
+ error.destination = destination;
372
+ this.logger?.error(`Step 0: Browser auth required but disabled for ${destination}`);
554
373
  throw error;
555
374
  }
375
+ const serviceKeyAuthConfig = await this.getAuthorizationConfigFromServiceKey(destination);
376
+ const tokenResult = await this.requestTokens(destination, 'serviceKey');
377
+ await this.persistTokenResult(destination, serviceUrl, connConfig, serviceKeyAuthConfig, tokenResult);
378
+ return tokenResult.authorizationToken;
556
379
  }
557
380
  // Step 1: Validate existing token
558
381
  if (hasToken && connConfig?.authorizationToken) {
@@ -566,25 +389,44 @@ class AuthBroker {
566
389
  return connConfig.authorizationToken;
567
390
  }
568
391
  }
569
- // Step 2: Refresh Token Flow
570
- this.logger?.debug(`Step 2: Attempting token refresh for ${destination}`);
571
- const uaaCredentials = await this.getUaaCredentials(destination, authConfig);
572
- // Step 2a: Try refresh from session (if refresh token exists)
573
- const refreshToken = authConfig?.refreshToken;
574
- if (refreshToken) {
392
+ // Step 2: Request tokens via provider, preferring session auth config
393
+ this.logger?.debug(`Step 2: Requesting tokens for ${destination}`);
394
+ let lastError = null;
395
+ if (authConfig) {
396
+ if (!this.allowBrowserAuth && !authConfig.refreshToken) {
397
+ const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Session has no refresh token.`);
398
+ error.code = 'BROWSER_AUTH_REQUIRED';
399
+ error.destination = destination;
400
+ this.logger?.error(`Step 2: Browser auth required but disabled for ${destination}`);
401
+ throw error;
402
+ }
575
403
  try {
576
- return await this.refreshTokenFromSession(destination, uaaCredentials, refreshToken, serviceUrl);
404
+ const tokenResult = await this.requestTokens(destination, 'session');
405
+ await this.persistTokenResult(destination, serviceUrl, connConfig, authConfig, tokenResult);
406
+ return tokenResult.authorizationToken;
577
407
  }
578
408
  catch (error) {
579
- this.logger?.debug(`Step 2a: refreshTokenFromSession failed for ${destination}: ${getErrorMessage(error)}, trying refreshTokenFromServiceKey`);
580
- // Continue to try service key refresh
409
+ lastError = error instanceof Error ? error : new Error(String(error));
410
+ this.logger?.debug(`Step 2: Token request via session failed for ${destination}: ${getErrorMessage(error)}, trying service key`);
581
411
  }
582
412
  }
583
- else {
584
- this.logger?.debug(`Step 2a: No refresh token in session for ${destination}, skipping to service key refresh`);
413
+ if (!this.allowBrowserAuth) {
414
+ const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Token refresh via session failed and browser auth is not allowed. Either enable browser auth or ensure a valid refresh token exists in session.`);
415
+ error.code = 'BROWSER_AUTH_REQUIRED';
416
+ error.destination = destination;
417
+ this.logger?.error(`Step 2: Browser auth required but disabled for ${destination}`);
418
+ throw error;
419
+ }
420
+ if (!this.serviceKeyStore) {
421
+ if (lastError) {
422
+ throw lastError;
423
+ }
424
+ throw new Error(`Authorization config not found for ${destination}. Session has no auth config and serviceKeyStore is not available.`);
585
425
  }
586
- // Step 2b: Try refresh from service key (browser authentication)
587
- return await this.refreshTokenFromServiceKey(destination, uaaCredentials, serviceUrl);
426
+ const serviceKeyAuthConfig = await this.getAuthorizationConfigFromServiceKey(destination);
427
+ const tokenResult = await this.requestTokens(destination, 'serviceKey');
428
+ await this.persistTokenResult(destination, serviceUrl, connConfig, serviceKeyAuthConfig, tokenResult);
429
+ return tokenResult.authorizationToken;
588
430
  }
589
431
  /**
590
432
  * Force refresh token for destination.
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export type { AuthType, ILogger, ITokenRefresher, } from '@mcp-abap-adt/interfaces';
6
6
  export { AuthBroker, type AuthBrokerConfig } from './AuthBroker';
7
- export type { ITokenProvider, TokenProviderOptions, TokenProviderResult, } from './providers';
7
+ export type { ITokenProvider, ITokenResult, TokenProviderOptions, } from './providers';
8
8
  export type { IAuthorizationConfig, IConnectionConfig, IServiceKeyStore, ISessionStore, } from './stores/interfaces';
9
9
  export type { IConfig } from './types';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,YAAY,EACV,QAAQ,EACR,OAAO,EACP,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEjE,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAGrB,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,YAAY,EACV,QAAQ,EACR,OAAO,EACP,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEjE,YAAY,EACV,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,aAAa,CAAC;AAGrB,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC"}
@@ -1,13 +1,9 @@
1
1
  /**
2
2
  * Token Provider interface
3
3
  *
4
- * Converts IAuthorizationConfig to IConnectionConfig by obtaining tokens.
5
- * Different implementations handle different authentication flows:
6
- * - XSUAA: client_credentials grant type (no browser)
7
- * - BTP/ABAP: browser-based OAuth2 or refresh token
4
+ * Stateful providers handle token lifecycle internally (refresh/relogin).
8
5
  */
9
- import type { IAuthorizationConfig, IConnectionConfig, ITokenProvider, ITokenProviderOptions, ITokenProviderResult } from '@mcp-abap-adt/interfaces';
10
- export type { ITokenProvider, IAuthorizationConfig, IConnectionConfig };
11
- export type TokenProviderResult = ITokenProviderResult;
6
+ import type { IAuthorizationConfig, IConnectionConfig, ITokenProvider, ITokenProviderOptions, ITokenResult } from '@mcp-abap-adt/interfaces';
7
+ export type { ITokenProvider, IAuthorizationConfig, IConnectionConfig, ITokenResult, };
12
8
  export type TokenProviderOptions = ITokenProviderOptions;
13
9
  //# sourceMappingURL=ITokenProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ITokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/ITokenProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,CAAC;AACxE,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC;AACvD,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAC"}
1
+ {"version":3,"file":"ITokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/ITokenProvider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,YAAY,EACb,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,GACb,CAAC;AACF,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAC"}
@@ -2,9 +2,6 @@
2
2
  /**
3
3
  * Token Provider interface
4
4
  *
5
- * Converts IAuthorizationConfig to IConnectionConfig by obtaining tokens.
6
- * Different implementations handle different authentication flows:
7
- * - XSUAA: client_credentials grant type (no browser)
8
- * - BTP/ABAP: browser-based OAuth2 or refresh token
5
+ * Stateful providers handle token lifecycle internally (refresh/relogin).
9
6
  */
10
7
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -4,5 +4,5 @@
4
4
  * Provider implementations are in separate packages:
5
5
  * - @mcp-abap-adt/auth-providers - XSUAA and BTP providers
6
6
  */
7
- export type { ITokenProvider, TokenProviderOptions, TokenProviderResult, } from './ITokenProvider';
7
+ export type { ITokenProvider, ITokenResult, TokenProviderOptions, } from './ITokenProvider';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,kBAAkB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-broker",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "JWT authentication broker for MCP ABAP ADT - manages tokens based on destination headers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",