@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +180 -27
  3. package/dist/auth/clientCredentialsAuth.d.ts.map +1 -1
  4. package/dist/auth/clientCredentialsAuth.js +2 -1
  5. package/dist/auth/oauthErrorBody.d.ts +15 -0
  6. package/dist/auth/oauthErrorBody.d.ts.map +1 -0
  7. package/dist/auth/oauthErrorBody.js +58 -0
  8. package/dist/auth/saml2TokenExchange.d.ts.map +1 -1
  9. package/dist/auth/saml2TokenExchange.js +9 -2
  10. package/dist/auth/samlBearerAssertion.d.ts.map +1 -1
  11. package/dist/auth/samlBearerAssertion.js +4 -1
  12. package/dist/auth/tokenRefresher.d.ts.map +1 -1
  13. package/dist/auth/tokenRefresher.js +2 -1
  14. package/dist/providers/BaseTokenProvider.d.ts +3 -1
  15. package/dist/providers/BaseTokenProvider.d.ts.map +1 -1
  16. package/dist/providers/BaseTokenProvider.js +4 -4
  17. package/dist/providers/saml2Utils.d.ts +3 -2
  18. package/dist/providers/saml2Utils.d.ts.map +1 -1
  19. package/dist/providers/saml2Utils.js +8 -0
  20. package/dist/validation/assertionValidator.d.ts.map +1 -1
  21. package/dist/validation/assertionValidator.js +207 -132
  22. package/dist/validation/signedNode.d.ts +2 -2
  23. package/dist/validation/signedNode.js +16 -5
  24. package/package.json +6 -12
  25. package/bin/auth-authorization-code.ts +0 -147
  26. package/bin/auth-client-credentials.ts +0 -109
  27. package/bin/utils/parseConfig.ts +0 -270
  28. package/dist/__tests__/helpers/configHelpers.d.ts +0 -46
  29. package/dist/__tests__/helpers/configHelpers.d.ts.map +0 -1
  30. package/dist/__tests__/helpers/configHelpers.js +0 -232
  31. package/dist/__tests__/helpers/netHelpers.d.ts +0 -19
  32. package/dist/__tests__/helpers/netHelpers.d.ts.map +0 -1
  33. package/dist/__tests__/helpers/netHelpers.js +0 -92
  34. package/dist/__tests__/helpers/testLogger.d.ts +0 -7
  35. package/dist/__tests__/helpers/testLogger.d.ts.map +0 -1
  36. package/dist/__tests__/helpers/testLogger.js +0 -45
  37. package/dist/__tests__/integration/stand/formLogin.d.ts +0 -68
  38. package/dist/__tests__/integration/stand/formLogin.d.ts.map +0 -1
  39. 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(/&amp;/g, '&')
156
- .replace(/&quot;/g, '"')
157
- .replace(/&#39;/g, "'")
158
- .replace(/&lt;/g, '<')
159
- .replace(/&gt;/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
- }