@mcp-abap-adt/auth-providers 4.0.0 → 4.1.2
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 +144 -0
- package/README.md +180 -27
- package/dist/auth/clientCredentialsAuth.d.ts.map +1 -1
- package/dist/auth/clientCredentialsAuth.js +2 -1
- package/dist/auth/oauthErrorBody.d.ts +15 -0
- package/dist/auth/oauthErrorBody.d.ts.map +1 -0
- package/dist/auth/oauthErrorBody.js +58 -0
- package/dist/auth/saml2TokenExchange.d.ts.map +1 -1
- package/dist/auth/saml2TokenExchange.js +9 -2
- package/dist/auth/samlBearerAssertion.d.ts.map +1 -1
- package/dist/auth/samlBearerAssertion.js +4 -1
- package/dist/auth/tokenRefresher.d.ts.map +1 -1
- package/dist/auth/tokenRefresher.js +2 -1
- package/dist/providers/BaseTokenProvider.d.ts +3 -1
- package/dist/providers/BaseTokenProvider.d.ts.map +1 -1
- package/dist/providers/BaseTokenProvider.js +4 -4
- package/dist/providers/saml2Utils.d.ts +3 -2
- package/dist/providers/saml2Utils.d.ts.map +1 -1
- package/dist/providers/saml2Utils.js +8 -0
- package/dist/validation/assertionValidator.d.ts.map +1 -1
- package/dist/validation/assertionValidator.js +207 -132
- package/dist/validation/signedNode.d.ts +2 -2
- package/dist/validation/signedNode.js +16 -5
- package/package.json +6 -12
- package/bin/auth-authorization-code.ts +0 -147
- package/bin/auth-client-credentials.ts +0 -109
- package/bin/utils/parseConfig.ts +0 -270
- package/dist/__tests__/helpers/configHelpers.d.ts +0 -46
- package/dist/__tests__/helpers/configHelpers.d.ts.map +0 -1
- package/dist/__tests__/helpers/configHelpers.js +0 -232
- package/dist/__tests__/helpers/netHelpers.d.ts +0 -19
- package/dist/__tests__/helpers/netHelpers.d.ts.map +0 -1
- package/dist/__tests__/helpers/netHelpers.js +0 -92
- package/dist/__tests__/helpers/testLogger.d.ts +0 -7
- package/dist/__tests__/helpers/testLogger.d.ts.map +0 -1
- package/dist/__tests__/helpers/testLogger.js +0 -45
- package/dist/__tests__/integration/stand/formLogin.d.ts +0 -68
- package/dist/__tests__/integration/stand/formLogin.d.ts.map +0 -1
- package/dist/__tests__/integration/stand/formLogin.js +0 -194
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Configuration helpers for auth-providers tests
|
|
4
|
-
* Loads test configuration from test-config.yaml
|
|
5
|
-
*/
|
|
6
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
-
if (k2 === undefined) k2 = k;
|
|
8
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
-
}
|
|
12
|
-
Object.defineProperty(o, k2, desc);
|
|
13
|
-
}) : (function(o, m, k, k2) {
|
|
14
|
-
if (k2 === undefined) k2 = k;
|
|
15
|
-
o[k2] = m[k];
|
|
16
|
-
}));
|
|
17
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
-
}) : function(o, v) {
|
|
20
|
-
o["default"] = v;
|
|
21
|
-
});
|
|
22
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
-
var ownKeys = function(o) {
|
|
24
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
-
var ar = [];
|
|
26
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
-
return ar;
|
|
28
|
-
};
|
|
29
|
-
return ownKeys(o);
|
|
30
|
-
};
|
|
31
|
-
return function (mod) {
|
|
32
|
-
if (mod && mod.__esModule) return mod;
|
|
33
|
-
var result = {};
|
|
34
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
-
__setModuleDefault(result, mod);
|
|
36
|
-
return result;
|
|
37
|
-
};
|
|
38
|
-
})();
|
|
39
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
exports.loadTestConfig = loadTestConfig;
|
|
41
|
-
exports.hasRealConfigValue = hasRealConfigValue;
|
|
42
|
-
exports.getDestination = getDestination;
|
|
43
|
-
exports.getServiceKeysDir = getServiceKeysDir;
|
|
44
|
-
exports.getSessionsDir = getSessionsDir;
|
|
45
|
-
exports.getServiceKeyPath = getServiceKeyPath;
|
|
46
|
-
exports.getSessionPath = getSessionPath;
|
|
47
|
-
exports.getAbapDestination = getAbapDestination;
|
|
48
|
-
exports.hasRealConfig = hasRealConfig;
|
|
49
|
-
const fs = __importStar(require("node:fs"));
|
|
50
|
-
const path = __importStar(require("node:path"));
|
|
51
|
-
const yaml = __importStar(require("js-yaml"));
|
|
52
|
-
let cachedConfig = null;
|
|
53
|
-
/**
|
|
54
|
-
* Find project root directory by looking for package.json
|
|
55
|
-
*/
|
|
56
|
-
function findProjectRoot() {
|
|
57
|
-
let currentDir = __dirname;
|
|
58
|
-
while (currentDir !== path.dirname(currentDir)) {
|
|
59
|
-
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
60
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
61
|
-
return currentDir;
|
|
62
|
-
}
|
|
63
|
-
currentDir = path.dirname(currentDir);
|
|
64
|
-
}
|
|
65
|
-
// Fallback to process.cwd() if package.json not found
|
|
66
|
-
return process.cwd();
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Load test configuration from YAML
|
|
70
|
-
* Uses test-config.yaml from tests/ directory
|
|
71
|
-
*/
|
|
72
|
-
function loadTestConfig() {
|
|
73
|
-
if (cachedConfig) {
|
|
74
|
-
return cachedConfig;
|
|
75
|
-
}
|
|
76
|
-
// Find project root and load from tests/test-config.yaml
|
|
77
|
-
const projectRoot = findProjectRoot();
|
|
78
|
-
const configPath = path.resolve(projectRoot, 'tests', 'test-config.yaml');
|
|
79
|
-
const templatePath = path.resolve(projectRoot, 'tests', 'test-config.yaml.template');
|
|
80
|
-
if (process.env.TEST_VERBOSE) {
|
|
81
|
-
console.log(`[configHelpers] Project root: ${projectRoot}`);
|
|
82
|
-
console.log(`[configHelpers] Config path: ${configPath}`);
|
|
83
|
-
console.log(`[configHelpers] Config exists: ${fs.existsSync(configPath)}`);
|
|
84
|
-
}
|
|
85
|
-
if (fs.existsSync(configPath)) {
|
|
86
|
-
try {
|
|
87
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
88
|
-
cachedConfig = yaml.load(configContent) || {};
|
|
89
|
-
if (process.env.TEST_VERBOSE) {
|
|
90
|
-
console.log(`[configHelpers] Loaded config:`, JSON.stringify(cachedConfig, null, 2));
|
|
91
|
-
}
|
|
92
|
-
return cachedConfig;
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
console.warn(`Failed to load test config from ${configPath}:`, error);
|
|
96
|
-
return {};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (fs.existsSync(templatePath)) {
|
|
100
|
-
console.warn('⚠️ tests/test-config.yaml not found. Using template (all integration tests will be disabled).');
|
|
101
|
-
try {
|
|
102
|
-
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
103
|
-
cachedConfig = yaml.load(templateContent) || {};
|
|
104
|
-
return cachedConfig;
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
console.warn(`Failed to load test config template from ${templatePath}:`, error);
|
|
108
|
-
return {};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
console.warn('⚠️ Test configuration files not found.');
|
|
112
|
-
console.warn('Please create tests/test-config.yaml with test parameters.');
|
|
113
|
-
return {};
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Check if test config has real values (not placeholders)
|
|
117
|
-
*/
|
|
118
|
-
function hasRealConfigValue(config) {
|
|
119
|
-
const cfg = config || loadTestConfig();
|
|
120
|
-
if (!cfg.destination) {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
// Check if destination is not a placeholder
|
|
124
|
-
return !cfg.destination.includes('<') && !cfg.destination.includes('>');
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Get destination from config
|
|
128
|
-
*/
|
|
129
|
-
function getDestination(config) {
|
|
130
|
-
const cfg = config || loadTestConfig();
|
|
131
|
-
return cfg.destination || null;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Get default destination directory based on platform
|
|
135
|
-
*/
|
|
136
|
-
function getDefaultDestinationDir() {
|
|
137
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
138
|
-
if (process.platform === 'win32') {
|
|
139
|
-
return path.join(homeDir, 'Documents', 'mcp-abap-adt');
|
|
140
|
-
}
|
|
141
|
-
return path.join(homeDir, '.config', 'mcp-abap-adt');
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Get destination directory from config or use default
|
|
145
|
-
*/
|
|
146
|
-
function getDestinationDir(config) {
|
|
147
|
-
const cfg = config || loadTestConfig();
|
|
148
|
-
if (cfg.destination_dir) {
|
|
149
|
-
// Expand ~ to home directory
|
|
150
|
-
if (cfg.destination_dir.startsWith('~')) {
|
|
151
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
152
|
-
return cfg.destination_dir.replace('~', homeDir);
|
|
153
|
-
}
|
|
154
|
-
return cfg.destination_dir;
|
|
155
|
-
}
|
|
156
|
-
return getDefaultDestinationDir();
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Get service keys directory from config
|
|
160
|
-
* Uses base_dir/service-keys or default platform path
|
|
161
|
-
*/
|
|
162
|
-
function getServiceKeysDir(config) {
|
|
163
|
-
const cfg = config || loadTestConfig();
|
|
164
|
-
// If service_key_path is specified, return its directory
|
|
165
|
-
if (cfg.service_key_path) {
|
|
166
|
-
const projectRoot = findProjectRoot();
|
|
167
|
-
const fullPath = path.resolve(projectRoot, cfg.service_key_path);
|
|
168
|
-
return path.dirname(fullPath);
|
|
169
|
-
}
|
|
170
|
-
// Use destination_dir/service-keys
|
|
171
|
-
const destinationDir = getDestinationDir(cfg);
|
|
172
|
-
return path.join(destinationDir, 'service-keys');
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Get sessions directory from config
|
|
176
|
-
* Uses base_dir/sessions or default platform path
|
|
177
|
-
*/
|
|
178
|
-
function getSessionsDir(config) {
|
|
179
|
-
const cfg = config || loadTestConfig();
|
|
180
|
-
// If session_path is specified, return its directory
|
|
181
|
-
if (cfg.session_path) {
|
|
182
|
-
const projectRoot = findProjectRoot();
|
|
183
|
-
const fullPath = path.resolve(projectRoot, cfg.session_path);
|
|
184
|
-
return path.dirname(fullPath);
|
|
185
|
-
}
|
|
186
|
-
// Use destination_dir/sessions
|
|
187
|
-
const destinationDir = getDestinationDir(cfg);
|
|
188
|
-
return path.join(destinationDir, 'sessions');
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Get service key file path
|
|
192
|
-
* Returns full path to service key file
|
|
193
|
-
*/
|
|
194
|
-
function getServiceKeyPath(config) {
|
|
195
|
-
const cfg = config || loadTestConfig();
|
|
196
|
-
const destination = cfg.destination;
|
|
197
|
-
if (!destination)
|
|
198
|
-
return null;
|
|
199
|
-
// If service_key_path is specified, use it
|
|
200
|
-
if (cfg.service_key_path) {
|
|
201
|
-
const projectRoot = findProjectRoot();
|
|
202
|
-
return path.resolve(projectRoot, cfg.service_key_path);
|
|
203
|
-
}
|
|
204
|
-
// Construct from directory + destination
|
|
205
|
-
const serviceKeysDir = getServiceKeysDir(cfg);
|
|
206
|
-
return path.join(serviceKeysDir, `${destination}.json`);
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Get session file path
|
|
210
|
-
* Returns full path to session file
|
|
211
|
-
*/
|
|
212
|
-
function getSessionPath(config) {
|
|
213
|
-
const cfg = config || loadTestConfig();
|
|
214
|
-
const destination = cfg.destination;
|
|
215
|
-
if (!destination)
|
|
216
|
-
return null;
|
|
217
|
-
// If session_path is specified, use it
|
|
218
|
-
if (cfg.session_path) {
|
|
219
|
-
const projectRoot = findProjectRoot();
|
|
220
|
-
return path.resolve(projectRoot, cfg.session_path);
|
|
221
|
-
}
|
|
222
|
-
// Construct from directory + destination
|
|
223
|
-
const sessionsDir = getSessionsDir(cfg);
|
|
224
|
-
return path.join(sessionsDir, `${destination}.env`);
|
|
225
|
-
}
|
|
226
|
-
// Legacy functions for backward compatibility
|
|
227
|
-
function getAbapDestination(config) {
|
|
228
|
-
return getDestination(config);
|
|
229
|
-
}
|
|
230
|
-
function hasRealConfig(config, _section) {
|
|
231
|
-
return hasRealConfigValue(config);
|
|
232
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export declare function getAvailablePort(): Promise<number>;
|
|
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>;
|
|
19
|
-
//# sourceMappingURL=netHelpers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.getAvailablePort = getAvailablePort;
|
|
37
|
-
exports.canListenOnLocalhost = canListenOnLocalhost;
|
|
38
|
-
exports.canOwnPort = canOwnPort;
|
|
39
|
-
const net = __importStar(require("node:net"));
|
|
40
|
-
async function getAvailablePort() {
|
|
41
|
-
return new Promise((resolve, reject) => {
|
|
42
|
-
const server = net.createServer();
|
|
43
|
-
server.once('error', reject);
|
|
44
|
-
server.listen(0, '127.0.0.1', () => {
|
|
45
|
-
const address = server.address();
|
|
46
|
-
if (typeof address === 'object' && address?.port) {
|
|
47
|
-
const port = address.port;
|
|
48
|
-
server.close(() => resolve(port));
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
server.close(() => reject(new Error('Failed to acquire a port')));
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
async function canListenOnLocalhost() {
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
const server = net.createServer();
|
|
59
|
-
server.once('error', () => resolve(false));
|
|
60
|
-
server.listen(0, '127.0.0.1', () => {
|
|
61
|
-
server.close(() => resolve(true));
|
|
62
|
-
});
|
|
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
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test logger with environment variable control
|
|
3
|
-
* Uses DefaultLogger from @mcp-abap-adt/logger for proper formatting
|
|
4
|
-
*/
|
|
5
|
-
import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
|
|
6
|
-
export declare function createTestLogger(prefix?: string): ILogger;
|
|
7
|
-
//# sourceMappingURL=testLogger.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Test logger with environment variable control
|
|
4
|
-
* Uses DefaultLogger from @mcp-abap-adt/logger for proper formatting
|
|
5
|
-
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.createTestLogger = createTestLogger;
|
|
8
|
-
const logger_1 = require("@mcp-abap-adt/logger");
|
|
9
|
-
function createTestLogger(prefix = 'TEST') {
|
|
10
|
-
// Check if logging is enabled
|
|
11
|
-
const isEnabled = () => {
|
|
12
|
-
return (process.env.DEBUG_PROVIDER === 'true' ||
|
|
13
|
-
process.env.DEBUG_AUTH_PROVIDERS === 'true' ||
|
|
14
|
-
process.env.DEBUG_BROWSER_AUTH === 'true' ||
|
|
15
|
-
process.env.DEBUG === 'true' ||
|
|
16
|
-
process.env.DEBUG?.includes('provider') === true ||
|
|
17
|
-
process.env.DEBUG?.includes('auth-providers') === true);
|
|
18
|
-
};
|
|
19
|
-
// Create DefaultLogger with appropriate log level
|
|
20
|
-
// getLogLevel respects AUTH_LOG_LEVEL env var and defaults to INFO
|
|
21
|
-
const baseLogger = new logger_1.DefaultLogger((0, logger_1.getLogLevel)());
|
|
22
|
-
// Return wrapper that checks if logging is enabled
|
|
23
|
-
return {
|
|
24
|
-
debug: (message, meta) => {
|
|
25
|
-
if (isEnabled()) {
|
|
26
|
-
baseLogger.debug(`[${prefix}] ${message}`, meta);
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
info: (message, meta) => {
|
|
30
|
-
if (isEnabled()) {
|
|
31
|
-
baseLogger.info(`[${prefix}] ${message}`, meta);
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
warn: (message, meta) => {
|
|
35
|
-
if (isEnabled()) {
|
|
36
|
-
baseLogger.warn(`[${prefix}] ${message}`, meta);
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
error: (message, meta) => {
|
|
40
|
-
if (isEnabled()) {
|
|
41
|
-
baseLogger.error(`[${prefix}] ${message}`, meta);
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plays the user in an interactive login against a stand server's own login
|
|
3
|
-
* page: follows redirects, keeps cookies, finds the form with a password
|
|
4
|
-
* field, fills it in with every hidden field it carries (UAA's CSRF token,
|
|
5
|
-
* Keycloak's session code), and submits it.
|
|
6
|
-
*
|
|
7
|
-
* Deliberately small: enough for UAA's and Keycloak's stock login pages, which
|
|
8
|
-
* the pinned image versions keep stable. It is a test helper, not a browser.
|
|
9
|
-
*/
|
|
10
|
-
export interface Credentials {
|
|
11
|
-
username: string;
|
|
12
|
-
password: string;
|
|
13
|
-
}
|
|
14
|
-
export declare class FormBrowser {
|
|
15
|
-
private readonly cookies;
|
|
16
|
-
/** GET `url` and follow redirects until `stop(url)` or a page is served. */
|
|
17
|
-
open(url: string, stop?: (next: string) => boolean): Promise<{
|
|
18
|
-
url: string;
|
|
19
|
-
html?: string;
|
|
20
|
-
}>;
|
|
21
|
-
/**
|
|
22
|
-
* Submit the login form on `page`, then follow redirects until `stop` says
|
|
23
|
-
* the next location is the one the caller wants — typically the client's
|
|
24
|
-
* redirect URI carrying the code — without requesting it.
|
|
25
|
-
*/
|
|
26
|
-
submitLogin(page: {
|
|
27
|
-
url: string;
|
|
28
|
-
html?: string;
|
|
29
|
-
}, credentials: Credentials, stop?: (next: string) => boolean): Promise<{
|
|
30
|
-
url: string;
|
|
31
|
-
html?: string;
|
|
32
|
-
}>;
|
|
33
|
-
/**
|
|
34
|
-
* Accept a consent page ("Do you grant these access privileges?"): submit
|
|
35
|
-
* the form that carries an `accept` button, with its hidden fields.
|
|
36
|
-
*/
|
|
37
|
-
acceptConsent(page: {
|
|
38
|
-
url: string;
|
|
39
|
-
html?: string;
|
|
40
|
-
}): Promise<{
|
|
41
|
-
url: string;
|
|
42
|
-
html?: string;
|
|
43
|
-
}>;
|
|
44
|
-
private follow;
|
|
45
|
-
private remember;
|
|
46
|
-
private cookieHeader;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Log in through the authorization URL and return the redirect URI the server
|
|
50
|
-
* sends the browser back to, with its `code` — not requested, since nothing
|
|
51
|
-
* listens there.
|
|
52
|
-
*/
|
|
53
|
-
export declare function authorizeByForm(authorizationUrl: string, redirectUri: string, credentials: Credentials): Promise<URL>;
|
|
54
|
-
/**
|
|
55
|
-
* Approve a device authorization the way a user would: open the verification
|
|
56
|
-
* URI (with the user code already in it), log in, and grant access.
|
|
57
|
-
*/
|
|
58
|
-
export declare function approveDevice(verificationUriComplete: string, credentials: Credentials): Promise<void>;
|
|
59
|
-
/**
|
|
60
|
-
* A SAML login at an identity provider: open the AuthnRequest URL, log in,
|
|
61
|
-
* and take the SAMLResponse from the auto-posting form the IdP answers with —
|
|
62
|
-
* what a browser would post to the assertion consumer service.
|
|
63
|
-
*/
|
|
64
|
-
export declare function samlResponseByForm(authnRequestUrl: string, credentials: Credentials): Promise<{
|
|
65
|
-
samlResponse: string;
|
|
66
|
-
acsUrl: string;
|
|
67
|
-
}>;
|
|
68
|
-
//# sourceMappingURL=formLogin.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"formLogin.d.ts","sourceRoot":"","sources":["../../../../src/__tests__/integration/stand/formLogin.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,4EAA4E;IACtE,IAAI,CACR,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAqB,GAC5C,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAI1C;;;;OAIG;IACG,WAAW,CACf,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EACpC,WAAW,EAAE,WAAW,EACxB,IAAI,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAqB,GAC5C,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAmB1C;;;OAGG;IACG,aAAa,CAAC,IAAI,EAAE;QACxB,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;YAgC7B,MAAM;IA4BpB,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,YAAY;CAGrB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,gBAAgB,EAAE,MAAM,EACxB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,GAAG,CAAC,CAcd;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,uBAAuB,EAAE,MAAM,EAC/B,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAgBnD"}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Plays the user in an interactive login against a stand server's own login
|
|
4
|
-
* page: follows redirects, keeps cookies, finds the form with a password
|
|
5
|
-
* field, fills it in with every hidden field it carries (UAA's CSRF token,
|
|
6
|
-
* Keycloak's session code), and submits it.
|
|
7
|
-
*
|
|
8
|
-
* Deliberately small: enough for UAA's and Keycloak's stock login pages, which
|
|
9
|
-
* the pinned image versions keep stable. It is a test helper, not a browser.
|
|
10
|
-
*/
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.FormBrowser = void 0;
|
|
13
|
-
exports.authorizeByForm = authorizeByForm;
|
|
14
|
-
exports.approveDevice = approveDevice;
|
|
15
|
-
exports.samlResponseByForm = samlResponseByForm;
|
|
16
|
-
const MAX_HOPS = 20;
|
|
17
|
-
class FormBrowser {
|
|
18
|
-
cookies = new Map();
|
|
19
|
-
/** GET `url` and follow redirects until `stop(url)` or a page is served. */
|
|
20
|
-
async open(url, stop = () => false) {
|
|
21
|
-
return this.follow(url, { method: 'GET' }, stop);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Submit the login form on `page`, then follow redirects until `stop` says
|
|
25
|
-
* the next location is the one the caller wants — typically the client's
|
|
26
|
-
* redirect URI carrying the code — without requesting it.
|
|
27
|
-
*/
|
|
28
|
-
async submitLogin(page, credentials, stop = () => false) {
|
|
29
|
-
const form = findPasswordForm(page.html ?? '');
|
|
30
|
-
if (!form) {
|
|
31
|
-
throw new Error(`no login form on ${page.url}`);
|
|
32
|
-
}
|
|
33
|
-
const body = new URLSearchParams(form.hidden);
|
|
34
|
-
body.set(form.userField, credentials.username);
|
|
35
|
-
body.set(form.passwordField, credentials.password);
|
|
36
|
-
return this.follow(new URL(form.action, page.url).toString(), {
|
|
37
|
-
method: 'POST',
|
|
38
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
39
|
-
body: body.toString(),
|
|
40
|
-
}, stop);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Accept a consent page ("Do you grant these access privileges?"): submit
|
|
44
|
-
* the form that carries an `accept` button, with its hidden fields.
|
|
45
|
-
*/
|
|
46
|
-
async acceptConsent(page) {
|
|
47
|
-
for (const match of (page.html ?? '').matchAll(/<form\b[^>]*>[\s\S]*?<\/form>/gi)) {
|
|
48
|
-
const formHtml = match[0];
|
|
49
|
-
const inputs = [...formHtml.matchAll(/<(?:input|button)\b[^>]*>/gi)].map((m) => m[0]);
|
|
50
|
-
const accept = inputs.find((i) => attribute(i, 'name') === 'accept');
|
|
51
|
-
if (!accept)
|
|
52
|
-
continue;
|
|
53
|
-
const body = new URLSearchParams();
|
|
54
|
-
for (const input of inputs) {
|
|
55
|
-
const name = attribute(input, 'name');
|
|
56
|
-
if (name && attribute(input, 'type') === 'hidden') {
|
|
57
|
-
body.set(name, attribute(input, 'value') ?? '');
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
body.set('accept', attribute(accept, 'value') ?? 'Yes');
|
|
61
|
-
const formTag = /<form\b[^>]*>/i.exec(formHtml)?.[0] ?? '';
|
|
62
|
-
return this.follow(new URL(attribute(formTag, 'action') ?? '', page.url).toString(), {
|
|
63
|
-
method: 'POST',
|
|
64
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
65
|
-
body: body.toString(),
|
|
66
|
-
}, () => false);
|
|
67
|
-
}
|
|
68
|
-
throw new Error(`no consent form on ${page.url}`);
|
|
69
|
-
}
|
|
70
|
-
async follow(url, init, stop) {
|
|
71
|
-
let current = url;
|
|
72
|
-
let request = init;
|
|
73
|
-
for (let hop = 0; hop < MAX_HOPS; hop++) {
|
|
74
|
-
const response = await fetch(current, {
|
|
75
|
-
...request,
|
|
76
|
-
redirect: 'manual',
|
|
77
|
-
headers: { ...(request.headers ?? {}), Cookie: this.cookieHeader() },
|
|
78
|
-
signal: AbortSignal.timeout(15_000),
|
|
79
|
-
});
|
|
80
|
-
this.remember(response);
|
|
81
|
-
const location = response.headers.get('location');
|
|
82
|
-
if (response.status >= 300 && response.status < 400 && location) {
|
|
83
|
-
const next = new URL(location, current).toString();
|
|
84
|
-
if (stop(next))
|
|
85
|
-
return { url: next };
|
|
86
|
-
current = next;
|
|
87
|
-
request = { method: 'GET' };
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
return { url: current, html: await response.text() };
|
|
91
|
-
}
|
|
92
|
-
throw new Error(`more than ${MAX_HOPS} redirects from ${url}`);
|
|
93
|
-
}
|
|
94
|
-
remember(response) {
|
|
95
|
-
for (const line of response.headers.getSetCookie()) {
|
|
96
|
-
const [pair] = line.split(';');
|
|
97
|
-
const eq = pair.indexOf('=');
|
|
98
|
-
if (eq > 0) {
|
|
99
|
-
this.cookies.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
cookieHeader() {
|
|
104
|
-
return [...this.cookies].map(([k, v]) => `${k}=${v}`).join('; ');
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
exports.FormBrowser = FormBrowser;
|
|
108
|
-
/**
|
|
109
|
-
* Log in through the authorization URL and return the redirect URI the server
|
|
110
|
-
* sends the browser back to, with its `code` — not requested, since nothing
|
|
111
|
-
* listens there.
|
|
112
|
-
*/
|
|
113
|
-
async function authorizeByForm(authorizationUrl, redirectUri, credentials) {
|
|
114
|
-
const reached = (next) => next.startsWith(redirectUri);
|
|
115
|
-
const browser = new FormBrowser();
|
|
116
|
-
const page = await browser.open(authorizationUrl, reached);
|
|
117
|
-
const done = page.html === undefined
|
|
118
|
-
? page
|
|
119
|
-
: await browser.submitLogin(page, credentials, reached);
|
|
120
|
-
if (!reached(done.url)) {
|
|
121
|
-
throw new Error(`login did not return to ${redirectUri}; ended at ${done.url}`);
|
|
122
|
-
}
|
|
123
|
-
return new URL(done.url);
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Approve a device authorization the way a user would: open the verification
|
|
127
|
-
* URI (with the user code already in it), log in, and grant access.
|
|
128
|
-
*/
|
|
129
|
-
async function approveDevice(verificationUriComplete, credentials) {
|
|
130
|
-
const browser = new FormBrowser();
|
|
131
|
-
const page = await browser.open(verificationUriComplete);
|
|
132
|
-
const consent = await browser.submitLogin(page, credentials);
|
|
133
|
-
await browser.acceptConsent(consent);
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* A SAML login at an identity provider: open the AuthnRequest URL, log in,
|
|
137
|
-
* and take the SAMLResponse from the auto-posting form the IdP answers with —
|
|
138
|
-
* what a browser would post to the assertion consumer service.
|
|
139
|
-
*/
|
|
140
|
-
async function samlResponseByForm(authnRequestUrl, credentials) {
|
|
141
|
-
const browser = new FormBrowser();
|
|
142
|
-
const page = await browser.submitLogin(await browser.open(authnRequestUrl), credentials);
|
|
143
|
-
const html = page.html ?? '';
|
|
144
|
-
const input = [...html.matchAll(/<input\b[^>]*>/gi)]
|
|
145
|
-
.map((m) => m[0])
|
|
146
|
-
.find((i) => attribute(i, 'name') === 'SAMLResponse');
|
|
147
|
-
const samlResponse = input ? attribute(input, 'value') : undefined;
|
|
148
|
-
const formTag = /<form\b[^>]*>/i.exec(html)?.[0] ?? '';
|
|
149
|
-
if (!samlResponse) {
|
|
150
|
-
throw new Error(`no SAMLResponse form on ${page.url}`);
|
|
151
|
-
}
|
|
152
|
-
return { samlResponse, acsUrl: attribute(formTag, 'action') ?? '' };
|
|
153
|
-
}
|
|
154
|
-
const decode = (value) => value
|
|
155
|
-
.replace(/&/g, '&')
|
|
156
|
-
.replace(/"/g, '"')
|
|
157
|
-
.replace(/'/g, "'")
|
|
158
|
-
.replace(/</g, '<')
|
|
159
|
-
.replace(/>/g, '>');
|
|
160
|
-
const attribute = (tag, name) => {
|
|
161
|
-
const match = new RegExp(`\\s${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, 'i').exec(tag);
|
|
162
|
-
return match ? decode(match[2] ?? match[3] ?? '') : undefined;
|
|
163
|
-
};
|
|
164
|
-
function findPasswordForm(html) {
|
|
165
|
-
for (const match of html.matchAll(/<form\b[^>]*>[\s\S]*?<\/form>/gi)) {
|
|
166
|
-
const formHtml = match[0];
|
|
167
|
-
const inputs = [...formHtml.matchAll(/<input\b[^>]*>/gi)].map((m) => m[0]);
|
|
168
|
-
const password = inputs.find((i) => attribute(i, 'type') === 'password');
|
|
169
|
-
if (!password)
|
|
170
|
-
continue;
|
|
171
|
-
const hidden = {};
|
|
172
|
-
let userField = 'username';
|
|
173
|
-
for (const input of inputs) {
|
|
174
|
-
const type = (attribute(input, 'type') ?? 'text').toLowerCase();
|
|
175
|
-
const name = attribute(input, 'name');
|
|
176
|
-
if (!name)
|
|
177
|
-
continue;
|
|
178
|
-
if (type === 'hidden')
|
|
179
|
-
hidden[name] = attribute(input, 'value') ?? '';
|
|
180
|
-
if ((type === 'text' || type === 'email') &&
|
|
181
|
-
/user|email|login/i.test(name)) {
|
|
182
|
-
userField = name;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const formTag = /<form\b[^>]*>/i.exec(formHtml)?.[0] ?? '';
|
|
186
|
-
return {
|
|
187
|
-
action: attribute(formTag, 'action') ?? '',
|
|
188
|
-
userField,
|
|
189
|
-
passwordField: attribute(password, 'name') ?? 'password',
|
|
190
|
-
hidden,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|