@mcp-abap-adt/auth-providers 1.2.0 → 2.2.0
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 +135 -0
- package/COPYING +674 -0
- package/LICENSE +160 -17
- package/README.md +401 -66
- package/bin/auth-authorization-code.ts +5 -3
- package/dist/__tests__/helpers/netHelpers.d.ts +16 -0
- package/dist/__tests__/helpers/netHelpers.d.ts.map +1 -1
- package/dist/__tests__/helpers/netHelpers.js +29 -0
- package/dist/__tests__/helpers/testLogger.d.ts +1 -1
- package/dist/__tests__/helpers/testLogger.d.ts.map +1 -1
- package/dist/auth/announce.d.ts +11 -0
- package/dist/auth/announce.d.ts.map +1 -0
- package/dist/auth/announce.js +19 -0
- package/dist/auth/browserAuth.d.ts +23 -15
- package/dist/auth/browserAuth.d.ts.map +1 -1
- package/dist/auth/browserAuth.js +25 -111
- package/dist/auth/callbackServer.d.ts +7 -1
- package/dist/auth/callbackServer.d.ts.map +1 -1
- package/dist/auth/callbackServer.js +31 -21
- package/dist/auth/deviceFlowAuth.d.ts +1 -1
- package/dist/auth/deviceFlowAuth.d.ts.map +1 -1
- package/dist/auth/oidcBrowserAuth.d.ts +2 -9
- package/dist/auth/oidcBrowserAuth.d.ts.map +1 -1
- package/dist/auth/oidcBrowserAuth.js +16 -125
- package/dist/auth/oidcDiscovery.d.ts +1 -1
- package/dist/auth/oidcDiscovery.d.ts.map +1 -1
- package/dist/auth/oidcToken.d.ts +1 -1
- package/dist/auth/oidcToken.d.ts.map +1 -1
- package/dist/auth/saml2Auth.d.ts +2 -9
- package/dist/auth/saml2Auth.d.ts.map +1 -1
- package/dist/auth/saml2Auth.js +10 -130
- package/dist/auth/saml2TokenExchange.d.ts +1 -1
- package/dist/auth/saml2TokenExchange.d.ts.map +1 -1
- package/dist/errors/TokenProviderErrors.js +6 -6
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/dist/providers/AuthorizationCodeProvider.d.ts +8 -3
- package/dist/providers/AuthorizationCodeProvider.d.ts.map +1 -1
- package/dist/providers/AuthorizationCodeProvider.js +55 -44
- package/dist/providers/BaseTokenProvider.d.ts +2 -1
- package/dist/providers/BaseTokenProvider.d.ts.map +1 -1
- package/dist/providers/ClientCredentialsProvider.d.ts +1 -1
- package/dist/providers/ClientCredentialsProvider.d.ts.map +1 -1
- package/dist/providers/ClientCredentialsProvider.js +3 -3
- package/dist/providers/DeviceFlowProvider.d.ts +3 -1
- package/dist/providers/DeviceFlowProvider.d.ts.map +1 -1
- package/dist/providers/DeviceFlowProvider.js +17 -14
- package/dist/providers/OidcBrowserProvider.d.ts +5 -7
- package/dist/providers/OidcBrowserProvider.d.ts.map +1 -1
- package/dist/providers/OidcBrowserProvider.js +58 -61
- package/dist/providers/OidcDeviceFlowProvider.d.ts +2 -1
- package/dist/providers/OidcDeviceFlowProvider.d.ts.map +1 -1
- package/dist/providers/OidcDeviceFlowProvider.js +13 -11
- package/dist/providers/OidcPasswordProvider.d.ts +2 -1
- package/dist/providers/OidcPasswordProvider.d.ts.map +1 -1
- package/dist/providers/OidcPasswordProvider.js +4 -4
- package/dist/providers/OidcTokenExchangeProvider.d.ts +2 -1
- package/dist/providers/OidcTokenExchangeProvider.d.ts.map +1 -1
- package/dist/providers/OidcTokenExchangeProvider.js +3 -3
- package/dist/providers/Saml2BearerProvider.d.ts +4 -3
- package/dist/providers/Saml2BearerProvider.d.ts.map +1 -1
- package/dist/providers/Saml2BearerProvider.js +6 -3
- package/dist/providers/Saml2PureProvider.d.ts +4 -3
- package/dist/providers/Saml2PureProvider.d.ts.map +1 -1
- package/dist/providers/Saml2PureProvider.js +6 -3
- package/dist/providers/saml2Utils.d.ts +13 -12
- package/dist/providers/saml2Utils.d.ts.map +1 -1
- package/dist/providers/saml2Utils.js +46 -26
- package/dist/sso/SsoProviderFactory.d.ts +1 -1
- package/dist/sso/SsoProviderFactory.d.ts.map +1 -1
- package/dist/sso/types.d.ts +1 -1
- package/dist/sso/types.d.ts.map +1 -1
- package/dist/strategies/BrowserCallbackStrategy.d.ts +64 -0
- package/dist/strategies/BrowserCallbackStrategy.d.ts.map +1 -0
- package/dist/strategies/BrowserCallbackStrategy.js +210 -0
- package/dist/strategies/asOidcResult.d.ts +11 -0
- package/dist/strategies/asOidcResult.d.ts.map +1 -0
- package/dist/strategies/asOidcResult.js +30 -0
- package/dist/strategies/codeStrategies.d.ts +22 -0
- package/dist/strategies/codeStrategies.d.ts.map +1 -0
- package/dist/strategies/codeStrategies.js +39 -0
- package/dist/strategies/index.d.ts +8 -0
- package/dist/strategies/index.d.ts.map +1 -0
- package/dist/strategies/index.js +18 -0
- package/dist/strategies/manualStrategies.d.ts +20 -0
- package/dist/strategies/manualStrategies.d.ts.map +1 -0
- package/dist/strategies/manualStrategies.js +77 -0
- package/package.json +8 -5
- package/dist/auth/manualInput.d.ts +0 -5
- package/dist/auth/manualInput.d.ts.map +0 -1
- package/dist/auth/manualInput.js +0 -19
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ABAP_CONNECTION_VARS,
|
|
11
11
|
} from '@mcp-abap-adt/auth-stores';
|
|
12
12
|
import { AuthorizationCodeProvider } from '../src/providers/AuthorizationCodeProvider';
|
|
13
|
+
import { browserCallbackStrategy } from '../src/strategies';
|
|
13
14
|
import {
|
|
14
15
|
getUaaCredentials,
|
|
15
16
|
parseEnvFile,
|
|
@@ -89,14 +90,15 @@ Example:
|
|
|
89
90
|
authorizationUrl = `${uaaUrl}/oauth/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// Create provider
|
|
93
|
+
// Create provider. `--port` now reaches the strategy that binds the socket;
|
|
94
|
+
// it must stay the port baked into the authorization URL above, or the
|
|
95
|
+
// provider rejects the mismatch before opening anything.
|
|
93
96
|
const provider = new AuthorizationCodeProvider({
|
|
94
97
|
authorizationUrl,
|
|
95
98
|
uaaUrl,
|
|
96
99
|
clientId,
|
|
97
100
|
clientSecret,
|
|
98
|
-
browser,
|
|
99
|
-
redirectPort: port,
|
|
101
|
+
authorization: browserCallbackStrategy({ browser, port }),
|
|
100
102
|
refreshToken: existingRefreshToken,
|
|
101
103
|
});
|
|
102
104
|
|
|
@@ -1,3 +1,19 @@
|
|
|
1
1
|
export declare function getAvailablePort(): Promise<number>;
|
|
2
2
|
export declare function canListenOnLocalhost(): Promise<boolean>;
|
|
3
|
+
/**
|
|
4
|
+
* Whether a login in this test could own `port` — and therefore whether
|
|
5
|
+
* asserting that it released the port means anything.
|
|
6
|
+
*
|
|
7
|
+
* A release assertion is a claim about our own cleanup. When an unrelated
|
|
8
|
+
* process already holds the port, the login fails at the probe, never binds
|
|
9
|
+
* anything, and cannot release anything: the port is still held afterwards,
|
|
10
|
+
* the code is behaving exactly as it should, and the assertion fails anyway.
|
|
11
|
+
* That is a test reporting on what else is running on the machine.
|
|
12
|
+
*
|
|
13
|
+
* So the gate is placed *before* the login rather than the assertion being
|
|
14
|
+
* widened until it always passes — a `toBe(true)` relaxed into "free or held"
|
|
15
|
+
* would also pass when our own cleanup leaked, which is the one thing these
|
|
16
|
+
* cases exist to catch.
|
|
17
|
+
*/
|
|
18
|
+
export declare function canOwnPort(port: number, context: string): Promise<boolean>;
|
|
3
19
|
//# sourceMappingURL=netHelpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"netHelpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/netHelpers.ts"],"names":[],"mappings":"AAEA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAcxD;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAQ7D"}
|
|
1
|
+
{"version":3,"file":"netHelpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/netHelpers.ts"],"names":[],"mappings":"AAEA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAcxD;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAQ7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAclB"}
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getAvailablePort = getAvailablePort;
|
|
37
37
|
exports.canListenOnLocalhost = canListenOnLocalhost;
|
|
38
|
+
exports.canOwnPort = canOwnPort;
|
|
38
39
|
const net = __importStar(require("node:net"));
|
|
39
40
|
async function getAvailablePort() {
|
|
40
41
|
return new Promise((resolve, reject) => {
|
|
@@ -61,3 +62,31 @@ async function canListenOnLocalhost() {
|
|
|
61
62
|
});
|
|
62
63
|
});
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether a login in this test could own `port` — and therefore whether
|
|
67
|
+
* asserting that it released the port means anything.
|
|
68
|
+
*
|
|
69
|
+
* A release assertion is a claim about our own cleanup. When an unrelated
|
|
70
|
+
* process already holds the port, the login fails at the probe, never binds
|
|
71
|
+
* anything, and cannot release anything: the port is still held afterwards,
|
|
72
|
+
* the code is behaving exactly as it should, and the assertion fails anyway.
|
|
73
|
+
* That is a test reporting on what else is running on the machine.
|
|
74
|
+
*
|
|
75
|
+
* So the gate is placed *before* the login rather than the assertion being
|
|
76
|
+
* widened until it always passes — a `toBe(true)` relaxed into "free or held"
|
|
77
|
+
* would also pass when our own cleanup leaked, which is the one thing these
|
|
78
|
+
* cases exist to catch.
|
|
79
|
+
*/
|
|
80
|
+
async function canOwnPort(port, context) {
|
|
81
|
+
const free = await new Promise((resolve) => {
|
|
82
|
+
const probe = net.createServer();
|
|
83
|
+
probe.once('error', () => resolve(false));
|
|
84
|
+
probe.listen(port, () => probe.close(() => resolve(true)));
|
|
85
|
+
});
|
|
86
|
+
if (!free) {
|
|
87
|
+
console.warn(`⚠️ Port ${port} is held by another process — skipping the port-release ` +
|
|
88
|
+
`assertions in "${context}". A login that never bound the socket cannot ` +
|
|
89
|
+
'release it; the remaining assertions in this case still run.');
|
|
90
|
+
}
|
|
91
|
+
return free;
|
|
92
|
+
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Test logger with environment variable control
|
|
3
3
|
* Uses DefaultLogger from @mcp-abap-adt/logger for proper formatting
|
|
4
4
|
*/
|
|
5
|
-
import type { ILogger } from '@mcp-abap-adt/interfaces';
|
|
5
|
+
import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
|
|
6
6
|
export declare function createTestLogger(prefix?: string): ILogger;
|
|
7
7
|
//# sourceMappingURL=testLogger.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testLogger.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/testLogger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"testLogger.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/testLogger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,MAAe,GAAG,OAAO,CAwCjE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an essential, user-facing prompt goes.
|
|
3
|
+
*
|
|
4
|
+
* Prompts are not log lines: a device code or an authorization URL the user
|
|
5
|
+
* cannot see makes the flow impassable, so they must survive the absence of a
|
|
6
|
+
* logger. They must equally never reach stdout, which carries protocol traffic
|
|
7
|
+
* under an MCP or LSP stdio transport.
|
|
8
|
+
*/
|
|
9
|
+
import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
|
|
10
|
+
export declare function announcer(logger?: ILogger): (msg: string) => void;
|
|
11
|
+
//# sourceMappingURL=announce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"announce.d.ts","sourceRoot":"","sources":["../../src/auth/announce.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAE9D,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAKjE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Where an essential, user-facing prompt goes.
|
|
4
|
+
*
|
|
5
|
+
* Prompts are not log lines: a device code or an authorization URL the user
|
|
6
|
+
* cannot see makes the flow impassable, so they must survive the absence of a
|
|
7
|
+
* logger. They must equally never reach stdout, which carries protocol traffic
|
|
8
|
+
* under an MCP or LSP stdio transport.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.announcer = announcer;
|
|
12
|
+
function announcer(logger) {
|
|
13
|
+
return (msg) => {
|
|
14
|
+
if (logger)
|
|
15
|
+
logger.info(msg);
|
|
16
|
+
else
|
|
17
|
+
process.stderr.write(`${msg}\n`);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser authentication - OAuth2 flow for obtaining tokens
|
|
3
3
|
*/
|
|
4
|
-
import type { IAuthorizationConfig
|
|
5
|
-
type
|
|
6
|
-
authorizationUrl?: string;
|
|
7
|
-
};
|
|
4
|
+
import type { IAuthorizationConfig } from '@mcp-abap-adt/interfaces-auth-sap';
|
|
5
|
+
import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
|
|
8
6
|
/**
|
|
9
7
|
* Extract an OAuth2 authorization code from arbitrary pasted input.
|
|
10
8
|
*
|
|
@@ -17,25 +15,35 @@ type BrowserAuthConfig = IAuthorizationConfig & {
|
|
|
17
15
|
* @internal - Exported for testing and for manual-paste flows.
|
|
18
16
|
*/
|
|
19
17
|
export declare function extractCode(input: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Build the OAuth2 authorization URL for a redirect URI that is already known.
|
|
20
|
+
*
|
|
21
|
+
* The URI is a parameter rather than a port because the port may have been
|
|
22
|
+
* chosen by the OS moments earlier — see `ICallbackServerOptions.port`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getJwtAuthorizationUrl(authConfig: IAuthorizationConfig, redirectUri: string): string;
|
|
20
25
|
/**
|
|
21
26
|
* Exchange authorization code for tokens
|
|
22
27
|
* @internal - Exported for testing
|
|
23
28
|
*/
|
|
24
|
-
export declare function exchangeCodeForToken(authConfig: IAuthorizationConfig, code: string,
|
|
29
|
+
export declare function exchangeCodeForToken(authConfig: IAuthorizationConfig, code: string, redirectUri: string, log?: ILogger | null): Promise<{
|
|
25
30
|
accessToken: string;
|
|
26
31
|
refreshToken?: string;
|
|
27
32
|
}>;
|
|
28
33
|
/**
|
|
29
|
-
*
|
|
34
|
+
* Open the authorization URL, or tell the user how to do it.
|
|
30
35
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
36
|
+
* Never awaited on the critical path by the caller: a launcher that hangs must
|
|
37
|
+
* not delay the login timeout or the release of the port. A launcher that fails
|
|
38
|
+
* is reported through the scope's `fail`, which is just another way for the
|
|
39
|
+
* scope to end.
|
|
35
40
|
*/
|
|
36
|
-
export declare function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
export declare function launchBrowser(authorizationUrl: string, browser: string, callbackUri: string, announce: (msg: string) => void, log: ILogger | null,
|
|
42
|
+
/**
|
|
43
|
+
* Extra guidance for 'none'/'headless', supplied only by a flow whose
|
|
44
|
+
* transport really offers another way in. The UAA callback server has a paste
|
|
45
|
+
* form on `/`; the OIDC and SAML ones do not, and promising one there sends
|
|
46
|
+
* the user to a 404.
|
|
47
|
+
*/
|
|
48
|
+
remoteHint?: string): Promise<void>;
|
|
41
49
|
//# sourceMappingURL=browserAuth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/browserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"browserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/browserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAa9D;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBxD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,oBAAoB,EAChC,WAAW,EAAE,MAAM,GAClB,MAAM,CASR;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,GACnB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA+CzD;AAeD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAC/B,GAAG,EAAE,OAAO,GAAG,IAAI;AACnB;;;;;GAKG;AACH,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAiGf"}
|
package/dist/auth/browserAuth.js
CHANGED
|
@@ -40,14 +40,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
40
40
|
};
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
42
|
exports.extractCode = extractCode;
|
|
43
|
+
exports.getJwtAuthorizationUrl = getJwtAuthorizationUrl;
|
|
43
44
|
exports.exchangeCodeForToken = exchangeCodeForToken;
|
|
44
|
-
exports.
|
|
45
|
+
exports.launchBrowser = launchBrowser;
|
|
45
46
|
const child_process = __importStar(require("node:child_process"));
|
|
46
|
-
const http = __importStar(require("node:http"));
|
|
47
|
-
const net = __importStar(require("node:net"));
|
|
48
|
-
const readline = __importStar(require("node:readline"));
|
|
49
47
|
const axios_1 = __importDefault(require("axios"));
|
|
50
|
-
const callbackServer_1 = require("./callbackServer");
|
|
51
48
|
const BROWSER_MAP = {
|
|
52
49
|
chrome: 'chrome',
|
|
53
50
|
edge: 'msedge',
|
|
@@ -89,12 +86,14 @@ function extractCode(input) {
|
|
|
89
86
|
return trimmed;
|
|
90
87
|
}
|
|
91
88
|
/**
|
|
92
|
-
*
|
|
89
|
+
* Build the OAuth2 authorization URL for a redirect URI that is already known.
|
|
90
|
+
*
|
|
91
|
+
* The URI is a parameter rather than a port because the port may have been
|
|
92
|
+
* chosen by the OS moments earlier — see `ICallbackServerOptions.port`.
|
|
93
93
|
*/
|
|
94
|
-
function getJwtAuthorizationUrl(authConfig,
|
|
94
|
+
function getJwtAuthorizationUrl(authConfig, redirectUri) {
|
|
95
95
|
const oauthUrl = authConfig.uaaUrl;
|
|
96
96
|
const clientid = authConfig.uaaClientId;
|
|
97
|
-
const redirectUri = `http://localhost:${port}/callback`;
|
|
98
97
|
if (!oauthUrl || !clientid) {
|
|
99
98
|
throw new Error('Authorization config missing UAA URL or client ID');
|
|
100
99
|
}
|
|
@@ -104,10 +103,9 @@ function getJwtAuthorizationUrl(authConfig, port = 3001) {
|
|
|
104
103
|
* Exchange authorization code for tokens
|
|
105
104
|
* @internal - Exported for testing
|
|
106
105
|
*/
|
|
107
|
-
async function exchangeCodeForToken(authConfig, code,
|
|
106
|
+
async function exchangeCodeForToken(authConfig, code, redirectUri, log) {
|
|
108
107
|
const { uaaUrl: url, uaaClientId: clientid, uaaClientSecret: clientsecret, } = authConfig;
|
|
109
108
|
const tokenUrl = `${url}/oauth/token`;
|
|
110
|
-
const redirectUri = `http://localhost:${port}/callback`;
|
|
111
109
|
const params = new URLSearchParams();
|
|
112
110
|
params.append('grant_type', 'authorization_code');
|
|
113
111
|
params.append('code', code);
|
|
@@ -148,44 +146,29 @@ function _isDebugEnabled() {
|
|
|
148
146
|
process.env.DEBUG?.includes('browser-auth') === true);
|
|
149
147
|
}
|
|
150
148
|
/**
|
|
151
|
-
*
|
|
149
|
+
* Open the authorization URL, or tell the user how to do it.
|
|
150
|
+
*
|
|
151
|
+
* Never awaited on the critical path by the caller: a launcher that hangs must
|
|
152
|
+
* not delay the login timeout or the release of the port. A launcher that fails
|
|
153
|
+
* is reported through the scope's `fail`, which is just another way for the
|
|
154
|
+
* scope to end.
|
|
152
155
|
*/
|
|
153
|
-
function
|
|
154
|
-
return new Promise((resolve) => {
|
|
155
|
-
const server = net.createServer();
|
|
156
|
-
server.listen(port, () => {
|
|
157
|
-
server.once('close', () => resolve(true));
|
|
158
|
-
server.close();
|
|
159
|
-
});
|
|
160
|
-
server.on('error', () => resolve(false));
|
|
161
|
-
});
|
|
162
|
-
}
|
|
156
|
+
async function launchBrowser(authorizationUrl, browser, callbackUri, announce, log,
|
|
163
157
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* @internal - Internal function, not exported from package
|
|
171
|
-
*/ /**
|
|
172
|
-
* Open the authorization URL, or tell the user how to do it.
|
|
173
|
-
*
|
|
174
|
-
* Never awaited on the critical path by the caller: a launcher that hangs must
|
|
175
|
-
* not delay the login timeout or the release of the port. A launcher that fails
|
|
176
|
-
* is reported through the scope's `fail`, which is just another way for the
|
|
177
|
-
* scope to end.
|
|
178
|
-
*/
|
|
179
|
-
async function launchBrowser(authorizationUrl, browser, port, announce, log) {
|
|
158
|
+
* Extra guidance for 'none'/'headless', supplied only by a flow whose
|
|
159
|
+
* transport really offers another way in. The UAA callback server has a paste
|
|
160
|
+
* form on `/`; the OIDC and SAML ones do not, and promising one there sends
|
|
161
|
+
* the user to a 404.
|
|
162
|
+
*/
|
|
163
|
+
remoteHint) {
|
|
180
164
|
const browserApp = BROWSER_MAP[browser];
|
|
181
165
|
// 'none' / 'headless': show the URL and wait. For SSH and remote sessions.
|
|
182
166
|
if (browser === 'none' || browser === 'headless') {
|
|
183
167
|
announce('🔗 Open this URL in your browser to authenticate:');
|
|
184
168
|
announce(` ${authorizationUrl}`);
|
|
185
|
-
announce(` Waiting for callback on
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
`http://<this-host>:${port}/ — or paste it here and press Enter.`);
|
|
169
|
+
announce(` Waiting for callback on ${callbackUri} ...`);
|
|
170
|
+
if (remoteHint)
|
|
171
|
+
announce(remoteHint);
|
|
189
172
|
return;
|
|
190
173
|
}
|
|
191
174
|
if (browser === 'auto') {
|
|
@@ -200,7 +183,7 @@ async function launchBrowser(authorizationUrl, browser, port, announce, log) {
|
|
|
200
183
|
log?.warn(`⚠️ Could not open browser automatically: ${message}`);
|
|
201
184
|
announce('🔗 Please open this URL in your browser to authenticate:');
|
|
202
185
|
announce(` ${authorizationUrl}`);
|
|
203
|
-
announce(` Waiting for callback on
|
|
186
|
+
announce(` Waiting for callback on ${callbackUri} ...`);
|
|
204
187
|
}
|
|
205
188
|
return;
|
|
206
189
|
}
|
|
@@ -269,72 +252,3 @@ async function launchBrowser(authorizationUrl, browser, port, announce, log) {
|
|
|
269
252
|
else
|
|
270
253
|
await open(authorizationUrl);
|
|
271
254
|
}
|
|
272
|
-
/**
|
|
273
|
-
* Interactive browser login for the UAA authorization-code flow.
|
|
274
|
-
*
|
|
275
|
-
* The callback socket is owned by `withBrowserCallbackServer`: it is released
|
|
276
|
-
* when the scope ends, whatever ends it, and before the code is exchanged — so
|
|
277
|
-
* a slow UAA cannot hold the port, and a settled promise always means the port
|
|
278
|
-
* is free.
|
|
279
|
-
*/
|
|
280
|
-
async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3001, timeoutMs = 30 * 1000) {
|
|
281
|
-
const log = logger || null;
|
|
282
|
-
// Essential, user-facing prompts (the auth URL, paste instructions) must be
|
|
283
|
-
// visible even when no logger is supplied. Fall back to stderr — never stdout,
|
|
284
|
-
// so stdio-based RPC transports (MCP/LSP) are not corrupted.
|
|
285
|
-
const announce = (msg) => {
|
|
286
|
-
if (log)
|
|
287
|
-
log.info(msg);
|
|
288
|
-
else
|
|
289
|
-
process.stderr.write(`${msg}\n`);
|
|
290
|
-
};
|
|
291
|
-
// Pre-check kept for its message: AuthBroker matches /already in use/i to
|
|
292
|
-
// distinguish a busy port from other failures.
|
|
293
|
-
const portAvailable = await isPortAvailable(port);
|
|
294
|
-
if (!portAvailable) {
|
|
295
|
-
throw new Error(`Port ${port} is already in use. Please specify a different port or free the port.`);
|
|
296
|
-
}
|
|
297
|
-
let stdinReader = null;
|
|
298
|
-
const code = await (0, callbackServer_1.withBrowserCallbackServer)({ port, timeoutMs }, async (server) => {
|
|
299
|
-
const authorizationUrl = authConfig.authorizationUrl ??
|
|
300
|
-
getJwtAuthorizationUrl(authConfig, server.port);
|
|
301
|
-
log?.info(`[browserAuth] Authorization URL: ${authorizationUrl}`);
|
|
302
|
-
log?.info(`[browserAuth] Server listening on port: ${server.port}`);
|
|
303
|
-
const waiting = server.waitForResult();
|
|
304
|
-
// Not awaited: a launcher that hangs must not delay the timeout or the
|
|
305
|
-
// release, and one that fails ends the scope through `fail`.
|
|
306
|
-
void launchBrowser(authorizationUrl, browser, server.port, announce, log).catch((error) => {
|
|
307
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
-
log?.error(`❌ Failed to open browser: ${message}. Please open manually: ${authorizationUrl}`, { error: message, url: authorizationUrl });
|
|
309
|
-
server.fail(new Error(`Browser opening failed for destination authentication. Please open manually: ${authorizationUrl}`));
|
|
310
|
-
});
|
|
311
|
-
// Manual stdin paste — only when attached to an interactive terminal.
|
|
312
|
-
// Under a stdio RPC transport stdin carries the protocol, so we must never
|
|
313
|
-
// consume it; isTTY guards that. A pasted line is handed to the same
|
|
314
|
-
// `/submit` route the paste form uses, so there is one way in, not two.
|
|
315
|
-
if ((browser === 'none' || browser === 'headless') &&
|
|
316
|
-
process.stdin.isTTY) {
|
|
317
|
-
stdinReader = readline.createInterface({ input: process.stdin });
|
|
318
|
-
stdinReader.on('line', (line) => {
|
|
319
|
-
const pasted = extractCode(line);
|
|
320
|
-
if (!pasted) {
|
|
321
|
-
process.stderr.write('Could not read an authorization code from that input. Try again.\n');
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const req = http.get({
|
|
325
|
-
host: '127.0.0.1',
|
|
326
|
-
port: server.port,
|
|
327
|
-
path: `/submit?input=${encodeURIComponent(pasted)}`,
|
|
328
|
-
agent: false,
|
|
329
|
-
});
|
|
330
|
-
req.on('error', () => undefined);
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
return await waiting;
|
|
334
|
-
}).finally(() => {
|
|
335
|
-
stdinReader?.close();
|
|
336
|
-
stdinReader = null;
|
|
337
|
-
});
|
|
338
|
-
log?.info('[browserAuth] Exchanging code for token...');
|
|
339
|
-
return await exchangeCodeForToken(authConfig, code, port, log);
|
|
340
|
-
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* See `docs/superpowers/specs/2026-07-28-callback-server-contract-design.md`.
|
|
10
10
|
*/
|
|
11
|
-
import type { CallbackServerFactory, ICallbackServerHandle, ICallbackServerOptions } from '@mcp-abap-adt/interfaces';
|
|
11
|
+
import type { CallbackServerFactory, ICallbackServerHandle, ICallbackServerOptions } from '@mcp-abap-adt/interfaces-auth';
|
|
12
12
|
import express from 'express';
|
|
13
13
|
/**
|
|
14
14
|
* How a route reports an outcome. Settling is deferred until the response has
|
|
@@ -20,6 +20,12 @@ export interface Settle<TResult> {
|
|
|
20
20
|
ok(value: TResult, res?: express.Response): void;
|
|
21
21
|
/** The callback reported a failure. Ends the scope. */
|
|
22
22
|
err(error: Error, res?: express.Response): void;
|
|
23
|
+
/**
|
|
24
|
+
* This was not our redirect — a reloaded tab, a prefetch, a port scanner.
|
|
25
|
+
* Answered and counted; the login keeps waiting, bounded as ever by the
|
|
26
|
+
* timeout. Ends nothing.
|
|
27
|
+
*/
|
|
28
|
+
ignore(reason: string, res?: express.Response): void;
|
|
23
29
|
}
|
|
24
30
|
export type RouteSetup<TResult> = (app: express.Express, settle: Settle<TResult>) => void;
|
|
25
31
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"callbackServer.d.ts","sourceRoot":"","sources":["../../src/auth/callbackServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,
|
|
1
|
+
{"version":3,"file":"callbackServer.d.ts","sourceRoot":"","sources":["../../src/auth/callbackServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,OAAO,MAAM,SAAS,CAAC;AAS9B;;;;GAIG;AACH,MAAM,WAAW,MAAM,CAAC,OAAO;IAC7B,yEAAyE;IACzE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IACjD,uDAAuD;IACvD,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAChD;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;CACtD;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,CAChC,GAAG,EAAE,OAAO,CAAC,OAAO,EACpB,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,KACpB,IAAI,CAAC;AAqBV;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EACrD,OAAO,EAAE,sBAAsB,EAC/B,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAC3B,GAAG,EAAE,CAAC,MAAM,EAAE,qBAAqB,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAChE,OAAO,CAAC,OAAO,CAAC,CAqMlB;AAyCD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,qBAAqB,CAAC,MAAM,CA4DjE,CAAC"}
|
|
@@ -57,8 +57,8 @@ const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
|
57
57
|
const SHUTDOWN_GRACE_MS = 500;
|
|
58
58
|
function validate(options) {
|
|
59
59
|
const { port, timeoutMs } = options;
|
|
60
|
-
if (!Number.isInteger(port) || port <
|
|
61
|
-
throw new Error(`Invalid callback server port: ${String(port)}. Must be an integer in
|
|
60
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
61
|
+
throw new Error(`Invalid callback server port: ${String(port)}. Must be an integer in 0..65535.`);
|
|
62
62
|
}
|
|
63
63
|
if (!Number.isFinite(timeoutMs) ||
|
|
64
64
|
timeoutMs <= 0 ||
|
|
@@ -105,6 +105,7 @@ async function runCallbackScope(options, routes, use) {
|
|
|
105
105
|
});
|
|
106
106
|
let timer = null;
|
|
107
107
|
let alive = false;
|
|
108
|
+
let ignored = 0;
|
|
108
109
|
const settleResult = (outcome) => {
|
|
109
110
|
if (resultSettled)
|
|
110
111
|
return;
|
|
@@ -202,25 +203,13 @@ async function runCallbackScope(options, routes, use) {
|
|
|
202
203
|
endScope({ error });
|
|
203
204
|
});
|
|
204
205
|
},
|
|
206
|
+
ignore(reason) {
|
|
207
|
+
ignored += 1;
|
|
208
|
+
options.logger?.warn('[callbackServer] ignored an incomplete callback request', { reason, ignored });
|
|
209
|
+
},
|
|
205
210
|
};
|
|
206
211
|
routes(app, settle);
|
|
207
212
|
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
208
|
-
const handle = {
|
|
209
|
-
port: options.port,
|
|
210
|
-
redirectUri: `http://localhost:${options.port}/callback`,
|
|
211
|
-
waitForResult: () => alive
|
|
212
|
-
? resultPromise
|
|
213
|
-
: Promise.reject(new Error('Callback server scope has ended')),
|
|
214
|
-
// Silent no-op once the scope has ended: this is called fire-and-forget
|
|
215
|
-
// from a browser launcher's .catch(), and a late rejection must not become
|
|
216
|
-
// a fresh unhandled rejection.
|
|
217
|
-
fail: (error) => {
|
|
218
|
-
if (!alive)
|
|
219
|
-
return;
|
|
220
|
-
settleResult({ error });
|
|
221
|
-
endScope({ error });
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
213
|
server.once('error', (error) => {
|
|
225
214
|
endScope({ error });
|
|
226
215
|
});
|
|
@@ -232,10 +221,31 @@ async function runCallbackScope(options, routes, use) {
|
|
|
232
221
|
}
|
|
233
222
|
alive = true;
|
|
234
223
|
timer = setTimeout(() => {
|
|
224
|
+
const tally = ignored > 0
|
|
225
|
+
? ` ${ignored} incomplete request(s) reached /callback and were ignored.`
|
|
226
|
+
: '';
|
|
235
227
|
endScope({
|
|
236
|
-
error: new Error(`Authentication timeout after ${options.timeoutMs / 1000} seconds. Please try again
|
|
228
|
+
error: new Error(`Authentication timeout after ${options.timeoutMs / 1000} seconds. Please try again.${tally}`),
|
|
237
229
|
});
|
|
238
230
|
}, options.timeoutMs);
|
|
231
|
+
// The requested port may be 0, in which case only the OS knows the answer.
|
|
232
|
+
const bound = server.address().port;
|
|
233
|
+
const handle = {
|
|
234
|
+
port: bound,
|
|
235
|
+
redirectUri: `http://localhost:${bound}/callback`,
|
|
236
|
+
waitForResult: () => alive
|
|
237
|
+
? resultPromise
|
|
238
|
+
: Promise.reject(new Error('Callback server scope has ended')),
|
|
239
|
+
// Silent no-op once the scope has ended: this is called fire-and-forget
|
|
240
|
+
// from a browser launcher's .catch(), and a late rejection must not become
|
|
241
|
+
// a fresh unhandled rejection.
|
|
242
|
+
fail: (error) => {
|
|
243
|
+
if (!alive)
|
|
244
|
+
return;
|
|
245
|
+
settleResult({ error });
|
|
246
|
+
endScope({ error });
|
|
247
|
+
},
|
|
248
|
+
};
|
|
239
249
|
void use(handle).then((value) => endScope({ value }), (error) => endScope({ error }));
|
|
240
250
|
});
|
|
241
251
|
return await scopePromise;
|
|
@@ -296,8 +306,8 @@ const withBrowserCallbackServer = (options, use) => runCallbackScope(options, (a
|
|
|
296
306
|
}
|
|
297
307
|
const { code } = req.query;
|
|
298
308
|
if (!code || typeof code !== 'string') {
|
|
299
|
-
res.status(400).send('Error:
|
|
300
|
-
settle.
|
|
309
|
+
res.status(400).send('Error: not an authorization callback');
|
|
310
|
+
settle.ignore('no code and no error in query', res);
|
|
301
311
|
return;
|
|
302
312
|
}
|
|
303
313
|
res.send(successHtml);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* OAuth2 Device Flow for devices without browser or input capabilities.
|
|
5
5
|
* User authorizes on another device by entering a code.
|
|
6
6
|
*/
|
|
7
|
-
import type { ILogger } from '@mcp-abap-adt/interfaces';
|
|
7
|
+
import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
|
|
8
8
|
export interface DeviceFlowResult {
|
|
9
9
|
deviceCode: string;
|
|
10
10
|
userCode: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deviceFlowAuth.d.ts","sourceRoot":"","sources":["../../src/auth/deviceFlowAuth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"deviceFlowAuth.d.ts","sourceRoot":"","sources":["../../src/auth/deviceFlowAuth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,gBAAgB,CAAC,CA4D3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,GAAE,MAAU,EACpB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,gBAAgB,CAAC,CAoG3B"}
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OIDC browser authorization code flow (capture code)
|
|
3
3
|
*/
|
|
4
|
-
import type {
|
|
4
|
+
import type { CallbackServerFactory } from '@mcp-abap-adt/interfaces-auth';
|
|
5
5
|
export interface OidcCallbackResult {
|
|
6
6
|
code: string;
|
|
7
7
|
state?: string;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
* OIDC browser login.
|
|
11
|
-
*
|
|
12
|
-
* The callback socket belongs to the scope: it is released when the scope ends,
|
|
13
|
-
* whatever ends it. Before this, the flow had no timeout at all — an abandoned
|
|
14
|
-
* login never settled and the port was held for the life of the process.
|
|
15
|
-
*/
|
|
16
|
-
export declare function startOidcBrowserAuth(authorizationUrl: string, browser: string, logger?: ILogger, port?: number, timeoutMs?: number): Promise<OidcCallbackResult>;
|
|
9
|
+
export declare const withOidcCallbackServer: CallbackServerFactory<OidcCallbackResult>;
|
|
17
10
|
//# sourceMappingURL=oidcBrowserAuth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oidcBrowserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/oidcBrowserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"oidcBrowserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/oidcBrowserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,qBAAqB,EAGtB,MAAM,+BAA+B,CAAC;AAGvC,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,sBAAsB,EAAE,qBAAqB,CACxD,kBAAkB,CA6CjB,CAAC"}
|