@enterprisestandard/esv 0.0.5-beta.20260114.3 → 0.0.5-beta.20260115.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/dist/iam/index.js +664 -5500
- package/dist/iam/index.js.map +1 -23
- package/dist/index.js +156 -6554
- package/dist/index.js.map +1 -27
- package/dist/runner.js +280 -10928
- package/dist/runner.js.map +1 -33
- package/dist/server/crypto.js +134 -0
- package/dist/server/crypto.js.map +1 -0
- package/dist/server/iam.js +402 -0
- package/dist/server/iam.js.map +1 -0
- package/dist/server/index.js +34 -1380
- package/dist/server/index.js.map +1 -16
- package/dist/server/server.js +223 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/sso.js +428 -0
- package/dist/server/sso.js.map +1 -0
- package/dist/server/state.js +152 -0
- package/dist/server/state.js.map +1 -0
- package/dist/server/vault.js +92 -0
- package/dist/server/vault.js.map +1 -0
- package/dist/server/workload.js +226 -0
- package/dist/server/workload.js.map +1 -0
- package/dist/sso/index.js +355 -428
- package/dist/sso/index.js.map +1 -11
- package/dist/tenant/index.js +300 -0
- package/dist/tenant/index.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +139 -0
- package/dist/utils.js.map +1 -0
- package/dist/workload/index.js +404 -474
- package/dist/workload/index.js.map +1 -11
- package/package.json +1 -1
package/dist/sso/index.js
CHANGED
|
@@ -1,449 +1,376 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SSO Validation Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests validate that an application correctly implements
|
|
5
|
+
* Enterprise Standard SSO (Single Sign-On) functionality.
|
|
6
|
+
*/
|
|
7
|
+
import { assert, buildCookieHeader, createFetcher, parseCookies, runTest, skipTest } from '../utils';
|
|
8
|
+
/**
|
|
9
|
+
* Default configuration for SSO validation
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
loginPath: '/api/auth/login',
|
|
13
|
+
callbackPath: '/api/auth/callback',
|
|
14
|
+
userPath: '/api/auth/user',
|
|
15
|
+
logoutPath: '/api/auth/logout',
|
|
16
|
+
backChannelLogoutPath: '/api/auth/logout/backchannel',
|
|
17
|
+
tokenPath: '/api/auth/token',
|
|
18
|
+
refreshPath: '/api/auth/refresh',
|
|
18
19
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Test: Login endpoint exists and redirects to authorization URL
|
|
22
|
+
*/
|
|
23
|
+
async function testLoginEndpoint(config, fetch) {
|
|
24
|
+
return runTest('SSO Login Endpoint', async () => {
|
|
25
|
+
const response = await fetch(config.loginPath);
|
|
26
|
+
// Should return a redirect (302)
|
|
27
|
+
assert(response.status === 302, `Expected 302 redirect, got ${response.status}`);
|
|
28
|
+
// Should have a Location header pointing to the authorization URL
|
|
29
|
+
const location = response.headers.get('Location');
|
|
30
|
+
assert(location !== null, 'Missing Location header in redirect');
|
|
31
|
+
// If an expected pattern is provided, validate the URL
|
|
32
|
+
if (config.expectedAuthorizationUrlPattern) {
|
|
33
|
+
assert(config.expectedAuthorizationUrlPattern.test(location), `Authorization URL does not match expected pattern: ${location}`);
|
|
34
|
+
}
|
|
35
|
+
// Should have required OIDC parameters
|
|
36
|
+
const url = new URL(location);
|
|
37
|
+
assert(url.searchParams.has('client_id'), 'Missing client_id in authorization URL');
|
|
38
|
+
assert(url.searchParams.has('redirect_uri'), 'Missing redirect_uri in authorization URL');
|
|
39
|
+
assert(url.searchParams.has('response_type'), 'Missing response_type in authorization URL');
|
|
40
|
+
assert(url.searchParams.has('scope'), 'Missing scope in authorization URL');
|
|
41
|
+
assert(url.searchParams.has('state'), 'Missing state in authorization URL');
|
|
42
|
+
assert(url.searchParams.has('code_challenge'), 'Missing code_challenge (PKCE) in authorization URL');
|
|
43
|
+
assert(url.searchParams.has('code_challenge_method'), 'Missing code_challenge_method (PKCE) in authorization URL');
|
|
44
|
+
// Should set a state cookie
|
|
45
|
+
const cookies = parseCookies(response.headers);
|
|
46
|
+
const hasStateCookie = Array.from(cookies.keys()).some((name) => name.includes('.state'));
|
|
47
|
+
assert(hasStateCookie, 'Missing state cookie in response');
|
|
48
|
+
return {
|
|
49
|
+
details: {
|
|
50
|
+
authorizationUrl: location,
|
|
51
|
+
pkceEnabled: true,
|
|
52
|
+
stateParameter: url.searchParams.get('state'),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
41
55
|
});
|
|
42
|
-
};
|
|
43
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
44
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
45
|
-
|
|
46
|
-
// packages/esv/src/utils.ts
|
|
47
|
-
function createFetcher(config) {
|
|
48
|
-
const timeout = config.timeout ?? 5000;
|
|
49
|
-
const headers = config.headers ?? {};
|
|
50
|
-
return async function fetcher(path, options = {}) {
|
|
51
|
-
const url = `${config.baseUrl}${path}`;
|
|
52
|
-
const controller = new AbortController;
|
|
53
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
54
|
-
try {
|
|
55
|
-
const response = await fetch(url, {
|
|
56
|
-
...options,
|
|
57
|
-
signal: controller.signal,
|
|
58
|
-
headers: {
|
|
59
|
-
...headers,
|
|
60
|
-
...options.headers
|
|
61
|
-
},
|
|
62
|
-
redirect: "manual"
|
|
63
|
-
});
|
|
64
|
-
return response;
|
|
65
|
-
} finally {
|
|
66
|
-
clearTimeout(timeoutId);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
async function runTest(name, testFn) {
|
|
71
|
-
const start = performance.now();
|
|
72
|
-
try {
|
|
73
|
-
const result = await testFn();
|
|
74
|
-
return {
|
|
75
|
-
name,
|
|
76
|
-
passed: true,
|
|
77
|
-
duration: performance.now() - start,
|
|
78
|
-
details: result?.details
|
|
79
|
-
};
|
|
80
|
-
} catch (error) {
|
|
81
|
-
return {
|
|
82
|
-
name,
|
|
83
|
-
passed: false,
|
|
84
|
-
error: error instanceof Error ? error.message : String(error),
|
|
85
|
-
duration: performance.now() - start
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function skipTest(name, reason) {
|
|
90
|
-
return {
|
|
91
|
-
name,
|
|
92
|
-
passed: true,
|
|
93
|
-
duration: 0,
|
|
94
|
-
details: { skipped: true, reason }
|
|
95
|
-
};
|
|
96
56
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Test: User endpoint returns 401 when not authenticated
|
|
59
|
+
*/
|
|
60
|
+
async function testUserEndpointUnauthenticated(config, fetch) {
|
|
61
|
+
return runTest('SSO User Endpoint (Unauthenticated)', async () => {
|
|
62
|
+
const response = await fetch(config.userPath);
|
|
63
|
+
// Should return 401 Unauthorized
|
|
64
|
+
assert(response.status === 401, `Expected 401 Unauthorized, got ${response.status}`);
|
|
65
|
+
return {
|
|
66
|
+
details: {
|
|
67
|
+
status: response.status,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
});
|
|
106
71
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Test: Logout endpoint clears cookies
|
|
74
|
+
*/
|
|
75
|
+
async function testLogoutEndpoint(config, fetch) {
|
|
76
|
+
return runTest('SSO Logout Endpoint', async () => {
|
|
77
|
+
const response = await fetch(`${config.logoutPath}?redirect=/`);
|
|
78
|
+
// Should return 302 redirect (with redirect param) or 200
|
|
79
|
+
assert(response.status === 302 || response.status === 200, `Expected 302 or 200, got ${response.status}`);
|
|
80
|
+
// Should clear cookies
|
|
81
|
+
const cookies = parseCookies(response.headers);
|
|
82
|
+
const clearedCookies = [];
|
|
83
|
+
for (const [name, value] of cookies.entries()) {
|
|
84
|
+
// Check if the cookie is being cleared (Max-Age=0 or empty value)
|
|
85
|
+
if (value === '' || value.includes('Max-Age=0')) {
|
|
86
|
+
clearedCookies.push(name);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Check for standard SSO cookies being cleared
|
|
90
|
+
const setCookieHeaders = response.headers.getSetCookie?.() ?? [];
|
|
91
|
+
const hasExpiredCookies = setCookieHeaders.some((cookie) => cookie.includes('Max-Age=0') || cookie.includes('expires=Thu, 01 Jan 1970'));
|
|
92
|
+
assert(hasExpiredCookies || clearedCookies.length > 0 || setCookieHeaders.length === 0, 'Logout should clear session cookies');
|
|
93
|
+
return {
|
|
94
|
+
details: {
|
|
95
|
+
status: response.status,
|
|
96
|
+
clearedCookies,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
117
99
|
});
|
|
118
|
-
throw new Error(message ?? `Validation failed: ${errorMessages.join("; ")}`);
|
|
119
|
-
}
|
|
120
100
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Test: Back-channel logout endpoint exists
|
|
103
|
+
*/
|
|
104
|
+
async function testBackChannelLogoutEndpoint(config, fetch) {
|
|
105
|
+
return runTest('SSO Back-Channel Logout Endpoint', async () => {
|
|
106
|
+
// Send a POST request without a valid logout token
|
|
107
|
+
// The endpoint should exist and return 400 (bad request) for missing token
|
|
108
|
+
const response = await fetch(config.backChannelLogoutPath, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
112
|
+
},
|
|
113
|
+
body: '',
|
|
114
|
+
});
|
|
115
|
+
// Should return 400 (missing logout_token) or 200 (if session store not configured)
|
|
116
|
+
assert(response.status === 400 || response.status === 200 || response.status === 404, `Expected 400 or 200, got ${response.status}`);
|
|
117
|
+
return {
|
|
118
|
+
details: {
|
|
119
|
+
status: response.status,
|
|
120
|
+
endpointExists: response.status !== 404,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
});
|
|
132
124
|
}
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Test: Callback endpoint returns error without valid code
|
|
127
|
+
*/
|
|
128
|
+
async function testCallbackEndpointInvalid(config, fetch) {
|
|
129
|
+
return runTest('SSO Callback Endpoint (Invalid)', async () => {
|
|
130
|
+
// Send a callback request without proper state/code
|
|
131
|
+
const response = await fetch(`${config.callbackPath}?code=invalid&state=invalid`);
|
|
132
|
+
// Should return 400 or 500 (validation error)
|
|
133
|
+
assert(response.status >= 400, `Expected error status for invalid callback, got ${response.status}`);
|
|
134
|
+
return {
|
|
135
|
+
details: {
|
|
136
|
+
status: response.status,
|
|
137
|
+
handlesInvalidCallback: true,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
});
|
|
135
141
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
143
|
-
async function testLoginEndpoint(config, fetch2) {
|
|
144
|
-
return runTest("SSO Login Endpoint", async () => {
|
|
145
|
-
const response = await fetch2(config.loginPath);
|
|
146
|
-
assert(response.status === 302, `Expected 302 redirect, got ${response.status}`);
|
|
147
|
-
const location = response.headers.get("Location");
|
|
148
|
-
assert(location !== null, "Missing Location header in redirect");
|
|
149
|
-
if (config.expectedAuthorizationUrlPattern) {
|
|
150
|
-
assert(config.expectedAuthorizationUrlPattern.test(location), `Authorization URL does not match expected pattern: ${location}`);
|
|
142
|
+
/**
|
|
143
|
+
* Test: Token endpoint returns 401 when not authenticated
|
|
144
|
+
*/
|
|
145
|
+
async function testTokenEndpointUnauthenticated(config, fetch) {
|
|
146
|
+
if (!config.tokenPath) {
|
|
147
|
+
return skipTest('SSO Token Endpoint (Unauthenticated)', 'Token path not configured');
|
|
151
148
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
assert(hasStateCookie, "Missing state cookie in response");
|
|
163
|
-
return {
|
|
164
|
-
details: {
|
|
165
|
-
authorizationUrl: location,
|
|
166
|
-
pkceEnabled: true,
|
|
167
|
-
stateParameter: url.searchParams.get("state")
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
async function testUserEndpointUnauthenticated(config, fetch2) {
|
|
173
|
-
return runTest("SSO User Endpoint (Unauthenticated)", async () => {
|
|
174
|
-
const response = await fetch2(config.userPath);
|
|
175
|
-
assert(response.status === 401, `Expected 401 Unauthorized, got ${response.status}`);
|
|
176
|
-
return {
|
|
177
|
-
details: {
|
|
178
|
-
status: response.status
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
});
|
|
149
|
+
return runTest('SSO Token Endpoint (Unauthenticated)', async () => {
|
|
150
|
+
const response = await fetch(config.tokenPath);
|
|
151
|
+
// Should return 401 Unauthorized
|
|
152
|
+
assert(response.status === 401 || response.status === 404, `Expected 401 Unauthorized or 404, got ${response.status}`);
|
|
153
|
+
return {
|
|
154
|
+
details: {
|
|
155
|
+
status: response.status,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
});
|
|
182
159
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
for (const [name, value] of cookies.entries()) {
|
|
190
|
-
if (value === "" || value.includes("Max-Age=0")) {
|
|
191
|
-
clearedCookies.push(name);
|
|
192
|
-
}
|
|
160
|
+
/**
|
|
161
|
+
* Test: Refresh endpoint returns 401 when not authenticated
|
|
162
|
+
*/
|
|
163
|
+
async function testRefreshEndpointUnauthenticated(config, fetch) {
|
|
164
|
+
if (!config.refreshPath) {
|
|
165
|
+
return skipTest('SSO Refresh Endpoint (Unauthenticated)', 'Refresh path not configured');
|
|
193
166
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
async function testBackChannelLogoutEndpoint(config, fetch2) {
|
|
206
|
-
return runTest("SSO Back-Channel Logout Endpoint", async () => {
|
|
207
|
-
const response = await fetch2(config.backChannelLogoutPath, {
|
|
208
|
-
method: "POST",
|
|
209
|
-
headers: {
|
|
210
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
211
|
-
},
|
|
212
|
-
body: ""
|
|
167
|
+
return runTest('SSO Refresh Endpoint (Unauthenticated)', async () => {
|
|
168
|
+
const response = await fetch(config.refreshPath);
|
|
169
|
+
// Should return 401 Unauthorized
|
|
170
|
+
assert(response.status === 401 || response.status === 404, `Expected 401 Unauthorized or 404, got ${response.status}`);
|
|
171
|
+
return {
|
|
172
|
+
details: {
|
|
173
|
+
status: response.status,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
213
176
|
});
|
|
214
|
-
assert(response.status === 400 || response.status === 200 || response.status === 404, `Expected 400 or 200, got ${response.status}`);
|
|
215
|
-
return {
|
|
216
|
-
details: {
|
|
217
|
-
status: response.status,
|
|
218
|
-
endpointExists: response.status !== 404
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
async function testCallbackEndpointInvalid(config, fetch2) {
|
|
224
|
-
return runTest("SSO Callback Endpoint (Invalid)", async () => {
|
|
225
|
-
const response = await fetch2(`${config.callbackPath}?code=invalid&state=invalid`);
|
|
226
|
-
assert(response.status >= 400, `Expected error status for invalid callback, got ${response.status}`);
|
|
227
|
-
return {
|
|
228
|
-
details: {
|
|
229
|
-
status: response.status,
|
|
230
|
-
handlesInvalidCallback: true
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
});
|
|
234
177
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
const callbackUrl = authResponse.headers.get("Location");
|
|
279
|
-
if (!callbackUrl) {
|
|
280
|
-
return { cookies: new Map, success: false, error: "No redirect location from auth" };
|
|
178
|
+
/**
|
|
179
|
+
* Helper: Complete a full SSO login flow through the mock server
|
|
180
|
+
*
|
|
181
|
+
* Returns the cookies set after successful authentication
|
|
182
|
+
*/
|
|
183
|
+
async function completeLoginFlow(config, fetch) {
|
|
184
|
+
try {
|
|
185
|
+
// Step 1: Hit the login endpoint to get the authorization redirect
|
|
186
|
+
const loginResponse = await fetch(config.loginPath);
|
|
187
|
+
if (loginResponse.status !== 302) {
|
|
188
|
+
return { cookies: new Map(), success: false, error: `Login did not redirect: ${loginResponse.status}` };
|
|
189
|
+
}
|
|
190
|
+
const authUrl = loginResponse.headers.get('Location');
|
|
191
|
+
if (!authUrl) {
|
|
192
|
+
return { cookies: new Map(), success: false, error: 'No redirect location from login' };
|
|
193
|
+
}
|
|
194
|
+
// Collect cookies from login response (includes state cookie)
|
|
195
|
+
const loginCookies = parseCookies(loginResponse.headers);
|
|
196
|
+
// Step 2: Follow the redirect to the authorization endpoint (mock server auto-authenticates)
|
|
197
|
+
const authResponse = await globalThis.fetch(authUrl, { redirect: 'manual' });
|
|
198
|
+
if (authResponse.status !== 302) {
|
|
199
|
+
return { cookies: new Map(), success: false, error: `Auth did not redirect: ${authResponse.status}` };
|
|
200
|
+
}
|
|
201
|
+
const callbackUrl = authResponse.headers.get('Location');
|
|
202
|
+
if (!callbackUrl) {
|
|
203
|
+
return { cookies: new Map(), success: false, error: 'No redirect location from auth' };
|
|
204
|
+
}
|
|
205
|
+
// Step 3: Complete the callback with the authorization code
|
|
206
|
+
// Extract just the path and query from the callback URL
|
|
207
|
+
const callbackPath = new URL(callbackUrl).pathname + new URL(callbackUrl).search;
|
|
208
|
+
const callbackResponse = await fetch(callbackPath, {
|
|
209
|
+
headers: {
|
|
210
|
+
Cookie: buildCookieHeader(loginCookies),
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
// Should redirect to landing page on success
|
|
214
|
+
if (callbackResponse.status !== 302) {
|
|
215
|
+
return { cookies: new Map(), success: false, error: `Callback did not redirect: ${callbackResponse.status}` };
|
|
216
|
+
}
|
|
217
|
+
// Collect all cookies from the callback response
|
|
218
|
+
const allCookies = parseCookies(callbackResponse.headers);
|
|
219
|
+
return { cookies: allCookies, success: true };
|
|
281
220
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
headers: {
|
|
285
|
-
Cookie: buildCookieHeader(loginCookies)
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
if (callbackResponse.status !== 302) {
|
|
289
|
-
return { cookies: new Map, success: false, error: `Callback did not redirect: ${callbackResponse.status}` };
|
|
221
|
+
catch (error) {
|
|
222
|
+
return { cookies: new Map(), success: false, error: error instanceof Error ? error.message : String(error) };
|
|
290
223
|
}
|
|
291
|
-
const allCookies = parseCookies(callbackResponse.headers);
|
|
292
|
-
return { cookies: allCookies, success: true };
|
|
293
|
-
} catch (error) {
|
|
294
|
-
return { cookies: new Map, success: false, error: error instanceof Error ? error.message : String(error) };
|
|
295
|
-
}
|
|
296
224
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Test: JIT user provisioning works when enabled
|
|
227
|
+
*
|
|
228
|
+
* This test completes a full SSO flow and verifies the user is authenticated.
|
|
229
|
+
* The actual JIT provisioning behavior depends on the server configuration.
|
|
230
|
+
*/
|
|
231
|
+
async function testJitUserProvisioningFlow(config, fetch) {
|
|
232
|
+
return runTest('SSO JIT User Provisioning Flow', async () => {
|
|
233
|
+
// Complete the full login flow
|
|
234
|
+
const { cookies, success, error } = await completeLoginFlow(config, fetch);
|
|
235
|
+
assert(success, error || 'Login flow failed');
|
|
236
|
+
// Verify we got authentication cookies
|
|
237
|
+
const hasAuthCookies = Array.from(cookies.keys()).some((name) => name.includes('.access') || name.includes('.id'));
|
|
238
|
+
assert(hasAuthCookies, 'No authentication cookies set after login');
|
|
239
|
+
// Verify we can now access the user endpoint
|
|
240
|
+
const userResponse = await fetch(config.userPath, {
|
|
241
|
+
headers: {
|
|
242
|
+
Cookie: buildCookieHeader(cookies),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
assert(userResponse.status === 200, `User endpoint returned ${userResponse.status} after login, expected 200`);
|
|
246
|
+
const userData = await userResponse.json();
|
|
247
|
+
assert(userData.id, 'User data missing id field');
|
|
248
|
+
return {
|
|
249
|
+
details: {
|
|
250
|
+
userId: userData.id,
|
|
251
|
+
userName: userData.userName,
|
|
252
|
+
email: userData.email,
|
|
253
|
+
jitFlowCompleted: true,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
307
256
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Runs all SSO validation tests
|
|
260
|
+
*/
|
|
261
|
+
export async function validateSSO(config) {
|
|
262
|
+
const startTime = performance.now();
|
|
263
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
264
|
+
const fetch = createFetcher(mergedConfig);
|
|
265
|
+
const tests = [];
|
|
266
|
+
// Run all tests
|
|
267
|
+
tests.push(await testLoginEndpoint(mergedConfig, fetch));
|
|
268
|
+
tests.push(await testUserEndpointUnauthenticated(mergedConfig, fetch));
|
|
269
|
+
tests.push(await testLogoutEndpoint(mergedConfig, fetch));
|
|
270
|
+
tests.push(await testBackChannelLogoutEndpoint(mergedConfig, fetch));
|
|
271
|
+
tests.push(await testCallbackEndpointInvalid(mergedConfig, fetch));
|
|
272
|
+
tests.push(await testTokenEndpointUnauthenticated(mergedConfig, fetch));
|
|
273
|
+
tests.push(await testRefreshEndpointUnauthenticated(mergedConfig, fetch));
|
|
274
|
+
tests.push(await testJitUserProvisioningFlow(mergedConfig, fetch));
|
|
275
|
+
const duration = performance.now() - startTime;
|
|
276
|
+
const passed = tests.filter((t) => t.passed).length;
|
|
277
|
+
const failed = tests.filter((t) => !t.passed).length;
|
|
278
|
+
const skipped = tests.filter((t) => t.details?.skipped).length;
|
|
311
279
|
return {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
280
|
+
suite: 'SSO',
|
|
281
|
+
passed: failed === 0,
|
|
282
|
+
tests,
|
|
283
|
+
duration,
|
|
284
|
+
summary: {
|
|
285
|
+
total: tests.length,
|
|
286
|
+
passed: passed - skipped,
|
|
287
|
+
failed,
|
|
288
|
+
skipped,
|
|
289
|
+
},
|
|
318
290
|
};
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
async function validateSSO(config) {
|
|
322
|
-
const startTime = performance.now();
|
|
323
|
-
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
324
|
-
const fetch2 = createFetcher(mergedConfig);
|
|
325
|
-
const tests = [];
|
|
326
|
-
tests.push(await testLoginEndpoint(mergedConfig, fetch2));
|
|
327
|
-
tests.push(await testUserEndpointUnauthenticated(mergedConfig, fetch2));
|
|
328
|
-
tests.push(await testLogoutEndpoint(mergedConfig, fetch2));
|
|
329
|
-
tests.push(await testBackChannelLogoutEndpoint(mergedConfig, fetch2));
|
|
330
|
-
tests.push(await testCallbackEndpointInvalid(mergedConfig, fetch2));
|
|
331
|
-
tests.push(await testTokenEndpointUnauthenticated(mergedConfig, fetch2));
|
|
332
|
-
tests.push(await testRefreshEndpointUnauthenticated(mergedConfig, fetch2));
|
|
333
|
-
tests.push(await testJitUserProvisioningFlow(mergedConfig, fetch2));
|
|
334
|
-
const duration = performance.now() - startTime;
|
|
335
|
-
const passed = tests.filter((t) => t.passed).length;
|
|
336
|
-
const failed = tests.filter((t) => !t.passed).length;
|
|
337
|
-
const skipped = tests.filter((t) => t.details?.skipped).length;
|
|
338
|
-
return {
|
|
339
|
-
suite: "SSO",
|
|
340
|
-
passed: failed === 0,
|
|
341
|
-
tests,
|
|
342
|
-
duration,
|
|
343
|
-
summary: {
|
|
344
|
-
total: tests.length,
|
|
345
|
-
passed: passed - skipped,
|
|
346
|
-
failed,
|
|
347
|
-
skipped
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
291
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
{
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Creates Vitest-compatible test suite for SSO validation
|
|
294
|
+
*/
|
|
295
|
+
export function createSSOTests(config) {
|
|
296
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
297
|
+
const fetch = createFetcher(mergedConfig);
|
|
298
|
+
// Core/mandatory SSO tests
|
|
299
|
+
const tests = [
|
|
300
|
+
{
|
|
301
|
+
name: 'login endpoint redirects to authorization URL',
|
|
302
|
+
fn: async () => {
|
|
303
|
+
const result = await testLoginEndpoint(mergedConfig, fetch);
|
|
304
|
+
if (!result.passed)
|
|
305
|
+
throw new Error(result.error);
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'user endpoint returns 401 when unauthenticated',
|
|
310
|
+
fn: async () => {
|
|
311
|
+
const result = await testUserEndpointUnauthenticated(mergedConfig, fetch);
|
|
312
|
+
if (!result.passed)
|
|
313
|
+
throw new Error(result.error);
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'callback endpoint rejects invalid requests',
|
|
318
|
+
fn: async () => {
|
|
319
|
+
const result = await testCallbackEndpointInvalid(mergedConfig, fetch);
|
|
320
|
+
if (!result.passed)
|
|
321
|
+
throw new Error(result.error);
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'token endpoint returns 401 when unauthenticated',
|
|
326
|
+
fn: async () => {
|
|
327
|
+
const result = await testTokenEndpointUnauthenticated(mergedConfig, fetch);
|
|
328
|
+
if (!result.passed)
|
|
329
|
+
throw new Error(result.error);
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: 'refresh endpoint returns 401 when unauthenticated',
|
|
334
|
+
fn: async () => {
|
|
335
|
+
const result = await testRefreshEndpointUnauthenticated(mergedConfig, fetch);
|
|
336
|
+
if (!result.passed)
|
|
337
|
+
throw new Error(result.error);
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
];
|
|
341
|
+
// Extension methods for optional functionality
|
|
342
|
+
const ext = {
|
|
343
|
+
createJITTests: () => [
|
|
344
|
+
{
|
|
345
|
+
name: 'user provisioning flow completes successfully',
|
|
346
|
+
fn: async () => {
|
|
347
|
+
const result = await testJitUserProvisioningFlow(mergedConfig, fetch);
|
|
348
|
+
if (!result.passed)
|
|
349
|
+
throw new Error(result.error);
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
createLogoutTests: () => [
|
|
354
|
+
{
|
|
355
|
+
name: 'logout endpoint clears cookies',
|
|
356
|
+
fn: async () => {
|
|
357
|
+
const result = await testLogoutEndpoint(mergedConfig, fetch);
|
|
358
|
+
if (!result.passed)
|
|
359
|
+
throw new Error(result.error);
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
createBackChannelLogoutTests: () => [
|
|
364
|
+
{
|
|
365
|
+
name: 'endpoint exists',
|
|
366
|
+
fn: async () => {
|
|
367
|
+
const result = await testBackChannelLogoutEndpoint(mergedConfig, fetch);
|
|
368
|
+
if (!result.passed)
|
|
369
|
+
throw new Error(result.error);
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
};
|
|
374
|
+
return { tests, ext };
|
|
429
375
|
}
|
|
430
|
-
|
|
431
|
-
var init_sso = __esm(() => {
|
|
432
|
-
DEFAULT_CONFIG = {
|
|
433
|
-
loginPath: "/api/auth/login",
|
|
434
|
-
callbackPath: "/api/auth/callback",
|
|
435
|
-
userPath: "/api/auth/user",
|
|
436
|
-
logoutPath: "/api/auth/logout",
|
|
437
|
-
backChannelLogoutPath: "/api/auth/logout/backchannel",
|
|
438
|
-
tokenPath: "/api/auth/token",
|
|
439
|
-
refreshPath: "/api/auth/refresh"
|
|
440
|
-
};
|
|
441
|
-
});
|
|
442
|
-
init_sso();
|
|
443
|
-
|
|
444
|
-
export {
|
|
445
|
-
validateSSO,
|
|
446
|
-
createSSOTests
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
//# debugId=65C413DE1A60173A64756E2164756E21
|
|
376
|
+
//# sourceMappingURL=index.js.map
|