@enterprisestandard/esv 0.0.5-beta.20260115.1 → 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.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/workload/index.js
CHANGED
|
@@ -1,503 +1,433 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: () => mod[key],
|
|
15
|
-
enumerable: true
|
|
16
|
-
});
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
20
|
-
var __toCommonJS = (from) => {
|
|
21
|
-
var entry = __moduleCache.get(from), desc;
|
|
22
|
-
if (entry)
|
|
23
|
-
return entry;
|
|
24
|
-
entry = __defProp({}, "__esModule", { value: true });
|
|
25
|
-
if (from && typeof from === "object" || typeof from === "function")
|
|
26
|
-
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
27
|
-
get: () => from[key],
|
|
28
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
29
|
-
}));
|
|
30
|
-
__moduleCache.set(from, entry);
|
|
31
|
-
return entry;
|
|
32
|
-
};
|
|
33
|
-
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
-
var __export = (target, all) => {
|
|
35
|
-
for (var name in all)
|
|
36
|
-
__defProp(target, name, {
|
|
37
|
-
get: all[name],
|
|
38
|
-
enumerable: true,
|
|
39
|
-
configurable: true,
|
|
40
|
-
set: (newValue) => all[name] = () => newValue
|
|
41
|
-
});
|
|
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) {
|
|
1
|
+
/**
|
|
2
|
+
* Workload Validation Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests validate that an application correctly implements
|
|
5
|
+
* Enterprise Standard Workload Identity authentication.
|
|
6
|
+
*/
|
|
7
|
+
import { assert, assertValid, createFetcher, runTest, skipTest } from '../utils';
|
|
8
|
+
/**
|
|
9
|
+
* Gets a workload token response validator (workload tokens don't have id_token, only access_token)
|
|
10
|
+
* This is different from OIDC tokenResponse which includes id_token
|
|
11
|
+
*/
|
|
12
|
+
function getWorkloadTokenResponseValidator() {
|
|
81
13
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
14
|
+
'~standard': {
|
|
15
|
+
validate: (value) => {
|
|
16
|
+
if (typeof value !== 'object' || value === null) {
|
|
17
|
+
return {
|
|
18
|
+
issues: [{ message: `Expected object, got ${typeof value}` }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const token = value;
|
|
22
|
+
const issues = [];
|
|
23
|
+
if (typeof token.access_token !== 'string') {
|
|
24
|
+
issues.push({ message: 'Expected access_token to be string', path: ['access_token'] });
|
|
25
|
+
}
|
|
26
|
+
if (typeof token.token_type !== 'string') {
|
|
27
|
+
issues.push({ message: 'Expected token_type to be string', path: ['token_type'] });
|
|
28
|
+
}
|
|
29
|
+
// expires_in is optional but if present should be a number
|
|
30
|
+
if (token.expires_in !== undefined && typeof token.expires_in !== 'number') {
|
|
31
|
+
issues.push({ message: 'Expected expires_in to be number or undefined', path: ['expires_in'] });
|
|
32
|
+
}
|
|
33
|
+
// scope is optional but if present should be a string
|
|
34
|
+
if (token.scope !== undefined && typeof token.scope !== 'string') {
|
|
35
|
+
issues.push({ message: 'Expected scope to be string or undefined', path: ['scope'] });
|
|
36
|
+
}
|
|
37
|
+
if (issues.length > 0) {
|
|
38
|
+
return { issues };
|
|
39
|
+
}
|
|
40
|
+
return { value };
|
|
41
|
+
},
|
|
42
|
+
},
|
|
86
43
|
};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function skipTest(name, reason) {
|
|
90
|
-
return {
|
|
91
|
-
name,
|
|
92
|
-
passed: true,
|
|
93
|
-
duration: 0,
|
|
94
|
-
details: { skipped: true, reason }
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
function assert(condition, message) {
|
|
98
|
-
if (!condition) {
|
|
99
|
-
throw new Error(message);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
function assertEqual(actual, expected, message) {
|
|
103
|
-
if (actual !== expected) {
|
|
104
|
-
throw new Error(message ?? `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function assertValid(data, validator, message) {
|
|
108
|
-
const result = validator["~standard"].validate(data);
|
|
109
|
-
if (result instanceof Promise) {
|
|
110
|
-
throw new Error(message ?? "Async validators are not supported in assertValid. Use the validator directly with await.");
|
|
111
|
-
}
|
|
112
|
-
if ("issues" in result) {
|
|
113
|
-
const issues = result.issues;
|
|
114
|
-
const errorMessages = issues.map((issue) => {
|
|
115
|
-
const path = issue.path ? issue.path.map(String).join(".") : "";
|
|
116
|
-
return path ? `${path}: ${issue.message}` : issue.message;
|
|
117
|
-
});
|
|
118
|
-
throw new Error(message ?? `Validation failed: ${errorMessages.join("; ")}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function parseCookies(headers) {
|
|
122
|
-
const cookies = new Map;
|
|
123
|
-
const setCookieHeaders = headers.getSetCookie?.() ?? [];
|
|
124
|
-
for (const cookie of setCookieHeaders) {
|
|
125
|
-
const [pair] = cookie.split(";");
|
|
126
|
-
const [name, value] = pair.split("=");
|
|
127
|
-
if (name && value !== undefined) {
|
|
128
|
-
cookies.set(name.trim(), value.trim());
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return cookies;
|
|
132
|
-
}
|
|
133
|
-
function buildCookieHeader(cookies) {
|
|
134
|
-
return Array.from(cookies.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// packages/esv/src/workload/index.ts
|
|
138
|
-
var exports_workload = {};
|
|
139
|
-
__export(exports_workload, {
|
|
140
|
-
validateWorkload: () => validateWorkload,
|
|
141
|
-
createWorkloadTests: () => createWorkloadTests
|
|
142
|
-
});
|
|
143
|
-
function getWorkloadTokenResponseValidator() {
|
|
144
|
-
return {
|
|
145
|
-
"~standard": {
|
|
146
|
-
validate: (value) => {
|
|
147
|
-
if (typeof value !== "object" || value === null) {
|
|
148
|
-
return {
|
|
149
|
-
issues: [{ message: `Expected object, got ${typeof value}` }]
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
const token = value;
|
|
153
|
-
const issues = [];
|
|
154
|
-
if (typeof token.access_token !== "string") {
|
|
155
|
-
issues.push({ message: "Expected access_token to be string", path: ["access_token"] });
|
|
156
|
-
}
|
|
157
|
-
if (typeof token.token_type !== "string") {
|
|
158
|
-
issues.push({ message: "Expected token_type to be string", path: ["token_type"] });
|
|
159
|
-
}
|
|
160
|
-
if (token.expires_in !== undefined && typeof token.expires_in !== "number") {
|
|
161
|
-
issues.push({ message: "Expected expires_in to be number or undefined", path: ["expires_in"] });
|
|
162
|
-
}
|
|
163
|
-
if (token.scope !== undefined && typeof token.scope !== "string") {
|
|
164
|
-
issues.push({ message: "Expected scope to be string or undefined", path: ["scope"] });
|
|
165
|
-
}
|
|
166
|
-
if (issues.length > 0) {
|
|
167
|
-
return { issues };
|
|
168
|
-
}
|
|
169
|
-
return { value };
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Gets a JWKS key validator (simple inline validator for standard JWKS key structure)
|
|
47
|
+
*/
|
|
174
48
|
function getJwksKeyValidator() {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
49
|
+
// JWKS keys have a standard structure - create a simple validator inline
|
|
50
|
+
// This validates the minimal required fields: kty and kid
|
|
51
|
+
return {
|
|
52
|
+
'~standard': {
|
|
53
|
+
validate: (value) => {
|
|
54
|
+
if (typeof value !== 'object' || value === null) {
|
|
55
|
+
return {
|
|
56
|
+
issues: [{ message: `Expected object, got ${typeof value}` }],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const key = value;
|
|
60
|
+
const issues = [];
|
|
61
|
+
if (typeof key.kty !== 'string') {
|
|
62
|
+
issues.push({ message: 'Expected kty to be string', path: ['kty'] });
|
|
63
|
+
}
|
|
64
|
+
if (typeof key.kid !== 'string') {
|
|
65
|
+
issues.push({ message: 'Expected kid to be string', path: ['kid'] });
|
|
66
|
+
}
|
|
67
|
+
if (issues.length > 0) {
|
|
68
|
+
return { issues };
|
|
69
|
+
}
|
|
70
|
+
return { value };
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
198
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Gets a token validation result validator (simple inline validator)
|
|
77
|
+
*/
|
|
199
78
|
function getTokenValidationResultValidator() {
|
|
200
|
-
return {
|
|
201
|
-
"~standard": {
|
|
202
|
-
validate: (value) => {
|
|
203
|
-
if (typeof value !== "object" || value === null) {
|
|
204
|
-
return {
|
|
205
|
-
issues: [{ message: `Expected object, got ${typeof value}` }]
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
const result = value;
|
|
209
|
-
const issues = [];
|
|
210
|
-
if (typeof result.valid !== "boolean") {
|
|
211
|
-
issues.push({ message: "Expected valid to be boolean", path: ["valid"] });
|
|
212
|
-
}
|
|
213
|
-
if (issues.length > 0) {
|
|
214
|
-
return { issues };
|
|
215
|
-
}
|
|
216
|
-
return { value };
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
async function testJwksEndpoint(config, fetch2) {
|
|
222
|
-
return runTest("Workload JWKS Endpoint", async () => {
|
|
223
|
-
const response = await fetch2(config.jwksPath);
|
|
224
|
-
assert(response.status === 200, `Expected 200 OK, got ${response.status}`);
|
|
225
|
-
const data = await response.json();
|
|
226
|
-
assert(Array.isArray(data.keys), "JWKS response missing keys array");
|
|
227
|
-
const keyValidator = getJwksKeyValidator();
|
|
228
|
-
for (const key of data.keys) {
|
|
229
|
-
assertValid(key, keyValidator);
|
|
230
|
-
}
|
|
231
79
|
return {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
80
|
+
'~standard': {
|
|
81
|
+
validate: (value) => {
|
|
82
|
+
if (typeof value !== 'object' || value === null) {
|
|
83
|
+
return {
|
|
84
|
+
issues: [{ message: `Expected object, got ${typeof value}` }],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const result = value;
|
|
88
|
+
const issues = [];
|
|
89
|
+
if (typeof result.valid !== 'boolean') {
|
|
90
|
+
issues.push({ message: 'Expected valid to be boolean', path: ['valid'] });
|
|
91
|
+
}
|
|
92
|
+
if (issues.length > 0) {
|
|
93
|
+
return { issues };
|
|
94
|
+
}
|
|
95
|
+
return { value };
|
|
96
|
+
},
|
|
97
|
+
},
|
|
236
98
|
};
|
|
237
|
-
});
|
|
238
99
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Default configuration for Workload validation
|
|
102
|
+
*/
|
|
103
|
+
const DEFAULT_CONFIG = {
|
|
104
|
+
tokenPath: '/api/workload/token',
|
|
105
|
+
validatePath: '/api/workload/validate',
|
|
106
|
+
jwksPath: '/api/workload/jwks',
|
|
107
|
+
refreshPath: '/api/workload/refresh',
|
|
108
|
+
esvUrl: 'http://localhost:3555',
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Test: JWKS endpoint returns valid JWKS structure
|
|
112
|
+
*/
|
|
113
|
+
async function testJwksEndpoint(config, fetch) {
|
|
114
|
+
return runTest('Workload JWKS Endpoint', async () => {
|
|
115
|
+
const response = await fetch(config.jwksPath);
|
|
116
|
+
// Should return 200 OK
|
|
117
|
+
assert(response.status === 200, `Expected 200 OK, got ${response.status}`);
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
// Should have JWKS structure
|
|
120
|
+
assert(Array.isArray(data.keys), 'JWKS response missing keys array');
|
|
121
|
+
// Each key should have required fields - validate using validator
|
|
122
|
+
const keyValidator = getJwksKeyValidator();
|
|
123
|
+
for (const key of data.keys) {
|
|
124
|
+
assertValid(key, keyValidator);
|
|
248
125
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
details: {
|
|
257
|
-
tokenType: data.token_type,
|
|
258
|
-
hasToken: !!data.access_token,
|
|
259
|
-
token: data.access_token
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
});
|
|
263
|
-
return {
|
|
264
|
-
...result,
|
|
265
|
-
token: result.details?.token
|
|
266
|
-
};
|
|
126
|
+
return {
|
|
127
|
+
details: {
|
|
128
|
+
keyCount: data.keys.length,
|
|
129
|
+
algorithms: data.keys.map((k) => k.alg).filter(Boolean),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
});
|
|
267
133
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Test: Token endpoint returns access token
|
|
136
|
+
*/
|
|
137
|
+
async function testTokenEndpoint(config, fetch) {
|
|
138
|
+
const result = await runTest('Workload Token Endpoint', async () => {
|
|
139
|
+
const url = config.testScopes
|
|
140
|
+
? `${config.tokenPath}?scope=${encodeURIComponent(config.testScopes)}`
|
|
141
|
+
: config.tokenPath;
|
|
142
|
+
const response = await fetch(url);
|
|
143
|
+
// Should return 200 OK (or 503 if workload not configured)
|
|
144
|
+
if (response.status === 503) {
|
|
145
|
+
return {
|
|
146
|
+
details: {
|
|
147
|
+
skipped: true,
|
|
148
|
+
reason: 'Workload authentication not configured',
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
assert(response.status === 200, `Expected 200 OK, got ${response.status}`);
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
// Should have token response structure - validate using workload-specific validator
|
|
155
|
+
// (workload tokens don't have id_token, only access_token)
|
|
156
|
+
const validator = getWorkloadTokenResponseValidator();
|
|
157
|
+
assertValid(data, validator);
|
|
158
|
+
return {
|
|
159
|
+
details: {
|
|
160
|
+
tokenType: data.token_type,
|
|
161
|
+
hasToken: !!data.access_token,
|
|
162
|
+
token: data.access_token,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
275
165
|
});
|
|
276
|
-
assert(response.status === 200, `Expected 200 OK for valid token, got ${response.status}`);
|
|
277
|
-
const data = await response.json();
|
|
278
|
-
const validationResultValidator = getTokenValidationResultValidator();
|
|
279
|
-
assertValid(data, validationResultValidator);
|
|
280
|
-
assert(data.valid === true, "Token should be valid");
|
|
281
166
|
return {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
claims: data.claims
|
|
285
|
-
}
|
|
167
|
+
...result,
|
|
168
|
+
token: result.details?.token,
|
|
286
169
|
};
|
|
287
|
-
});
|
|
288
170
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Test: Token validation endpoint works
|
|
173
|
+
*/
|
|
174
|
+
async function testValidateEndpoint(config, fetch, token) {
|
|
175
|
+
return runTest('Workload Validate Endpoint', async () => {
|
|
176
|
+
const response = await fetch(config.validatePath, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: {
|
|
179
|
+
Authorization: `Bearer ${token}`,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
// Should return 200 OK for valid token
|
|
183
|
+
assert(response.status === 200, `Expected 200 OK for valid token, got ${response.status}`);
|
|
184
|
+
const data = await response.json();
|
|
185
|
+
// Should have validation result structure - validate using validator
|
|
186
|
+
const validationResultValidator = getTokenValidationResultValidator();
|
|
187
|
+
assertValid(data, validationResultValidator);
|
|
188
|
+
assert(data.valid === true, 'Token should be valid');
|
|
189
|
+
return {
|
|
190
|
+
details: {
|
|
191
|
+
valid: data.valid,
|
|
192
|
+
claims: data.claims,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
296
195
|
});
|
|
297
|
-
assert(response.status === 401, `Expected 401 Unauthorized for invalid token, got ${response.status}`);
|
|
298
|
-
const data = await response.json();
|
|
299
|
-
const validationResultValidator = getTokenValidationResultValidator();
|
|
300
|
-
assertValid(data, validationResultValidator);
|
|
301
|
-
assert(data.valid === false, "Invalid token should not validate");
|
|
302
|
-
return {
|
|
303
|
-
details: {
|
|
304
|
-
valid: data.valid,
|
|
305
|
-
error: data.error
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
});
|
|
309
196
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Test: Token validation rejects invalid tokens
|
|
199
|
+
*/
|
|
200
|
+
async function testValidateEndpointInvalid(config, fetch) {
|
|
201
|
+
return runTest('Workload Validate Endpoint (Invalid Token)', async () => {
|
|
202
|
+
const response = await fetch(config.validatePath, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: {
|
|
205
|
+
Authorization: 'Bearer invalid.token.here',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
// Should return 401 for invalid token
|
|
209
|
+
assert(response.status === 401, `Expected 401 Unauthorized for invalid token, got ${response.status}`);
|
|
210
|
+
const data = await response.json();
|
|
211
|
+
// Should indicate token is invalid - validate using validator
|
|
212
|
+
const validationResultValidator = getTokenValidationResultValidator();
|
|
213
|
+
assertValid(data, validationResultValidator);
|
|
214
|
+
assert(data.valid === false, 'Invalid token should not validate');
|
|
215
|
+
return {
|
|
216
|
+
details: {
|
|
217
|
+
valid: data.valid,
|
|
218
|
+
error: data.error,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
314
221
|
});
|
|
315
|
-
assert(response.status === 401, `Expected 401 Unauthorized for missing auth, got ${response.status}`);
|
|
316
|
-
return {
|
|
317
|
-
details: {
|
|
318
|
-
status: response.status,
|
|
319
|
-
requiresAuth: true
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
});
|
|
323
222
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Test: Token validation rejects missing authorization header
|
|
225
|
+
*/
|
|
226
|
+
async function testValidateEndpointNoAuth(config, fetch) {
|
|
227
|
+
return runTest('Workload Validate Endpoint (No Auth)', async () => {
|
|
228
|
+
const response = await fetch(config.validatePath, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
});
|
|
231
|
+
// Should return 401 for missing auth
|
|
232
|
+
assert(response.status === 401, `Expected 401 Unauthorized for missing auth, got ${response.status}`);
|
|
233
|
+
return {
|
|
234
|
+
details: {
|
|
235
|
+
status: response.status,
|
|
236
|
+
requiresAuth: true,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
328
239
|
});
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Test: Refresh endpoint works
|
|
243
|
+
*/
|
|
244
|
+
async function testRefreshEndpoint(config, fetch) {
|
|
245
|
+
return runTest('Workload Refresh Endpoint', async () => {
|
|
246
|
+
const response = await fetch(config.refreshPath, {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
});
|
|
249
|
+
// Should return 200 OK or 503 if not configured
|
|
250
|
+
if (response.status === 503) {
|
|
251
|
+
return {
|
|
252
|
+
details: {
|
|
253
|
+
skipped: true,
|
|
254
|
+
reason: 'Workload authentication not configured',
|
|
255
|
+
},
|
|
256
|
+
};
|
|
334
257
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
258
|
+
assert(response.status === 200, `Expected 200 OK, got ${response.status}`);
|
|
259
|
+
const data = await response.json();
|
|
260
|
+
// Should have token response structure - validate using workload-specific validator
|
|
261
|
+
// (workload tokens don't have id_token, only access_token)
|
|
262
|
+
const validator = getWorkloadTokenResponseValidator();
|
|
263
|
+
assertValid(data, validator);
|
|
264
|
+
return {
|
|
265
|
+
details: {
|
|
266
|
+
tokenType: data.token_type,
|
|
267
|
+
hasToken: !!data.access_token,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
});
|
|
348
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Test: Whoami endpoint with workload authentication
|
|
274
|
+
*
|
|
275
|
+
* This test calls the ESV mock server's /api/whoami endpoint to validate
|
|
276
|
+
* that the workload token issued by the application is valid and can be
|
|
277
|
+
* used for cross-service authentication. This allows each application to
|
|
278
|
+
* be tested independently without requiring other applications to be running.
|
|
279
|
+
*/
|
|
349
280
|
async function testWhoamiWithWorkload(config, token) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
281
|
+
return runTest('Workload Authentication (Whoami)', async () => {
|
|
282
|
+
// Call the ESV mock server's whoami endpoint with the workload token
|
|
283
|
+
const esvWhoamiUrl = `${config.esvUrl}/api/whoami`;
|
|
284
|
+
const response = await globalThis.fetch(esvWhoamiUrl, {
|
|
285
|
+
headers: {
|
|
286
|
+
Authorization: `Bearer ${token}`,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
// Should return 200 OK
|
|
290
|
+
if (response.status === 404) {
|
|
291
|
+
return {
|
|
292
|
+
details: {
|
|
293
|
+
skipped: true,
|
|
294
|
+
reason: 'ESV whoami endpoint not available',
|
|
295
|
+
},
|
|
296
|
+
};
|
|
362
297
|
}
|
|
363
|
-
|
|
298
|
+
assert(response.status === 200, `Expected 200 OK, got ${response.status}`);
|
|
299
|
+
const data = await response.json();
|
|
300
|
+
// Should have workload identity in response
|
|
301
|
+
assert(data.workload !== undefined, 'Response should include workload identity');
|
|
302
|
+
return {
|
|
303
|
+
details: {
|
|
304
|
+
workloadId: data.workload?.workload_id,
|
|
305
|
+
clientId: data.workload?.client_id,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Runs all Workload validation tests
|
|
312
|
+
*/
|
|
313
|
+
export async function validateWorkload(config) {
|
|
314
|
+
const startTime = performance.now();
|
|
315
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
316
|
+
const fetch = createFetcher(mergedConfig);
|
|
317
|
+
const tests = [];
|
|
318
|
+
// Test JWKS endpoint
|
|
319
|
+
tests.push(await testJwksEndpoint(mergedConfig, fetch));
|
|
320
|
+
// Test token endpoint and get a token for further tests
|
|
321
|
+
const tokenResult = await testTokenEndpoint(mergedConfig, fetch);
|
|
322
|
+
tests.push(tokenResult);
|
|
323
|
+
// Test validation endpoints
|
|
324
|
+
tests.push(await testValidateEndpointNoAuth(mergedConfig, fetch));
|
|
325
|
+
tests.push(await testValidateEndpointInvalid(mergedConfig, fetch));
|
|
326
|
+
// If we have a token, test validation with valid token
|
|
327
|
+
const token = tokenResult.token ?? config.validToken;
|
|
328
|
+
if (token) {
|
|
329
|
+
tests.push(await testValidateEndpoint(mergedConfig, fetch, token));
|
|
330
|
+
tests.push(await testWhoamiWithWorkload(mergedConfig, token));
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
tests.push(skipTest('Workload Validate Endpoint', 'No valid token available'));
|
|
334
|
+
tests.push(skipTest('Workload Authentication (Whoami)', 'No valid token available'));
|
|
364
335
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
336
|
+
// Test refresh endpoint
|
|
337
|
+
tests.push(await testRefreshEndpoint(mergedConfig, fetch));
|
|
338
|
+
const duration = performance.now() - startTime;
|
|
339
|
+
const passed = tests.filter((t) => t.passed).length;
|
|
340
|
+
const failed = tests.filter((t) => !t.passed).length;
|
|
341
|
+
const skipped = tests.filter((t) => t.details?.skipped).length;
|
|
368
342
|
return {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
343
|
+
suite: 'Workload',
|
|
344
|
+
passed: failed === 0,
|
|
345
|
+
tests,
|
|
346
|
+
duration,
|
|
347
|
+
summary: {
|
|
348
|
+
total: tests.length,
|
|
349
|
+
passed: passed - skipped,
|
|
350
|
+
failed,
|
|
351
|
+
skipped,
|
|
352
|
+
},
|
|
373
353
|
};
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
async function validateWorkload(config) {
|
|
377
|
-
const startTime = performance.now();
|
|
378
|
-
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
379
|
-
const fetch2 = createFetcher(mergedConfig);
|
|
380
|
-
const tests = [];
|
|
381
|
-
tests.push(await testJwksEndpoint(mergedConfig, fetch2));
|
|
382
|
-
const tokenResult = await testTokenEndpoint(mergedConfig, fetch2);
|
|
383
|
-
tests.push(tokenResult);
|
|
384
|
-
tests.push(await testValidateEndpointNoAuth(mergedConfig, fetch2));
|
|
385
|
-
tests.push(await testValidateEndpointInvalid(mergedConfig, fetch2));
|
|
386
|
-
const token = tokenResult.token ?? config.validToken;
|
|
387
|
-
if (token) {
|
|
388
|
-
tests.push(await testValidateEndpoint(mergedConfig, fetch2, token));
|
|
389
|
-
tests.push(await testWhoamiWithWorkload(mergedConfig, token));
|
|
390
|
-
} else {
|
|
391
|
-
tests.push(skipTest("Workload Validate Endpoint", "No valid token available"));
|
|
392
|
-
tests.push(skipTest("Workload Authentication (Whoami)", "No valid token available"));
|
|
393
|
-
}
|
|
394
|
-
tests.push(await testRefreshEndpoint(mergedConfig, fetch2));
|
|
395
|
-
const duration = performance.now() - startTime;
|
|
396
|
-
const passed = tests.filter((t) => t.passed).length;
|
|
397
|
-
const failed = tests.filter((t) => !t.passed).length;
|
|
398
|
-
const skipped = tests.filter((t) => t.details?.skipped).length;
|
|
399
|
-
return {
|
|
400
|
-
suite: "Workload",
|
|
401
|
-
passed: failed === 0,
|
|
402
|
-
tests,
|
|
403
|
-
duration,
|
|
404
|
-
summary: {
|
|
405
|
-
total: tests.length,
|
|
406
|
-
passed: passed - skipped,
|
|
407
|
-
failed,
|
|
408
|
-
skipped
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
354
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Creates Vitest-compatible test suite for Workload validation
|
|
357
|
+
*/
|
|
358
|
+
export function createWorkloadTests(config) {
|
|
359
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
360
|
+
const fetch = createFetcher(mergedConfig);
|
|
361
|
+
// Store token for dependent tests
|
|
362
|
+
let acquiredToken;
|
|
363
|
+
return [
|
|
364
|
+
{
|
|
365
|
+
name: 'JWKS endpoint returns valid keys',
|
|
366
|
+
fn: async () => {
|
|
367
|
+
const result = await testJwksEndpoint(mergedConfig, fetch);
|
|
368
|
+
if (!result.passed)
|
|
369
|
+
throw new Error(result.error);
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'token endpoint returns access token',
|
|
374
|
+
fn: async () => {
|
|
375
|
+
const result = await testTokenEndpoint(mergedConfig, fetch);
|
|
376
|
+
if (!result.passed && !result.details?.skipped)
|
|
377
|
+
throw new Error(result.error);
|
|
378
|
+
acquiredToken = result.token;
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'validate endpoint rejects missing auth',
|
|
383
|
+
fn: async () => {
|
|
384
|
+
const result = await testValidateEndpointNoAuth(mergedConfig, fetch);
|
|
385
|
+
if (!result.passed)
|
|
386
|
+
throw new Error(result.error);
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: 'validate endpoint rejects invalid tokens',
|
|
391
|
+
fn: async () => {
|
|
392
|
+
const result = await testValidateEndpointInvalid(mergedConfig, fetch);
|
|
393
|
+
if (!result.passed)
|
|
394
|
+
throw new Error(result.error);
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: 'validate endpoint accepts valid token',
|
|
399
|
+
fn: async () => {
|
|
400
|
+
const token = acquiredToken ?? config.validToken;
|
|
401
|
+
if (!token) {
|
|
402
|
+
console.warn('Skipping: No valid token available');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const result = await testValidateEndpoint(mergedConfig, fetch, token);
|
|
406
|
+
if (!result.passed)
|
|
407
|
+
throw new Error(result.error);
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: 'whoami endpoint works with workload authentication',
|
|
412
|
+
fn: async () => {
|
|
413
|
+
const token = acquiredToken ?? config.validToken;
|
|
414
|
+
if (!token) {
|
|
415
|
+
console.warn('Skipping: No valid token available');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const result = await testWhoamiWithWorkload(mergedConfig, token);
|
|
419
|
+
if (!result.passed && !result.details?.skipped)
|
|
420
|
+
throw new Error(result.error);
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: 'refresh endpoint works',
|
|
425
|
+
fn: async () => {
|
|
426
|
+
const result = await testRefreshEndpoint(mergedConfig, fetch);
|
|
427
|
+
if (!result.passed && !result.details?.skipped)
|
|
428
|
+
throw new Error(result.error);
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
];
|
|
485
432
|
}
|
|
486
|
-
|
|
487
|
-
var init_workload = __esm(() => {
|
|
488
|
-
DEFAULT_CONFIG = {
|
|
489
|
-
tokenPath: "/api/workload/token",
|
|
490
|
-
validatePath: "/api/workload/validate",
|
|
491
|
-
jwksPath: "/api/workload/jwks",
|
|
492
|
-
refreshPath: "/api/workload/refresh",
|
|
493
|
-
esvUrl: "http://localhost:3555"
|
|
494
|
-
};
|
|
495
|
-
});
|
|
496
|
-
init_workload();
|
|
497
|
-
|
|
498
|
-
export {
|
|
499
|
-
validateWorkload,
|
|
500
|
-
createWorkloadTests
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
//# debugId=A73B69B4545ADC4664756E2164756E21
|
|
433
|
+
//# sourceMappingURL=index.js.map
|