@enterprisestandard/react 0.0.5-beta.20251125.1 → 0.0.5-beta.20260114.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/group-store.d.ts +164 -0
- package/dist/group-store.d.ts.map +1 -0
- package/dist/iam.d.ts +205 -5
- package/dist/iam.d.ts.map +1 -1
- package/dist/index.d.ts +41 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2838 -555
- package/dist/index.js.map +17 -9
- package/dist/server.d.ts +6 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/session-store.d.ts +3 -3
- package/dist/session-store.d.ts.map +1 -1
- package/dist/sso.d.ts +58 -10
- package/dist/sso.d.ts.map +1 -1
- package/dist/tenant-server.d.ts +8 -0
- package/dist/tenant-server.d.ts.map +1 -0
- package/dist/tenant.d.ts +280 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/types/base-user.d.ts +27 -0
- package/dist/types/base-user.d.ts.map +1 -0
- package/dist/types/enterprise-user.d.ts +158 -0
- package/dist/types/enterprise-user.d.ts.map +1 -0
- package/dist/types/oidc-schema.d.ts.map +1 -0
- package/dist/{scim-schema.d.ts → types/scim-schema.d.ts} +66 -3
- package/dist/types/scim-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/user.d.ts +41 -0
- package/dist/types/user.d.ts.map +1 -0
- package/dist/types/workload-schema.d.ts +106 -0
- package/dist/types/workload-schema.d.ts.map +1 -0
- package/dist/ui/sso-provider.d.ts +3 -3
- package/dist/ui/sso-provider.d.ts.map +1 -1
- package/dist/user-store.d.ts +161 -0
- package/dist/user-store.d.ts.map +1 -0
- package/dist/workload-server.d.ts +126 -0
- package/dist/workload-server.d.ts.map +1 -0
- package/dist/workload-token-store.d.ts +187 -0
- package/dist/workload-token-store.d.ts.map +1 -0
- package/dist/workload.d.ts +227 -0
- package/dist/workload.d.ts.map +1 -0
- package/package.json +1 -2
- package/dist/enterprise-user.d.ts +0 -126
- package/dist/enterprise-user.d.ts.map +0 -1
- package/dist/oidc-schema.d.ts.map +0 -1
- package/dist/scim-schema.d.ts.map +0 -1
- package/dist/standard-schema.d.ts.map +0 -1
- /package/dist/{oidc-schema.d.ts → types/oidc-schema.d.ts} +0 -0
- /package/dist/{standard-schema.d.ts → types/standard-schema.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,288 @@
|
|
|
1
|
-
// packages/react/src/
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// packages/react/src/types/scim-schema.ts
|
|
2
|
+
function validateString(value, fieldName, required, issues, path) {
|
|
3
|
+
if (value === undefined || value === null) {
|
|
4
|
+
if (required) {
|
|
5
|
+
issues.push({
|
|
6
|
+
message: `${fieldName} is required`,
|
|
7
|
+
path
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (typeof value !== "string") {
|
|
13
|
+
issues.push({
|
|
14
|
+
message: `${fieldName} must be a string`,
|
|
15
|
+
path
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
4
20
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
21
|
+
function validateBoolean(value, fieldName, issues, path) {
|
|
22
|
+
if (value === undefined || value === null) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "boolean") {
|
|
26
|
+
issues.push({
|
|
27
|
+
message: `${fieldName} must be a boolean`,
|
|
28
|
+
path
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function validateName(value, issues, basePath) {
|
|
35
|
+
if (value === undefined || value === null) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== "object" || value === null) {
|
|
39
|
+
issues.push({
|
|
40
|
+
message: "name must be an object",
|
|
41
|
+
path: basePath
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const name = value;
|
|
46
|
+
const result = {};
|
|
47
|
+
result.formatted = validateString(name.formatted, "formatted", false, issues, [...basePath, "formatted"]);
|
|
48
|
+
result.familyName = validateString(name.familyName, "familyName", false, issues, [...basePath, "familyName"]);
|
|
49
|
+
result.givenName = validateString(name.givenName, "givenName", false, issues, [...basePath, "givenName"]);
|
|
50
|
+
result.middleName = validateString(name.middleName, "middleName", false, issues, [...basePath, "middleName"]);
|
|
51
|
+
result.honorificPrefix = validateString(name.honorificPrefix, "honorificPrefix", false, issues, [
|
|
52
|
+
...basePath,
|
|
53
|
+
"honorificPrefix"
|
|
54
|
+
]);
|
|
55
|
+
result.honorificSuffix = validateString(name.honorificSuffix, "honorificSuffix", false, issues, [
|
|
56
|
+
...basePath,
|
|
57
|
+
"honorificSuffix"
|
|
58
|
+
]);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function validateEmails(value, issues, basePath) {
|
|
62
|
+
if (value === undefined || value === null) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(value)) {
|
|
66
|
+
issues.push({
|
|
67
|
+
message: "emails must be an array",
|
|
68
|
+
path: basePath
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const emails = [];
|
|
73
|
+
for (let i = 0;i < value.length; i++) {
|
|
74
|
+
const email = value[i];
|
|
75
|
+
const emailPath = [...basePath, i];
|
|
76
|
+
if (typeof email !== "object" || email === null) {
|
|
77
|
+
issues.push({
|
|
78
|
+
message: "email must be an object",
|
|
79
|
+
path: emailPath
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const emailObj = email;
|
|
84
|
+
const emailValue = validateString(emailObj.value, "value", true, issues, [...emailPath, "value"]);
|
|
85
|
+
if (emailValue) {
|
|
86
|
+
emails.push({
|
|
87
|
+
value: emailValue,
|
|
88
|
+
display: validateString(emailObj.display, "display", false, issues, [...emailPath, "display"]),
|
|
89
|
+
type: validateString(emailObj.type, "type", false, issues, [...emailPath, "type"]),
|
|
90
|
+
primary: validateBoolean(emailObj.primary, "primary", issues, [...emailPath, "primary"])
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return emails.length > 0 ? emails : undefined;
|
|
95
|
+
}
|
|
96
|
+
function validatePhoneNumbers(value, issues, basePath) {
|
|
97
|
+
if (value === undefined || value === null) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(value)) {
|
|
101
|
+
issues.push({
|
|
102
|
+
message: "phoneNumbers must be an array",
|
|
103
|
+
path: basePath
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const phoneNumbers = [];
|
|
108
|
+
for (let i = 0;i < value.length; i++) {
|
|
109
|
+
const phone = value[i];
|
|
110
|
+
const phonePath = [...basePath, i];
|
|
111
|
+
if (typeof phone !== "object" || phone === null) {
|
|
112
|
+
issues.push({
|
|
113
|
+
message: "phoneNumber must be an object",
|
|
114
|
+
path: phonePath
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const phoneObj = phone;
|
|
119
|
+
const phoneValue = validateString(phoneObj.value, "value", true, issues, [...phonePath, "value"]);
|
|
120
|
+
if (phoneValue) {
|
|
121
|
+
phoneNumbers.push({
|
|
122
|
+
value: phoneValue,
|
|
123
|
+
display: validateString(phoneObj.display, "display", false, issues, [...phonePath, "display"]),
|
|
124
|
+
type: validateString(phoneObj.type, "type", false, issues, [...phonePath, "type"]),
|
|
125
|
+
primary: validateBoolean(phoneObj.primary, "primary", issues, [...phonePath, "primary"])
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return phoneNumbers.length > 0 ? phoneNumbers : undefined;
|
|
130
|
+
}
|
|
131
|
+
function validateAddresses(value, issues, basePath) {
|
|
132
|
+
if (value === undefined || value === null) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!Array.isArray(value)) {
|
|
136
|
+
issues.push({
|
|
137
|
+
message: "addresses must be an array",
|
|
138
|
+
path: basePath
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const addresses = [];
|
|
143
|
+
for (let i = 0;i < value.length; i++) {
|
|
144
|
+
const address = value[i];
|
|
145
|
+
const addressPath = [...basePath, i];
|
|
146
|
+
if (typeof address !== "object" || address === null) {
|
|
147
|
+
issues.push({
|
|
148
|
+
message: "address must be an object",
|
|
149
|
+
path: addressPath
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const addressObj = address;
|
|
154
|
+
addresses.push({
|
|
155
|
+
formatted: validateString(addressObj.formatted, "formatted", false, issues, [...addressPath, "formatted"]),
|
|
156
|
+
streetAddress: validateString(addressObj.streetAddress, "streetAddress", false, issues, [
|
|
157
|
+
...addressPath,
|
|
158
|
+
"streetAddress"
|
|
159
|
+
]),
|
|
160
|
+
locality: validateString(addressObj.locality, "locality", false, issues, [...addressPath, "locality"]),
|
|
161
|
+
region: validateString(addressObj.region, "region", false, issues, [...addressPath, "region"]),
|
|
162
|
+
postalCode: validateString(addressObj.postalCode, "postalCode", false, issues, [...addressPath, "postalCode"]),
|
|
163
|
+
country: validateString(addressObj.country, "country", false, issues, [...addressPath, "country"]),
|
|
164
|
+
type: validateString(addressObj.type, "type", false, issues, [...addressPath, "type"]),
|
|
165
|
+
primary: validateBoolean(addressObj.primary, "primary", issues, [...addressPath, "primary"])
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return addresses.length > 0 ? addresses : undefined;
|
|
169
|
+
}
|
|
170
|
+
function validateGroups(value, issues, basePath) {
|
|
171
|
+
if (value === undefined || value === null) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!Array.isArray(value)) {
|
|
175
|
+
issues.push({
|
|
176
|
+
message: "groups must be an array",
|
|
177
|
+
path: basePath
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const groups = [];
|
|
182
|
+
for (let i = 0;i < value.length; i++) {
|
|
183
|
+
const group = value[i];
|
|
184
|
+
const groupPath = [...basePath, i];
|
|
185
|
+
if (typeof group !== "object" || group === null) {
|
|
186
|
+
issues.push({
|
|
187
|
+
message: "group must be an object",
|
|
188
|
+
path: groupPath
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const groupObj = group;
|
|
193
|
+
const groupValue = validateString(groupObj.value, "value", true, issues, [...groupPath, "value"]);
|
|
194
|
+
if (groupValue) {
|
|
195
|
+
groups.push({
|
|
196
|
+
value: groupValue,
|
|
197
|
+
$ref: validateString(groupObj.$ref, "$ref", false, issues, [...groupPath, "$ref"]),
|
|
198
|
+
display: validateString(groupObj.display, "display", false, issues, [...groupPath, "display"]),
|
|
199
|
+
type: validateString(groupObj.type, "type", false, issues, [...groupPath, "type"])
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return groups.length > 0 ? groups : undefined;
|
|
204
|
+
}
|
|
205
|
+
function validateRoles(value, issues, basePath) {
|
|
206
|
+
if (value === undefined || value === null) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!Array.isArray(value)) {
|
|
210
|
+
issues.push({
|
|
211
|
+
message: "roles must be an array",
|
|
212
|
+
path: basePath
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const roles = [];
|
|
217
|
+
for (let i = 0;i < value.length; i++) {
|
|
218
|
+
const role = value[i];
|
|
219
|
+
const rolePath = [...basePath, i];
|
|
220
|
+
if (typeof role !== "object" || role === null) {
|
|
221
|
+
issues.push({
|
|
222
|
+
message: "role must be an object",
|
|
223
|
+
path: rolePath
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const roleObj = role;
|
|
228
|
+
const roleValue = validateString(roleObj.value, "value", true, issues, [...rolePath, "value"]);
|
|
229
|
+
if (roleValue) {
|
|
230
|
+
roles.push({
|
|
231
|
+
value: roleValue,
|
|
232
|
+
display: validateString(roleObj.display, "display", false, issues, [...rolePath, "display"]),
|
|
233
|
+
type: validateString(roleObj.type, "type", false, issues, [...rolePath, "type"]),
|
|
234
|
+
primary: validateBoolean(roleObj.primary, "primary", issues, [...rolePath, "primary"])
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return roles.length > 0 ? roles : undefined;
|
|
239
|
+
}
|
|
240
|
+
function validateEnterpriseUser(value, issues, basePath) {
|
|
241
|
+
if (value === undefined || value === null) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (typeof value !== "object" || value === null) {
|
|
245
|
+
issues.push({
|
|
246
|
+
message: "Enterprise User extension must be an object",
|
|
247
|
+
path: basePath
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const enterprise = value;
|
|
252
|
+
const result = {};
|
|
253
|
+
result.employeeNumber = validateString(enterprise.employeeNumber, "employeeNumber", false, issues, [
|
|
254
|
+
...basePath,
|
|
255
|
+
"employeeNumber"
|
|
256
|
+
]);
|
|
257
|
+
result.costCenter = validateString(enterprise.costCenter, "costCenter", false, issues, [...basePath, "costCenter"]);
|
|
258
|
+
result.organization = validateString(enterprise.organization, "organization", false, issues, [
|
|
259
|
+
...basePath,
|
|
260
|
+
"organization"
|
|
261
|
+
]);
|
|
262
|
+
result.division = validateString(enterprise.division, "division", false, issues, [...basePath, "division"]);
|
|
263
|
+
result.department = validateString(enterprise.department, "department", false, issues, [...basePath, "department"]);
|
|
264
|
+
if (enterprise.manager !== undefined && enterprise.manager !== null) {
|
|
265
|
+
if (typeof enterprise.manager !== "object" || enterprise.manager === null) {
|
|
266
|
+
issues.push({
|
|
267
|
+
message: "manager must be an object",
|
|
268
|
+
path: [...basePath, "manager"]
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
const manager = enterprise.manager;
|
|
272
|
+
result.manager = {
|
|
273
|
+
value: validateString(manager.value, "value", false, issues, [...basePath, "manager", "value"]),
|
|
274
|
+
$ref: validateString(manager.$ref, "$ref", false, issues, [...basePath, "manager", "$ref"]),
|
|
275
|
+
displayName: validateString(manager.displayName, "displayName", false, issues, [
|
|
276
|
+
...basePath,
|
|
277
|
+
"manager",
|
|
278
|
+
"displayName"
|
|
279
|
+
])
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
function userSchema(vendor) {
|
|
8
286
|
return {
|
|
9
287
|
"~standard": {
|
|
10
288
|
version: 1,
|
|
@@ -19,81 +297,62 @@ function oidcCallbackSchema(vendor) {
|
|
|
19
297
|
]
|
|
20
298
|
};
|
|
21
299
|
}
|
|
22
|
-
const
|
|
300
|
+
const user = value;
|
|
23
301
|
const issues = [];
|
|
24
302
|
const result = {};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} else {
|
|
29
|
-
issues.push({
|
|
30
|
-
message: "code must be a string",
|
|
31
|
-
path: ["code"]
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
} else if (!("error" in params)) {
|
|
35
|
-
issues.push({
|
|
36
|
-
message: "code is required",
|
|
37
|
-
path: ["code"]
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
if ("state" in params) {
|
|
41
|
-
if (typeof params.state === "string" || params.state === undefined) {
|
|
42
|
-
result.state = params.state;
|
|
43
|
-
} else {
|
|
44
|
-
issues.push({
|
|
45
|
-
message: "state must be a string",
|
|
46
|
-
path: ["state"]
|
|
47
|
-
});
|
|
48
|
-
}
|
|
303
|
+
const userName = validateString(user.userName, "userName", true, issues, ["userName"]);
|
|
304
|
+
if (!userName) {
|
|
305
|
+
return { issues };
|
|
49
306
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
307
|
+
result.userName = userName;
|
|
308
|
+
result.id = validateString(user.id, "id", false, issues, ["id"]);
|
|
309
|
+
result.externalId = validateString(user.externalId, "externalId", false, issues, ["externalId"]);
|
|
310
|
+
result.displayName = validateString(user.displayName, "displayName", false, issues, ["displayName"]);
|
|
311
|
+
result.nickName = validateString(user.nickName, "nickName", false, issues, ["nickName"]);
|
|
312
|
+
result.profileUrl = validateString(user.profileUrl, "profileUrl", false, issues, ["profileUrl"]);
|
|
313
|
+
result.title = validateString(user.title, "title", false, issues, ["title"]);
|
|
314
|
+
result.userType = validateString(user.userType, "userType", false, issues, ["userType"]);
|
|
315
|
+
result.preferredLanguage = validateString(user.preferredLanguage, "preferredLanguage", false, issues, [
|
|
316
|
+
"preferredLanguage"
|
|
317
|
+
]);
|
|
318
|
+
result.locale = validateString(user.locale, "locale", false, issues, ["locale"]);
|
|
319
|
+
result.timezone = validateString(user.timezone, "timezone", false, issues, ["timezone"]);
|
|
320
|
+
result.password = validateString(user.password, "password", false, issues, ["password"]);
|
|
321
|
+
result.active = validateBoolean(user.active, "active", issues, ["active"]);
|
|
322
|
+
result.name = validateName(user.name, issues, ["name"]);
|
|
323
|
+
result.emails = validateEmails(user.emails, issues, ["emails"]);
|
|
324
|
+
result.phoneNumbers = validatePhoneNumbers(user.phoneNumbers, issues, ["phoneNumbers"]);
|
|
325
|
+
result.addresses = validateAddresses(user.addresses, issues, ["addresses"]);
|
|
326
|
+
result.groups = validateGroups(user.groups, issues, ["groups"]);
|
|
327
|
+
result.roles = validateRoles(user.roles, issues, ["roles"]);
|
|
328
|
+
const enterpriseKey = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User";
|
|
329
|
+
if (user[enterpriseKey] !== undefined) {
|
|
330
|
+
result[enterpriseKey] = validateEnterpriseUser(user[enterpriseKey], issues, [enterpriseKey]);
|
|
59
331
|
}
|
|
60
|
-
if (
|
|
61
|
-
if (
|
|
62
|
-
result.
|
|
332
|
+
if (user.schemas !== undefined) {
|
|
333
|
+
if (Array.isArray(user.schemas)) {
|
|
334
|
+
result.schemas = user.schemas.filter((s) => typeof s === "string");
|
|
63
335
|
} else {
|
|
64
336
|
issues.push({
|
|
65
|
-
message: "
|
|
66
|
-
path: ["
|
|
337
|
+
message: "schemas must be an array",
|
|
338
|
+
path: ["schemas"]
|
|
67
339
|
});
|
|
68
340
|
}
|
|
69
|
-
if ("error_description" in params) {
|
|
70
|
-
if (typeof params.error_description === "string" || params.error_description === undefined) {
|
|
71
|
-
result.error_description = params.error_description;
|
|
72
|
-
} else {
|
|
73
|
-
issues.push({
|
|
74
|
-
message: "error_description must be a string",
|
|
75
|
-
path: ["error_description"]
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if ("error_uri" in params) {
|
|
80
|
-
if (typeof params.error_uri === "string" || params.error_uri === undefined) {
|
|
81
|
-
result.error_uri = params.error_uri;
|
|
82
|
-
} else {
|
|
83
|
-
issues.push({
|
|
84
|
-
message: "error_uri must be a string",
|
|
85
|
-
path: ["error_uri"]
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
341
|
}
|
|
90
|
-
if (
|
|
91
|
-
if (typeof
|
|
92
|
-
|
|
342
|
+
if (user.meta !== undefined) {
|
|
343
|
+
if (typeof user.meta === "object" && user.meta !== null) {
|
|
344
|
+
const meta = user.meta;
|
|
345
|
+
result.meta = {
|
|
346
|
+
resourceType: typeof meta.resourceType === "string" ? meta.resourceType : undefined,
|
|
347
|
+
created: typeof meta.created === "string" ? meta.created : undefined,
|
|
348
|
+
lastModified: typeof meta.lastModified === "string" ? meta.lastModified : undefined,
|
|
349
|
+
location: typeof meta.location === "string" ? meta.location : undefined,
|
|
350
|
+
version: typeof meta.version === "string" ? meta.version : undefined
|
|
351
|
+
};
|
|
93
352
|
} else {
|
|
94
353
|
issues.push({
|
|
95
|
-
message: "
|
|
96
|
-
path: ["
|
|
354
|
+
message: "meta must be an object",
|
|
355
|
+
path: ["meta"]
|
|
97
356
|
});
|
|
98
357
|
}
|
|
99
358
|
}
|
|
@@ -105,7 +364,43 @@ function oidcCallbackSchema(vendor) {
|
|
|
105
364
|
}
|
|
106
365
|
};
|
|
107
366
|
}
|
|
108
|
-
function
|
|
367
|
+
function validateMembers(value, issues, basePath) {
|
|
368
|
+
if (value === undefined || value === null) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!Array.isArray(value)) {
|
|
372
|
+
issues.push({
|
|
373
|
+
message: "members must be an array",
|
|
374
|
+
path: basePath
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const members = [];
|
|
379
|
+
for (let i = 0;i < value.length; i++) {
|
|
380
|
+
const member = value[i];
|
|
381
|
+
const memberPath = [...basePath, i];
|
|
382
|
+
if (typeof member !== "object" || member === null) {
|
|
383
|
+
issues.push({
|
|
384
|
+
message: "member must be an object",
|
|
385
|
+
path: memberPath
|
|
386
|
+
});
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const memberObj = member;
|
|
390
|
+
const memberValue = validateString(memberObj.value, "value", true, issues, [...memberPath, "value"]);
|
|
391
|
+
if (memberValue) {
|
|
392
|
+
const memberType = validateString(memberObj.type, "type", false, issues, [...memberPath, "type"]);
|
|
393
|
+
members.push({
|
|
394
|
+
value: memberValue,
|
|
395
|
+
$ref: validateString(memberObj.$ref, "$ref", false, issues, [...memberPath, "$ref"]),
|
|
396
|
+
display: validateString(memberObj.display, "display", false, issues, [...memberPath, "display"]),
|
|
397
|
+
type: memberType === "User" || memberType === "Group" ? memberType : undefined
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return members.length > 0 ? members : undefined;
|
|
402
|
+
}
|
|
403
|
+
function groupResourceSchema(vendor) {
|
|
109
404
|
return {
|
|
110
405
|
"~standard": {
|
|
111
406
|
version: 1,
|
|
@@ -120,123 +415,588 @@ function tokenResponseSchema(vendor) {
|
|
|
120
415
|
]
|
|
121
416
|
};
|
|
122
417
|
}
|
|
123
|
-
const
|
|
418
|
+
const group = value;
|
|
124
419
|
const issues = [];
|
|
125
420
|
const result = {};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
} else {
|
|
130
|
-
issues.push({
|
|
131
|
-
message: "access_token must be a string",
|
|
132
|
-
path: ["access_token"]
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
} else {
|
|
136
|
-
issues.push({
|
|
137
|
-
message: "access_token is required",
|
|
138
|
-
path: ["access_token"]
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
if ("id_token" in response) {
|
|
142
|
-
if (typeof response.id_token === "string") {
|
|
143
|
-
result.id_token = response.id_token;
|
|
144
|
-
} else {
|
|
145
|
-
issues.push({
|
|
146
|
-
message: "id_token must be a string",
|
|
147
|
-
path: ["id_token"]
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
issues.push({
|
|
152
|
-
message: "id_token is required",
|
|
153
|
-
path: ["id_token"]
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
if ("token_type" in response) {
|
|
157
|
-
if (typeof response.token_type === "string") {
|
|
158
|
-
result.token_type = response.token_type;
|
|
159
|
-
} else {
|
|
160
|
-
issues.push({
|
|
161
|
-
message: "token_type must be a string",
|
|
162
|
-
path: ["token_type"]
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
issues.push({
|
|
167
|
-
message: "token_type is required",
|
|
168
|
-
path: ["token_type"]
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
if ("refresh_token" in response) {
|
|
172
|
-
if (typeof response.refresh_token === "string" || response.refresh_token === undefined) {
|
|
173
|
-
result.refresh_token = response.refresh_token;
|
|
174
|
-
} else {
|
|
175
|
-
issues.push({
|
|
176
|
-
message: "refresh_token must be a string",
|
|
177
|
-
path: ["refresh_token"]
|
|
178
|
-
});
|
|
179
|
-
}
|
|
421
|
+
const displayName = validateString(group.displayName, "displayName", true, issues, ["displayName"]);
|
|
422
|
+
if (!displayName) {
|
|
423
|
+
return { issues };
|
|
180
424
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
425
|
+
result.displayName = displayName;
|
|
426
|
+
result.id = validateString(group.id, "id", false, issues, ["id"]);
|
|
427
|
+
result.externalId = validateString(group.externalId, "externalId", false, issues, ["externalId"]);
|
|
428
|
+
result.members = validateMembers(group.members, issues, ["members"]);
|
|
429
|
+
if (group.schemas !== undefined) {
|
|
430
|
+
if (Array.isArray(group.schemas)) {
|
|
431
|
+
result.schemas = group.schemas.filter((s) => typeof s === "string");
|
|
184
432
|
} else {
|
|
185
433
|
issues.push({
|
|
186
|
-
message: "
|
|
187
|
-
path: ["
|
|
434
|
+
message: "schemas must be an array",
|
|
435
|
+
path: ["schemas"]
|
|
188
436
|
});
|
|
189
437
|
}
|
|
190
438
|
}
|
|
191
|
-
if (
|
|
192
|
-
if (typeof
|
|
193
|
-
|
|
439
|
+
if (group.meta !== undefined) {
|
|
440
|
+
if (typeof group.meta === "object" && group.meta !== null) {
|
|
441
|
+
const meta = group.meta;
|
|
442
|
+
result.meta = {
|
|
443
|
+
resourceType: typeof meta.resourceType === "string" ? meta.resourceType : undefined,
|
|
444
|
+
created: typeof meta.created === "string" ? meta.created : undefined,
|
|
445
|
+
lastModified: typeof meta.lastModified === "string" ? meta.lastModified : undefined,
|
|
446
|
+
location: typeof meta.location === "string" ? meta.location : undefined,
|
|
447
|
+
version: typeof meta.version === "string" ? meta.version : undefined
|
|
448
|
+
};
|
|
194
449
|
} else {
|
|
195
450
|
issues.push({
|
|
196
|
-
message: "
|
|
197
|
-
path: ["
|
|
451
|
+
message: "meta must be an object",
|
|
452
|
+
path: ["meta"]
|
|
198
453
|
});
|
|
199
454
|
}
|
|
200
455
|
}
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
result.expires = response.expires;
|
|
204
|
-
} else {
|
|
205
|
-
issues.push({
|
|
206
|
-
message: "expires must be a string",
|
|
207
|
-
path: ["expires"]
|
|
208
|
-
});
|
|
209
|
-
}
|
|
456
|
+
if (issues.length > 0) {
|
|
457
|
+
return { issues };
|
|
210
458
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
459
|
+
return { value: result };
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// packages/react/src/iam.ts
|
|
466
|
+
var SCIM_CONTENT_TYPE = "application/scim+json";
|
|
467
|
+
function scimErrorResponse(status, detail, scimType) {
|
|
468
|
+
return new Response(JSON.stringify({
|
|
469
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
470
|
+
status: String(status),
|
|
471
|
+
scimType,
|
|
472
|
+
detail
|
|
473
|
+
}), {
|
|
474
|
+
status,
|
|
475
|
+
headers: { "Content-Type": SCIM_CONTENT_TYPE }
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
function scimListResponse(resources) {
|
|
479
|
+
return new Response(JSON.stringify({
|
|
480
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
481
|
+
totalResults: resources.length,
|
|
482
|
+
startIndex: 1,
|
|
483
|
+
itemsPerPage: resources.length,
|
|
484
|
+
Resources: resources
|
|
485
|
+
}), {
|
|
486
|
+
status: 200,
|
|
487
|
+
headers: { "Content-Type": SCIM_CONTENT_TYPE }
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function scimResourceResponse(resource, status = 200) {
|
|
491
|
+
return new Response(JSON.stringify(resource), {
|
|
492
|
+
status,
|
|
493
|
+
headers: { "Content-Type": SCIM_CONTENT_TYPE }
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
function storedGroupToResource(group) {
|
|
497
|
+
return {
|
|
498
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
|
499
|
+
id: group.id,
|
|
500
|
+
externalId: group.externalId,
|
|
501
|
+
displayName: group.displayName,
|
|
502
|
+
members: group.members,
|
|
503
|
+
meta: {
|
|
504
|
+
resourceType: "Group",
|
|
505
|
+
created: group.createdAt.toISOString(),
|
|
506
|
+
lastModified: group.updatedAt.toISOString()
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function generateId() {
|
|
511
|
+
return crypto.randomUUID();
|
|
512
|
+
}
|
|
513
|
+
function iam(config, workload) {
|
|
514
|
+
const { url, group_store } = config;
|
|
515
|
+
async function buildHeaders() {
|
|
516
|
+
const token = await workload.getToken();
|
|
517
|
+
return new Headers({
|
|
518
|
+
"Content-Type": SCIM_CONTENT_TYPE,
|
|
519
|
+
Accept: SCIM_CONTENT_TYPE,
|
|
520
|
+
Authorization: `Bearer ${token}`
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
async function scimRequest(method, endpoint, body, validator) {
|
|
524
|
+
if (!url) {
|
|
525
|
+
return {
|
|
526
|
+
success: false,
|
|
527
|
+
error: {
|
|
528
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
529
|
+
status: "500",
|
|
530
|
+
detail: "IAM URL not configured for outgoing requests"
|
|
531
|
+
},
|
|
532
|
+
status: 500
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const requestUrl = `${url}${endpoint}`;
|
|
536
|
+
try {
|
|
537
|
+
const headers = await buildHeaders();
|
|
538
|
+
const response = await fetch(requestUrl, {
|
|
539
|
+
method,
|
|
540
|
+
headers,
|
|
541
|
+
body: body ? JSON.stringify(body) : undefined
|
|
542
|
+
});
|
|
543
|
+
const responseData = await response.json();
|
|
544
|
+
if (!response.ok) {
|
|
545
|
+
return {
|
|
546
|
+
success: false,
|
|
547
|
+
error: responseData,
|
|
548
|
+
status: response.status
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (validator) {
|
|
552
|
+
const validationResult = await validator["~standard"].validate(responseData);
|
|
553
|
+
if ("issues" in validationResult) {
|
|
554
|
+
return {
|
|
555
|
+
success: false,
|
|
556
|
+
error: {
|
|
557
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
558
|
+
status: "400",
|
|
559
|
+
scimType: "invalidValue",
|
|
560
|
+
detail: `Response validation failed: ${validationResult.issues?.map((i) => i.message).join("; ")}`
|
|
561
|
+
},
|
|
562
|
+
status: 400
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
success: true,
|
|
567
|
+
data: validationResult.value,
|
|
568
|
+
status: response.status
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
success: true,
|
|
573
|
+
data: responseData,
|
|
574
|
+
status: response.status
|
|
575
|
+
};
|
|
576
|
+
} catch (error) {
|
|
577
|
+
return {
|
|
578
|
+
success: false,
|
|
579
|
+
error: {
|
|
580
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
581
|
+
status: "500",
|
|
582
|
+
detail: error instanceof Error ? error.message : "Unknown error occurred"
|
|
583
|
+
},
|
|
584
|
+
status: 500
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function getBaseUrl() {
|
|
589
|
+
return url;
|
|
590
|
+
}
|
|
591
|
+
let groups_outbound;
|
|
592
|
+
let createUser;
|
|
593
|
+
if (url) {
|
|
594
|
+
createUser = async (user, options) => {
|
|
595
|
+
const userPayload = {
|
|
596
|
+
...user,
|
|
597
|
+
schemas: user.schemas ?? [
|
|
598
|
+
"urn:ietf:params:scim:schemas:core:2.0:User",
|
|
599
|
+
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
|
|
600
|
+
]
|
|
601
|
+
};
|
|
602
|
+
const validator = options?.validation ?? userSchema("es-iam");
|
|
603
|
+
return scimRequest("POST", "/Users", userPayload, validator);
|
|
604
|
+
};
|
|
605
|
+
async function createGroup(displayName, options) {
|
|
606
|
+
const groupPayload = {
|
|
607
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
|
608
|
+
displayName,
|
|
609
|
+
externalId: options?.externalId,
|
|
610
|
+
members: options?.members
|
|
611
|
+
};
|
|
612
|
+
const validator = options?.validation ?? groupResourceSchema("es-iam");
|
|
613
|
+
return scimRequest("POST", "/Groups", groupPayload, validator);
|
|
614
|
+
}
|
|
615
|
+
groups_outbound = {
|
|
616
|
+
createGroup
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
let groups_inbound;
|
|
620
|
+
if (group_store) {
|
|
621
|
+
const store = group_store;
|
|
622
|
+
const validateAuth = async (request) => {
|
|
623
|
+
const auth = request.headers.get("Authorization");
|
|
624
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
const token = auth.substring(7);
|
|
629
|
+
const result = await workload.validateToken(token);
|
|
630
|
+
return result.valid;
|
|
631
|
+
} catch {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
const handler = async (request, handlerConfig) => {
|
|
636
|
+
const isAuthorized = await validateAuth(request);
|
|
637
|
+
if (!isAuthorized) {
|
|
638
|
+
return scimErrorResponse(401, "Authorization required");
|
|
639
|
+
}
|
|
640
|
+
const urlObj = new URL(request.url);
|
|
641
|
+
const basePath = handlerConfig?.basePath ?? "/Groups";
|
|
642
|
+
let path = urlObj.pathname;
|
|
643
|
+
if (path.startsWith(basePath)) {
|
|
644
|
+
path = path.substring(basePath.length);
|
|
645
|
+
}
|
|
646
|
+
const groupIdMatch = path.match(/^\/([^/]+)$/);
|
|
647
|
+
const groupId = groupIdMatch?.[1];
|
|
648
|
+
const method = request.method;
|
|
649
|
+
try {
|
|
650
|
+
if (groupId) {
|
|
651
|
+
switch (method) {
|
|
652
|
+
case "GET":
|
|
653
|
+
return await handleGetGroup(groupId);
|
|
654
|
+
case "PUT":
|
|
655
|
+
return await handleReplaceGroup(request, groupId);
|
|
656
|
+
case "PATCH":
|
|
657
|
+
return await handlePatchGroup(request, groupId);
|
|
658
|
+
case "DELETE":
|
|
659
|
+
return await handleDeleteGroup(groupId);
|
|
660
|
+
default:
|
|
661
|
+
return scimErrorResponse(405, "Method not allowed");
|
|
662
|
+
}
|
|
663
|
+
} else if (path === "" || path === "/") {
|
|
664
|
+
switch (method) {
|
|
665
|
+
case "GET":
|
|
666
|
+
return await handleListGroups();
|
|
667
|
+
case "POST":
|
|
668
|
+
return await handleCreateGroup(request);
|
|
669
|
+
default:
|
|
670
|
+
return scimErrorResponse(405, "Method not allowed");
|
|
219
671
|
}
|
|
220
672
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
673
|
+
return scimErrorResponse(404, "Resource not found");
|
|
674
|
+
} catch (error) {
|
|
675
|
+
console.error("Groups inbound handler error:", error);
|
|
676
|
+
return scimErrorResponse(500, error instanceof Error ? error.message : "Internal server error");
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
const handleListGroups = async () => {
|
|
680
|
+
const groups = await store.list();
|
|
681
|
+
const resources = groups.map(storedGroupToResource);
|
|
682
|
+
return scimListResponse(resources);
|
|
683
|
+
};
|
|
684
|
+
const handleGetGroup = async (id) => {
|
|
685
|
+
const group = await store.get(id);
|
|
686
|
+
if (!group) {
|
|
687
|
+
return scimErrorResponse(404, `Group ${id} not found`, "invalidValue");
|
|
688
|
+
}
|
|
689
|
+
return scimResourceResponse(storedGroupToResource(group));
|
|
690
|
+
};
|
|
691
|
+
const handleCreateGroup = async (request) => {
|
|
692
|
+
const body = await request.json();
|
|
693
|
+
if (!body.displayName) {
|
|
694
|
+
return scimErrorResponse(400, "displayName is required", "invalidValue");
|
|
695
|
+
}
|
|
696
|
+
const now = new Date;
|
|
697
|
+
const storedGroup = {
|
|
698
|
+
id: generateId(),
|
|
699
|
+
displayName: body.displayName,
|
|
700
|
+
externalId: body.externalId,
|
|
701
|
+
members: body.members,
|
|
702
|
+
createdAt: now,
|
|
703
|
+
updatedAt: now
|
|
704
|
+
};
|
|
705
|
+
await store.upsert(storedGroup);
|
|
706
|
+
return scimResourceResponse(storedGroupToResource(storedGroup), 201);
|
|
707
|
+
};
|
|
708
|
+
const handleReplaceGroup = async (request, id) => {
|
|
709
|
+
const existing = await store.get(id);
|
|
710
|
+
if (!existing) {
|
|
711
|
+
return scimErrorResponse(404, `Group ${id} not found`, "invalidValue");
|
|
712
|
+
}
|
|
713
|
+
const body = await request.json();
|
|
714
|
+
const updatedGroup = {
|
|
715
|
+
...existing,
|
|
716
|
+
displayName: body.displayName ?? existing.displayName,
|
|
717
|
+
externalId: body.externalId,
|
|
718
|
+
members: body.members,
|
|
719
|
+
updatedAt: new Date
|
|
720
|
+
};
|
|
721
|
+
await store.upsert(updatedGroup);
|
|
722
|
+
return scimResourceResponse(storedGroupToResource(updatedGroup));
|
|
723
|
+
};
|
|
724
|
+
const handlePatchGroup = async (request, id) => {
|
|
725
|
+
const existing = await store.get(id);
|
|
726
|
+
if (!existing) {
|
|
727
|
+
return scimErrorResponse(404, `Group ${id} not found`, "invalidValue");
|
|
728
|
+
}
|
|
729
|
+
const body = await request.json();
|
|
730
|
+
const operations = body.Operations ?? [];
|
|
731
|
+
const updated = { ...existing };
|
|
732
|
+
for (const op of operations) {
|
|
733
|
+
if (op.op === "replace" && op.path && op.value !== undefined) {
|
|
734
|
+
if (op.path === "displayName") {
|
|
735
|
+
updated.displayName = op.value;
|
|
736
|
+
}
|
|
737
|
+
} else if (op.op === "add" && op.path && op.value !== undefined) {
|
|
738
|
+
if (op.path === "members") {
|
|
739
|
+
const newMembers = op.value;
|
|
740
|
+
updated.members = [...updated.members ?? [], ...newMembers];
|
|
741
|
+
}
|
|
742
|
+
} else if (op.op === "remove" && op.path) {
|
|
743
|
+
if (op.path.startsWith("members[")) {
|
|
744
|
+
const match = op.path.match(/members\[value eq "([^"]+)"\]/);
|
|
745
|
+
if (match) {
|
|
746
|
+
updated.members = (updated.members ?? []).filter((m) => m.value !== match[1]);
|
|
747
|
+
}
|
|
229
748
|
}
|
|
230
749
|
}
|
|
231
|
-
|
|
232
|
-
|
|
750
|
+
}
|
|
751
|
+
updated.updatedAt = new Date;
|
|
752
|
+
await store.upsert(updated);
|
|
753
|
+
return scimResourceResponse(storedGroupToResource(updated));
|
|
754
|
+
};
|
|
755
|
+
const handleDeleteGroup = async (id) => {
|
|
756
|
+
const existing = await store.get(id);
|
|
757
|
+
if (!existing) {
|
|
758
|
+
return scimErrorResponse(404, `Group ${id} not found`, "invalidValue");
|
|
759
|
+
}
|
|
760
|
+
await store.delete(id);
|
|
761
|
+
return new Response(null, { status: 204 });
|
|
762
|
+
};
|
|
763
|
+
groups_inbound = {
|
|
764
|
+
handler
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
let users_inbound;
|
|
768
|
+
if (config.user_store) {
|
|
769
|
+
const store = config.user_store;
|
|
770
|
+
async function validateAuth(request) {
|
|
771
|
+
const auth = request.headers.get("Authorization");
|
|
772
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
const token = auth.substring(7);
|
|
777
|
+
const result = await workload.validateToken(token);
|
|
778
|
+
return result.valid;
|
|
779
|
+
} catch {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const storedUserToScimUser = (storedUser) => {
|
|
784
|
+
return {
|
|
785
|
+
schemas: [
|
|
786
|
+
"urn:ietf:params:scim:schemas:core:2.0:User",
|
|
787
|
+
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
|
|
788
|
+
],
|
|
789
|
+
id: storedUser.id,
|
|
790
|
+
userName: storedUser.userName || storedUser.email || storedUser.id,
|
|
791
|
+
displayName: storedUser.name || storedUser.userName || storedUser.email,
|
|
792
|
+
name: storedUser.name ? {
|
|
793
|
+
givenName: storedUser.name.split(" ")[0],
|
|
794
|
+
familyName: storedUser.name.split(" ").slice(1).join(" ") || undefined
|
|
795
|
+
} : undefined,
|
|
796
|
+
emails: storedUser.email ? [{ value: storedUser.email, primary: true }] : [],
|
|
797
|
+
active: true,
|
|
798
|
+
meta: {
|
|
799
|
+
resourceType: "User",
|
|
800
|
+
created: storedUser.createdAt.toISOString(),
|
|
801
|
+
lastModified: storedUser.updatedAt.toISOString()
|
|
233
802
|
}
|
|
234
|
-
|
|
803
|
+
};
|
|
804
|
+
};
|
|
805
|
+
const scimUserToStoredUser = (scimUser) => {
|
|
806
|
+
const now = new Date;
|
|
807
|
+
const primaryEmail = scimUser.emails?.find((e) => e.primary)?.value || scimUser.emails?.[0]?.value;
|
|
808
|
+
const name = scimUser.name ? `${scimUser.name.givenName || ""} ${scimUser.name.familyName || ""}`.trim() : scimUser.displayName;
|
|
809
|
+
const userId = scimUser.id || generateId();
|
|
810
|
+
const userName = scimUser.userName || primaryEmail || userId;
|
|
811
|
+
return {
|
|
812
|
+
id: userId,
|
|
813
|
+
userName,
|
|
814
|
+
name: name || scimUser.displayName || userName,
|
|
815
|
+
email: primaryEmail || userName,
|
|
816
|
+
avatarUrl: scimUser.profileUrl,
|
|
817
|
+
sso: {
|
|
818
|
+
profile: {
|
|
819
|
+
sub: userId,
|
|
820
|
+
iss: "iam-provisioned",
|
|
821
|
+
aud: "iam-provisioned",
|
|
822
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
823
|
+
iat: Math.floor(Date.now() / 1000),
|
|
824
|
+
email: primaryEmail || userName,
|
|
825
|
+
email_verified: true,
|
|
826
|
+
name: name || scimUser.displayName || userName,
|
|
827
|
+
preferred_username: userName
|
|
828
|
+
},
|
|
829
|
+
tenant: {
|
|
830
|
+
id: "iam-provisioned",
|
|
831
|
+
name: "IAM Provisioned"
|
|
832
|
+
},
|
|
833
|
+
scope: "openid profile email",
|
|
834
|
+
tokenType: "Bearer",
|
|
835
|
+
expires: new Date(Date.now() + 3600 * 1000)
|
|
836
|
+
},
|
|
837
|
+
createdAt: scimUser.meta?.created ? new Date(scimUser.meta.created) : now,
|
|
838
|
+
updatedAt: scimUser.meta?.lastModified ? new Date(scimUser.meta.lastModified) : now
|
|
839
|
+
};
|
|
840
|
+
};
|
|
841
|
+
const handler = async (request, handlerConfig) => {
|
|
842
|
+
const isAuthorized = await validateAuth(request);
|
|
843
|
+
if (!isAuthorized) {
|
|
844
|
+
return scimErrorResponse(401, "Authorization required");
|
|
845
|
+
}
|
|
846
|
+
const urlObj = new URL(request.url);
|
|
847
|
+
const basePath = handlerConfig?.basePath ?? "/Users";
|
|
848
|
+
let path = urlObj.pathname;
|
|
849
|
+
if (path.startsWith(basePath)) {
|
|
850
|
+
path = path.substring(basePath.length);
|
|
851
|
+
}
|
|
852
|
+
const userIdMatch = path.match(/^\/([^/]+)$/);
|
|
853
|
+
const userId = userIdMatch?.[1];
|
|
854
|
+
const method = request.method;
|
|
855
|
+
try {
|
|
856
|
+
if (userId) {
|
|
857
|
+
switch (method) {
|
|
858
|
+
case "GET":
|
|
859
|
+
return await handleGetUser(userId);
|
|
860
|
+
case "PUT":
|
|
861
|
+
return await handleReplaceUser(request, userId);
|
|
862
|
+
case "PATCH":
|
|
863
|
+
return await handlePatchUser(request, userId);
|
|
864
|
+
case "DELETE":
|
|
865
|
+
return await handleDeleteUser(userId);
|
|
866
|
+
default:
|
|
867
|
+
return scimErrorResponse(405, "Method not allowed");
|
|
868
|
+
}
|
|
869
|
+
} else if (path === "" || path === "/") {
|
|
870
|
+
switch (method) {
|
|
871
|
+
case "GET":
|
|
872
|
+
return await handleListUsers();
|
|
873
|
+
case "POST":
|
|
874
|
+
return await handleCreateUser(request);
|
|
875
|
+
default:
|
|
876
|
+
return scimErrorResponse(405, "Method not allowed");
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return scimErrorResponse(404, "Resource not found");
|
|
880
|
+
} catch (error) {
|
|
881
|
+
console.error("Users inbound handler error:", error);
|
|
882
|
+
return scimErrorResponse(500, error instanceof Error ? error.message : "Internal server error");
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
const handleListUsers = async () => {
|
|
886
|
+
return scimListResponse([]);
|
|
887
|
+
};
|
|
888
|
+
const handleGetUser = async (id) => {
|
|
889
|
+
const user = await store.get(id);
|
|
890
|
+
if (!user) {
|
|
891
|
+
return scimErrorResponse(404, `User ${id} not found`, "invalidValue");
|
|
892
|
+
}
|
|
893
|
+
return scimResourceResponse(storedUserToScimUser(user));
|
|
894
|
+
};
|
|
895
|
+
const handleCreateUser = async (request) => {
|
|
896
|
+
const body = await request.json();
|
|
897
|
+
if (!body.userName && !body.emails?.[0]?.value) {
|
|
898
|
+
return scimErrorResponse(400, "userName or email is required", "invalidValue");
|
|
899
|
+
}
|
|
900
|
+
const storedUser = scimUserToStoredUser(body);
|
|
901
|
+
await store.upsert(storedUser);
|
|
902
|
+
return scimResourceResponse(storedUserToScimUser(storedUser), 201);
|
|
903
|
+
};
|
|
904
|
+
const handleReplaceUser = async (request, id) => {
|
|
905
|
+
const existing = await store.get(id);
|
|
906
|
+
if (!existing) {
|
|
907
|
+
return scimErrorResponse(404, `User ${id} not found`, "invalidValue");
|
|
908
|
+
}
|
|
909
|
+
const body = await request.json();
|
|
910
|
+
const updatedUser = scimUserToStoredUser({ ...body, id });
|
|
911
|
+
updatedUser.createdAt = existing.createdAt;
|
|
912
|
+
updatedUser.updatedAt = new Date;
|
|
913
|
+
await store.upsert(updatedUser);
|
|
914
|
+
return scimResourceResponse(storedUserToScimUser(updatedUser));
|
|
915
|
+
};
|
|
916
|
+
const handlePatchUser = async (request, id) => {
|
|
917
|
+
const existing = await store.get(id);
|
|
918
|
+
if (!existing) {
|
|
919
|
+
return scimErrorResponse(404, `User ${id} not found`, "invalidValue");
|
|
920
|
+
}
|
|
921
|
+
const body = await request.json();
|
|
922
|
+
const operations = body.Operations ?? [];
|
|
923
|
+
const updated = { ...existing };
|
|
924
|
+
for (const op of operations) {
|
|
925
|
+
if (op.op === "replace" && op.path && op.value !== undefined) {
|
|
926
|
+
if (op.path === "displayName") {
|
|
927
|
+
updated.name = op.value;
|
|
928
|
+
} else if (op.path === "userName") {
|
|
929
|
+
updated.userName = op.value;
|
|
930
|
+
} else if (op.path.startsWith("name.")) {
|
|
931
|
+
const namePart = op.path.split(".")[1];
|
|
932
|
+
if (!updated.name)
|
|
933
|
+
updated.name = "";
|
|
934
|
+
if (namePart === "givenName") {
|
|
935
|
+
updated.name = `${op.value} ${updated.name.split(" ").slice(1).join(" ")}`.trim();
|
|
936
|
+
} else if (namePart === "familyName") {
|
|
937
|
+
updated.name = `${updated.name.split(" ")[0]} ${op.value}`.trim();
|
|
938
|
+
}
|
|
939
|
+
} else if (op.path === "emails") {
|
|
940
|
+
const emails = op.value;
|
|
941
|
+
const primaryEmail = emails?.find((e) => e.primary)?.value || emails?.[0]?.value;
|
|
942
|
+
if (primaryEmail)
|
|
943
|
+
updated.email = primaryEmail;
|
|
944
|
+
}
|
|
945
|
+
} else if (op.op === "add" && op.path && op.value !== undefined) {
|
|
946
|
+
if (op.path === "emails") {
|
|
947
|
+
const newEmails = op.value;
|
|
948
|
+
const primaryEmail = newEmails?.find((e) => e.primary)?.value || newEmails?.[0]?.value;
|
|
949
|
+
if (primaryEmail)
|
|
950
|
+
updated.email = primaryEmail;
|
|
951
|
+
}
|
|
952
|
+
} else if (op.op === "remove" && op.path) {
|
|
953
|
+
if (op.path === "displayName") {
|
|
954
|
+
updated.name = "";
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
updated.updatedAt = new Date;
|
|
959
|
+
await store.upsert(updated);
|
|
960
|
+
return scimResourceResponse(storedUserToScimUser(updated));
|
|
961
|
+
};
|
|
962
|
+
const handleDeleteUser = async (id) => {
|
|
963
|
+
const existing = await store.get(id);
|
|
964
|
+
if (!existing) {
|
|
965
|
+
return scimErrorResponse(404, `User ${id} not found`, "invalidValue");
|
|
235
966
|
}
|
|
967
|
+
await store.delete(id);
|
|
968
|
+
return new Response(null, { status: 204 });
|
|
969
|
+
};
|
|
970
|
+
users_inbound = {
|
|
971
|
+
handler
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
async function topLevelHandler(request, handlerConfig) {
|
|
975
|
+
const urlObj = new URL(request.url);
|
|
976
|
+
const path = urlObj.pathname;
|
|
977
|
+
const usersUrl = handlerConfig?.usersUrl ?? config.usersUrl ?? "/api/iam/Users";
|
|
978
|
+
const groupsUrl = handlerConfig?.groupsUrl ?? config.groupsUrl ?? "/api/iam/Groups";
|
|
979
|
+
if (path.startsWith(usersUrl) && users_inbound) {
|
|
980
|
+
return users_inbound.handler(request, { basePath: usersUrl });
|
|
236
981
|
}
|
|
982
|
+
if (path.startsWith(groupsUrl) && groups_inbound) {
|
|
983
|
+
return groups_inbound.handler(request, { basePath: groupsUrl });
|
|
984
|
+
}
|
|
985
|
+
return scimErrorResponse(404, "Resource not found");
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
...config,
|
|
989
|
+
createUser,
|
|
990
|
+
getBaseUrl,
|
|
991
|
+
groups_outbound,
|
|
992
|
+
groups_inbound,
|
|
993
|
+
users_inbound,
|
|
994
|
+
handler: topLevelHandler
|
|
237
995
|
};
|
|
238
996
|
}
|
|
239
|
-
|
|
997
|
+
|
|
998
|
+
// packages/react/src/types/oidc-schema.ts
|
|
999
|
+
function oidcCallbackSchema(vendor) {
|
|
240
1000
|
return {
|
|
241
1001
|
"~standard": {
|
|
242
1002
|
version: 1,
|
|
@@ -251,31 +1011,84 @@ function idTokenClaimsSchema(vendor) {
|
|
|
251
1011
|
]
|
|
252
1012
|
};
|
|
253
1013
|
}
|
|
254
|
-
const
|
|
1014
|
+
const params = value;
|
|
255
1015
|
const issues = [];
|
|
256
|
-
const result = {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
1016
|
+
const result = {};
|
|
1017
|
+
if ("code" in params) {
|
|
1018
|
+
if (typeof params.code === "string") {
|
|
1019
|
+
result.code = params.code;
|
|
1020
|
+
} else {
|
|
1021
|
+
issues.push({
|
|
1022
|
+
message: "code must be a string",
|
|
1023
|
+
path: ["code"]
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
} else if (!("error" in params)) {
|
|
1027
|
+
issues.push({
|
|
1028
|
+
message: "code is required",
|
|
1029
|
+
path: ["code"]
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if ("state" in params) {
|
|
1033
|
+
if (typeof params.state === "string" || params.state === undefined) {
|
|
1034
|
+
result.state = params.state;
|
|
1035
|
+
} else {
|
|
1036
|
+
issues.push({
|
|
1037
|
+
message: "state must be a string",
|
|
1038
|
+
path: ["state"]
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if ("session_state" in params) {
|
|
1043
|
+
if (typeof params.session_state === "string" || params.session_state === undefined) {
|
|
1044
|
+
result.session_state = params.session_state;
|
|
1045
|
+
} else {
|
|
1046
|
+
issues.push({
|
|
1047
|
+
message: "session_state must be a string",
|
|
1048
|
+
path: ["session_state"]
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if ("error" in params) {
|
|
1053
|
+
if (typeof params.error === "string") {
|
|
1054
|
+
result.error = params.error;
|
|
1055
|
+
} else {
|
|
1056
|
+
issues.push({
|
|
1057
|
+
message: "error must be a string",
|
|
1058
|
+
path: ["error"]
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
if ("error_description" in params) {
|
|
1062
|
+
if (typeof params.error_description === "string" || params.error_description === undefined) {
|
|
1063
|
+
result.error_description = params.error_description;
|
|
1064
|
+
} else {
|
|
261
1065
|
issues.push({
|
|
262
|
-
message:
|
|
263
|
-
path: [
|
|
1066
|
+
message: "error_description must be a string",
|
|
1067
|
+
path: ["error_description"]
|
|
264
1068
|
});
|
|
265
1069
|
}
|
|
266
1070
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (typeof claims[field] !== "number") {
|
|
1071
|
+
if ("error_uri" in params) {
|
|
1072
|
+
if (typeof params.error_uri === "string" || params.error_uri === undefined) {
|
|
1073
|
+
result.error_uri = params.error_uri;
|
|
1074
|
+
} else {
|
|
272
1075
|
issues.push({
|
|
273
|
-
message:
|
|
274
|
-
path: [
|
|
1076
|
+
message: "error_uri must be a string",
|
|
1077
|
+
path: ["error_uri"]
|
|
275
1078
|
});
|
|
276
1079
|
}
|
|
277
1080
|
}
|
|
278
1081
|
}
|
|
1082
|
+
if ("iss" in params) {
|
|
1083
|
+
if (typeof params.iss === "string" || params.iss === undefined) {
|
|
1084
|
+
result.iss = params.iss;
|
|
1085
|
+
} else {
|
|
1086
|
+
issues.push({
|
|
1087
|
+
message: "iss must be a string",
|
|
1088
|
+
path: ["iss"]
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
279
1092
|
if (issues.length > 0) {
|
|
280
1093
|
return { issues };
|
|
281
1094
|
}
|
|
@@ -284,50 +1097,241 @@ function idTokenClaimsSchema(vendor) {
|
|
|
284
1097
|
}
|
|
285
1098
|
};
|
|
286
1099
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
1100
|
+
function tokenResponseSchema(vendor) {
|
|
1101
|
+
return {
|
|
1102
|
+
"~standard": {
|
|
1103
|
+
version: 1,
|
|
1104
|
+
vendor,
|
|
1105
|
+
validate: (value) => {
|
|
1106
|
+
if (typeof value !== "object" || value === null) {
|
|
1107
|
+
return {
|
|
1108
|
+
issues: [
|
|
1109
|
+
{
|
|
1110
|
+
message: "Expected an object"
|
|
1111
|
+
}
|
|
1112
|
+
]
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
const response = value;
|
|
1116
|
+
const issues = [];
|
|
1117
|
+
const result = {};
|
|
1118
|
+
if ("access_token" in response) {
|
|
1119
|
+
if (typeof response.access_token === "string") {
|
|
1120
|
+
result.access_token = response.access_token;
|
|
1121
|
+
} else {
|
|
1122
|
+
issues.push({
|
|
1123
|
+
message: "access_token must be a string",
|
|
1124
|
+
path: ["access_token"]
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
} else {
|
|
1128
|
+
issues.push({
|
|
1129
|
+
message: "access_token is required",
|
|
1130
|
+
path: ["access_token"]
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
if ("id_token" in response) {
|
|
1134
|
+
if (typeof response.id_token === "string") {
|
|
1135
|
+
result.id_token = response.id_token;
|
|
1136
|
+
} else {
|
|
1137
|
+
issues.push({
|
|
1138
|
+
message: "id_token must be a string",
|
|
1139
|
+
path: ["id_token"]
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
} else {
|
|
1143
|
+
issues.push({
|
|
1144
|
+
message: "id_token is required",
|
|
1145
|
+
path: ["id_token"]
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
if ("token_type" in response) {
|
|
1149
|
+
if (typeof response.token_type === "string") {
|
|
1150
|
+
result.token_type = response.token_type;
|
|
1151
|
+
} else {
|
|
1152
|
+
issues.push({
|
|
1153
|
+
message: "token_type must be a string",
|
|
1154
|
+
path: ["token_type"]
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
} else {
|
|
1158
|
+
issues.push({
|
|
1159
|
+
message: "token_type is required",
|
|
1160
|
+
path: ["token_type"]
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
if ("refresh_token" in response) {
|
|
1164
|
+
if (typeof response.refresh_token === "string" || response.refresh_token === undefined) {
|
|
1165
|
+
result.refresh_token = response.refresh_token;
|
|
1166
|
+
} else {
|
|
1167
|
+
issues.push({
|
|
1168
|
+
message: "refresh_token must be a string",
|
|
1169
|
+
path: ["refresh_token"]
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if ("scope" in response) {
|
|
1174
|
+
if (typeof response.scope === "string" || response.scope === undefined) {
|
|
1175
|
+
result.scope = response.scope;
|
|
1176
|
+
} else {
|
|
1177
|
+
issues.push({
|
|
1178
|
+
message: "scope must be a string",
|
|
1179
|
+
path: ["scope"]
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if ("session_state" in response) {
|
|
1184
|
+
if (typeof response.session_state === "string" || response.session_state === undefined) {
|
|
1185
|
+
result.session_state = response.session_state;
|
|
1186
|
+
} else {
|
|
1187
|
+
issues.push({
|
|
1188
|
+
message: "session_state must be a string",
|
|
1189
|
+
path: ["session_state"]
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if ("expires" in response) {
|
|
1194
|
+
if (typeof response.expires === "string" || response.expires === undefined) {
|
|
1195
|
+
result.expires = response.expires;
|
|
1196
|
+
} else {
|
|
1197
|
+
issues.push({
|
|
1198
|
+
message: "expires must be a string",
|
|
1199
|
+
path: ["expires"]
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if ("expires_in" in response) {
|
|
1204
|
+
if (typeof response.expires_in === "number" || response.expires_in === undefined) {
|
|
1205
|
+
result.expires_in = response.expires_in;
|
|
1206
|
+
} else {
|
|
1207
|
+
issues.push({
|
|
1208
|
+
message: "expires_in must be a number",
|
|
1209
|
+
path: ["expires_in"]
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
if ("refresh_expires_in" in response) {
|
|
1214
|
+
if (typeof response.refresh_expires_in === "number" || response.refresh_expires_in === undefined) {
|
|
1215
|
+
result.refresh_expires_in = response.refresh_expires_in;
|
|
1216
|
+
} else {
|
|
1217
|
+
issues.push({
|
|
1218
|
+
message: "refresh_expires_in must be a number",
|
|
1219
|
+
path: ["refresh_expires_in"]
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (issues.length > 0) {
|
|
1224
|
+
return { issues };
|
|
1225
|
+
}
|
|
1226
|
+
return { value: result };
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function idTokenClaimsSchema(vendor) {
|
|
1232
|
+
return {
|
|
1233
|
+
"~standard": {
|
|
1234
|
+
version: 1,
|
|
1235
|
+
vendor,
|
|
1236
|
+
validate: (value) => {
|
|
1237
|
+
if (typeof value !== "object" || value === null) {
|
|
1238
|
+
return {
|
|
1239
|
+
issues: [
|
|
1240
|
+
{
|
|
1241
|
+
message: "Expected an object"
|
|
1242
|
+
}
|
|
1243
|
+
]
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
const claims = value;
|
|
1247
|
+
const issues = [];
|
|
1248
|
+
const result = { ...claims };
|
|
1249
|
+
const stringFields = ["iss", "aud", "sub", "sid", "name", "email", "preferred_username", "picture"];
|
|
1250
|
+
for (const field of stringFields) {
|
|
1251
|
+
if (field in claims && claims[field] !== undefined) {
|
|
1252
|
+
if (typeof claims[field] !== "string") {
|
|
1253
|
+
issues.push({
|
|
1254
|
+
message: `${field} must be a string`,
|
|
1255
|
+
path: [field]
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
const numberFields = ["exp", "iat"];
|
|
1261
|
+
for (const field of numberFields) {
|
|
1262
|
+
if (field in claims && claims[field] !== undefined) {
|
|
1263
|
+
if (typeof claims[field] !== "number") {
|
|
1264
|
+
issues.push({
|
|
1265
|
+
message: `${field} must be a number`,
|
|
1266
|
+
path: [field]
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (issues.length > 0) {
|
|
1272
|
+
return { issues };
|
|
1273
|
+
}
|
|
1274
|
+
return { value: result };
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// packages/react/src/utils.ts
|
|
1281
|
+
var defaultInstance;
|
|
1282
|
+
function must(value, message = "Assertion failed. Required value is null or undefined.") {
|
|
1283
|
+
if (value === undefined || value === null) {
|
|
1284
|
+
throw new Error(message);
|
|
1285
|
+
}
|
|
1286
|
+
return value;
|
|
1287
|
+
}
|
|
1288
|
+
function setDefaultInstance(es) {
|
|
1289
|
+
defaultInstance = es;
|
|
1290
|
+
}
|
|
1291
|
+
function getDefaultInstance() {
|
|
1292
|
+
return defaultInstance;
|
|
1293
|
+
}
|
|
1294
|
+
function getES(es) {
|
|
1295
|
+
if (es)
|
|
1296
|
+
return es;
|
|
1297
|
+
if (defaultInstance)
|
|
1298
|
+
return defaultInstance;
|
|
1299
|
+
throw new Error(`TODO standardize the error message when there isn't a default EntepriseStandard`);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// packages/react/src/sso.ts
|
|
1303
|
+
var jwksCache = new Map;
|
|
1304
|
+
function sso(config) {
|
|
1305
|
+
let configWithDefaults;
|
|
1306
|
+
const handlerDefaults = {
|
|
1307
|
+
loginUrl: config?.loginUrl,
|
|
1308
|
+
userUrl: config?.userUrl,
|
|
1309
|
+
errorUrl: config?.errorUrl,
|
|
1310
|
+
landingUrl: config?.landingUrl,
|
|
1311
|
+
tokenUrl: config?.tokenUrl,
|
|
1312
|
+
refreshUrl: config?.refreshUrl,
|
|
1313
|
+
jwksUrl: config?.jwksUrl,
|
|
1314
|
+
logoutUrl: config?.logoutUrl,
|
|
1315
|
+
logoutBackChannelUrl: config?.logoutBackChannelUrl,
|
|
1316
|
+
validation: config?.validation
|
|
1317
|
+
};
|
|
1318
|
+
configWithDefaults = !config ? undefined : {
|
|
1319
|
+
...config,
|
|
1320
|
+
authority: must(config.authority, "Missing 'authority' from SSO Config"),
|
|
1321
|
+
token_url: must(config.token_url, "Missing 'token_url' from SSO Config"),
|
|
1322
|
+
authorization_url: must(config.authorization_url, "Missing 'authorization_url' from SSO Config"),
|
|
1323
|
+
client_id: must(config.client_id, "Missing 'client_id' from SSO Config"),
|
|
1324
|
+
redirect_uri: must(config.redirect_uri, "Missing 'redirect_uri' from SSO Config"),
|
|
1325
|
+
scope: must(config.scope, "Missing 'scope' from SSO Config"),
|
|
1326
|
+
response_type: config.response_type ?? "code",
|
|
1327
|
+
cookies_secure: config.cookies_secure !== undefined ? config.cookies_secure : true,
|
|
1328
|
+
cookies_same_site: config.cookies_same_site !== undefined ? config.cookies_same_site : "Strict",
|
|
1329
|
+
cookies_prefix: config.cookies_prefix ?? `es.sso.${config.client_id}`,
|
|
1330
|
+
cookies_path: config.cookies_path ?? "/"
|
|
1331
|
+
};
|
|
1332
|
+
async function getUser(request) {
|
|
328
1333
|
if (!configWithDefaults) {
|
|
329
|
-
|
|
330
|
-
return;
|
|
1334
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
331
1335
|
}
|
|
332
1336
|
try {
|
|
333
1337
|
const { tokens } = await getTokenFromCookies(request);
|
|
@@ -348,16 +1352,35 @@ function sso(config) {
|
|
|
348
1352
|
statusText: "Unauthorized"
|
|
349
1353
|
});
|
|
350
1354
|
}
|
|
351
|
-
async function initiateLogin({ landingUrl, errorUrl }) {
|
|
1355
|
+
async function initiateLogin({ landingUrl, errorUrl }, requestUrl) {
|
|
352
1356
|
if (!configWithDefaults) {
|
|
353
|
-
|
|
354
|
-
return Promise.resolve(new Response("SSO Manager not initialized", { status: 503 }));
|
|
1357
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
355
1358
|
}
|
|
356
1359
|
const state = generateRandomString();
|
|
357
1360
|
const codeVerifier = generateRandomString(64);
|
|
1361
|
+
let normalizedRedirectUri = configWithDefaults.redirect_uri;
|
|
1362
|
+
try {
|
|
1363
|
+
new URL(normalizedRedirectUri);
|
|
1364
|
+
} catch {
|
|
1365
|
+
if (requestUrl) {
|
|
1366
|
+
try {
|
|
1367
|
+
const baseUrl = new URL(requestUrl);
|
|
1368
|
+
const path = normalizedRedirectUri.startsWith("//") ? normalizedRedirectUri.slice(1) : normalizedRedirectUri.startsWith("/") ? normalizedRedirectUri : `/${normalizedRedirectUri}`;
|
|
1369
|
+
normalizedRedirectUri = new URL(path, baseUrl.origin).toString();
|
|
1370
|
+
} catch {
|
|
1371
|
+
try {
|
|
1372
|
+
const authUrl = new URL(configWithDefaults.authorization_url);
|
|
1373
|
+
const path = normalizedRedirectUri.startsWith("//") ? normalizedRedirectUri.slice(1) : normalizedRedirectUri.startsWith("/") ? normalizedRedirectUri : `/${normalizedRedirectUri}`;
|
|
1374
|
+
normalizedRedirectUri = new URL(path, authUrl.origin).toString();
|
|
1375
|
+
} catch {
|
|
1376
|
+
throw new Error(`Invalid redirect_uri: "${configWithDefaults.redirect_uri}". It must be a valid absolute URL.`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
358
1381
|
const url = new URL(configWithDefaults.authorization_url);
|
|
359
1382
|
url.searchParams.append("client_id", configWithDefaults.client_id);
|
|
360
|
-
url.searchParams.append("redirect_uri",
|
|
1383
|
+
url.searchParams.append("redirect_uri", normalizedRedirectUri);
|
|
361
1384
|
url.searchParams.append("response_type", "code");
|
|
362
1385
|
url.searchParams.append("scope", configWithDefaults.scope);
|
|
363
1386
|
url.searchParams.append("state", state);
|
|
@@ -379,6 +1402,9 @@ function sso(config) {
|
|
|
379
1402
|
});
|
|
380
1403
|
}
|
|
381
1404
|
async function logout(request, _config) {
|
|
1405
|
+
if (!configWithDefaults) {
|
|
1406
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1407
|
+
}
|
|
382
1408
|
try {
|
|
383
1409
|
const refreshToken2 = getCookie("refresh", request);
|
|
384
1410
|
if (refreshToken2) {
|
|
@@ -387,12 +1413,12 @@ function sso(config) {
|
|
|
387
1413
|
} catch (error) {
|
|
388
1414
|
console.warn("Failed to revoke token:", error);
|
|
389
1415
|
}
|
|
390
|
-
if (
|
|
1416
|
+
if (configWithDefaults.session_store) {
|
|
391
1417
|
try {
|
|
392
1418
|
const user = await getUser(request);
|
|
393
1419
|
if (user?.sso?.profile.sid) {
|
|
394
1420
|
const sid = user.sso.profile.sid;
|
|
395
|
-
await
|
|
1421
|
+
await configWithDefaults.session_store.delete(sid);
|
|
396
1422
|
console.log(`Session ${sid} deleted from store`);
|
|
397
1423
|
}
|
|
398
1424
|
} catch (error) {
|
|
@@ -437,11 +1463,11 @@ function sso(config) {
|
|
|
437
1463
|
}
|
|
438
1464
|
}
|
|
439
1465
|
async function logoutBackChannel(request) {
|
|
1466
|
+
if (!configWithDefaults) {
|
|
1467
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1468
|
+
}
|
|
440
1469
|
if (!configWithDefaults.session_store) {
|
|
441
|
-
|
|
442
|
-
status: 400,
|
|
443
|
-
statusText: "Bad Request"
|
|
444
|
-
});
|
|
1470
|
+
throw new Error("Back-Channel Logout requires session_store configuration");
|
|
445
1471
|
}
|
|
446
1472
|
try {
|
|
447
1473
|
const contentType = request.headers.get("content-type");
|
|
@@ -472,8 +1498,7 @@ function sso(config) {
|
|
|
472
1498
|
}
|
|
473
1499
|
async function callbackHandler(request, validation) {
|
|
474
1500
|
if (!configWithDefaults) {
|
|
475
|
-
|
|
476
|
-
return Promise.resolve(new Response("SSO Manager not initialized", { status: 503 }));
|
|
1501
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
477
1502
|
}
|
|
478
1503
|
const url = new URL(request.url);
|
|
479
1504
|
const params = new URLSearchParams(url.search);
|
|
@@ -503,9 +1528,9 @@ function sso(config) {
|
|
|
503
1528
|
if (stateFromUrl !== state) {
|
|
504
1529
|
throw new Error('SSO State Verifier failed, the "state" request parameter does not equal the "state" in the SSO cookie');
|
|
505
1530
|
}
|
|
506
|
-
const tokenResponse = await exchangeCodeForToken(codeFromUrl, codeVerifier, validation);
|
|
1531
|
+
const tokenResponse = await exchangeCodeForToken(codeFromUrl, codeVerifier, validation, request.url);
|
|
507
1532
|
const user = await parseUser(tokenResponse, validation);
|
|
508
|
-
if (
|
|
1533
|
+
if (configWithDefaults.session_store) {
|
|
509
1534
|
try {
|
|
510
1535
|
const sid = user.sso.profile.sid;
|
|
511
1536
|
const sub = user.id;
|
|
@@ -516,7 +1541,7 @@ function sso(config) {
|
|
|
516
1541
|
createdAt: new Date,
|
|
517
1542
|
lastActivityAt: new Date
|
|
518
1543
|
};
|
|
519
|
-
await
|
|
1544
|
+
await configWithDefaults.session_store.create(session);
|
|
520
1545
|
} else {
|
|
521
1546
|
console.warn("Session creation skipped: missing sid or sub in ID token claims");
|
|
522
1547
|
}
|
|
@@ -524,6 +1549,31 @@ function sso(config) {
|
|
|
524
1549
|
console.warn("Failed to create session:", error);
|
|
525
1550
|
}
|
|
526
1551
|
}
|
|
1552
|
+
if (configWithDefaults.user_store) {
|
|
1553
|
+
try {
|
|
1554
|
+
const sub = user.id;
|
|
1555
|
+
if (sub) {
|
|
1556
|
+
const now = new Date;
|
|
1557
|
+
const existingUser = await configWithDefaults.user_store.get(sub);
|
|
1558
|
+
if (existingUser || configWithDefaults.enable_jit_user_provisioning) {
|
|
1559
|
+
const storedUser = {
|
|
1560
|
+
...existingUser ?? {},
|
|
1561
|
+
...user,
|
|
1562
|
+
id: sub,
|
|
1563
|
+
createdAt: existingUser?.createdAt ?? now,
|
|
1564
|
+
updatedAt: now
|
|
1565
|
+
};
|
|
1566
|
+
await configWithDefaults.user_store.upsert(storedUser);
|
|
1567
|
+
} else {
|
|
1568
|
+
console.warn("JIT user provisioning disabled: user not found in store and will not be created");
|
|
1569
|
+
}
|
|
1570
|
+
} else {
|
|
1571
|
+
console.warn("User storage skipped: missing sub in ID token claims");
|
|
1572
|
+
}
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
console.warn("Failed to store user:", error);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
527
1577
|
return new Response("Authentication successful, redirecting", {
|
|
528
1578
|
status: 302,
|
|
529
1579
|
headers: [
|
|
@@ -543,61 +1593,843 @@ function sso(config) {
|
|
|
543
1593
|
headers: [["Location", errorUrl]]
|
|
544
1594
|
});
|
|
545
1595
|
}
|
|
546
|
-
} catch (_err) {
|
|
547
|
-
console.warn("Error parsing the errorUrl from the OIDC cookie");
|
|
1596
|
+
} catch (_err) {
|
|
1597
|
+
console.warn("Error parsing the errorUrl from the OIDC cookie");
|
|
1598
|
+
}
|
|
1599
|
+
console.warn("No error page was found in the cookies. The user will be shown a default error page.");
|
|
1600
|
+
return new Response("An error occurred during authentication, please return to the application homepage and try again.", {
|
|
1601
|
+
status: 500
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
async function parseUser(token, validation) {
|
|
1606
|
+
if (!configWithDefaults) {
|
|
1607
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1608
|
+
}
|
|
1609
|
+
const idToken = await parseJwt(token.id_token, validation);
|
|
1610
|
+
const expiresIn = Number(token.refresh_expires_in ?? token.expires_in ?? 3600);
|
|
1611
|
+
const expires = token.expires ? new Date(token.expires) : new Date(Date.now() + expiresIn * 1000);
|
|
1612
|
+
return {
|
|
1613
|
+
id: idToken.sub,
|
|
1614
|
+
userName: idToken.preferred_username || "",
|
|
1615
|
+
name: idToken.name || "",
|
|
1616
|
+
email: idToken.email || "",
|
|
1617
|
+
emails: [
|
|
1618
|
+
{
|
|
1619
|
+
value: idToken.email || "",
|
|
1620
|
+
primary: true
|
|
1621
|
+
}
|
|
1622
|
+
],
|
|
1623
|
+
avatarUrl: idToken.picture,
|
|
1624
|
+
sso: {
|
|
1625
|
+
profile: {
|
|
1626
|
+
...idToken,
|
|
1627
|
+
iss: idToken.iss || configWithDefaults.authority,
|
|
1628
|
+
aud: idToken.aud || configWithDefaults.client_id
|
|
1629
|
+
},
|
|
1630
|
+
tenant: {
|
|
1631
|
+
id: idToken.idp || idToken.iss || configWithDefaults.authority,
|
|
1632
|
+
name: idToken.iss || configWithDefaults.authority
|
|
1633
|
+
},
|
|
1634
|
+
scope: token.scope,
|
|
1635
|
+
tokenType: token.token_type,
|
|
1636
|
+
sessionState: token.session_state,
|
|
1637
|
+
expires
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
async function exchangeCodeForToken(code, codeVerifier, validation, requestUrl) {
|
|
1642
|
+
if (!configWithDefaults) {
|
|
1643
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1644
|
+
}
|
|
1645
|
+
const tokenUrl = configWithDefaults.token_url;
|
|
1646
|
+
let normalizedRedirectUri = configWithDefaults.redirect_uri;
|
|
1647
|
+
try {
|
|
1648
|
+
new URL(normalizedRedirectUri);
|
|
1649
|
+
} catch {
|
|
1650
|
+
if (requestUrl) {
|
|
1651
|
+
try {
|
|
1652
|
+
const baseUrl = new URL(requestUrl);
|
|
1653
|
+
const path = normalizedRedirectUri.startsWith("//") ? normalizedRedirectUri.slice(1) : normalizedRedirectUri.startsWith("/") ? normalizedRedirectUri : `/${normalizedRedirectUri}`;
|
|
1654
|
+
normalizedRedirectUri = new URL(path, baseUrl.origin).toString();
|
|
1655
|
+
} catch {
|
|
1656
|
+
try {
|
|
1657
|
+
const tokenUrlObj = new URL(tokenUrl);
|
|
1658
|
+
const path = normalizedRedirectUri.startsWith("//") ? normalizedRedirectUri.slice(1) : normalizedRedirectUri.startsWith("/") ? normalizedRedirectUri : `/${normalizedRedirectUri}`;
|
|
1659
|
+
normalizedRedirectUri = new URL(path, tokenUrlObj.origin).toString();
|
|
1660
|
+
} catch {
|
|
1661
|
+
throw new Error(`Invalid redirect_uri: "${configWithDefaults.redirect_uri}". It must be a valid absolute URL.`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
const body = new URLSearchParams;
|
|
1667
|
+
body.append("grant_type", "authorization_code");
|
|
1668
|
+
body.append("code", code);
|
|
1669
|
+
body.append("redirect_uri", normalizedRedirectUri);
|
|
1670
|
+
body.append("client_id", configWithDefaults.client_id);
|
|
1671
|
+
if (configWithDefaults.client_secret) {
|
|
1672
|
+
body.append("client_secret", configWithDefaults.client_secret);
|
|
1673
|
+
}
|
|
1674
|
+
body.append("code_verifier", codeVerifier);
|
|
1675
|
+
try {
|
|
1676
|
+
const response = await fetch(tokenUrl, {
|
|
1677
|
+
method: "POST",
|
|
1678
|
+
headers: {
|
|
1679
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1680
|
+
Accept: "application/json"
|
|
1681
|
+
},
|
|
1682
|
+
body: body.toString()
|
|
1683
|
+
});
|
|
1684
|
+
const data = await response.json();
|
|
1685
|
+
if (!response.ok) {
|
|
1686
|
+
console.error("Token exchange error:", data);
|
|
1687
|
+
throw new Error(`Token exchange failed: ${data.error || response.statusText} - ${data.error_description || ""}`.trim());
|
|
1688
|
+
}
|
|
1689
|
+
const tokenResponseValidator = validation?.tokenResponse ?? tokenResponseSchema("builtin");
|
|
1690
|
+
const tokenResult = await tokenResponseValidator["~standard"].validate(data);
|
|
1691
|
+
if ("issues" in tokenResult) {
|
|
1692
|
+
console.error("Token response validation failed:", tokenResult.issues);
|
|
1693
|
+
throw new Error(`Token response validation failed: ${tokenResult.issues?.map((i) => i.message).join("; ")}`);
|
|
1694
|
+
}
|
|
1695
|
+
return tokenResult.value;
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
console.error("Error during token exchange:", error);
|
|
1698
|
+
throw error;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
async function refreshToken(refreshToken2) {
|
|
1702
|
+
return retryWithBackoff(async () => {
|
|
1703
|
+
if (!configWithDefaults) {
|
|
1704
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1705
|
+
}
|
|
1706
|
+
const tokenUrl = configWithDefaults.token_url;
|
|
1707
|
+
const body = new URLSearchParams;
|
|
1708
|
+
body.append("grant_type", "refresh_token");
|
|
1709
|
+
body.append("refresh_token", refreshToken2);
|
|
1710
|
+
body.append("client_id", configWithDefaults.client_id);
|
|
1711
|
+
const response = await fetch(tokenUrl, {
|
|
1712
|
+
method: "POST",
|
|
1713
|
+
headers: {
|
|
1714
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1715
|
+
Accept: "application/json"
|
|
1716
|
+
},
|
|
1717
|
+
body: body.toString()
|
|
1718
|
+
});
|
|
1719
|
+
const data = await response.json();
|
|
1720
|
+
if (!response.ok) {
|
|
1721
|
+
console.error("Token refresh error:", data);
|
|
1722
|
+
throw new Error(`Token refresh failed: ${data.error || response.statusText} - ${data.error_description || ""}`.trim());
|
|
1723
|
+
}
|
|
1724
|
+
return data;
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
async function revokeToken(token) {
|
|
1728
|
+
try {
|
|
1729
|
+
if (!configWithDefaults) {
|
|
1730
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1731
|
+
}
|
|
1732
|
+
if (!configWithDefaults.revocation_endpoint) {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const body = new URLSearchParams;
|
|
1736
|
+
body.append("token", token);
|
|
1737
|
+
body.append("token_type_hint", "refresh_token");
|
|
1738
|
+
body.append("client_id", configWithDefaults.client_id);
|
|
1739
|
+
const response = await fetch(configWithDefaults.revocation_endpoint, {
|
|
1740
|
+
method: "POST",
|
|
1741
|
+
headers: {
|
|
1742
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1743
|
+
},
|
|
1744
|
+
body: body.toString()
|
|
1745
|
+
});
|
|
1746
|
+
if (!response.ok) {
|
|
1747
|
+
console.warn("Token revocation failed:", response.status, response.statusText);
|
|
1748
|
+
} else {
|
|
1749
|
+
console.log("Token revoked successfully");
|
|
1750
|
+
}
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
console.warn("Error revoking token:", error);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
async function fetchJwks() {
|
|
1756
|
+
if (!configWithDefaults) {
|
|
1757
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1758
|
+
}
|
|
1759
|
+
const url = configWithDefaults.jwks_uri || `${configWithDefaults.authority}/protocol/openid-connect/certs`;
|
|
1760
|
+
const cached = jwksCache.get(url);
|
|
1761
|
+
if (cached)
|
|
1762
|
+
return cached;
|
|
1763
|
+
return retryWithBackoff(async () => {
|
|
1764
|
+
if (!configWithDefaults)
|
|
1765
|
+
throw new Error("SSO Manager not initialized");
|
|
1766
|
+
const response = await fetch(url);
|
|
1767
|
+
if (!response.ok)
|
|
1768
|
+
throw new Error("Failed to fetch JWKS");
|
|
1769
|
+
const jwks = await response.json();
|
|
1770
|
+
jwksCache.set(url, jwks);
|
|
1771
|
+
return jwks;
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000, maxDelay = 30000) {
|
|
1775
|
+
let lastError = new Error("Placeholder Error");
|
|
1776
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
1777
|
+
try {
|
|
1778
|
+
return await operation();
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1781
|
+
if (error instanceof Error && error.message.includes("400")) {
|
|
1782
|
+
throw error;
|
|
1783
|
+
}
|
|
1784
|
+
if (attempt === maxRetries) {
|
|
1785
|
+
throw lastError;
|
|
1786
|
+
}
|
|
1787
|
+
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
1788
|
+
const jitter = Math.random() * 0.1 * delay;
|
|
1789
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
1790
|
+
console.warn(`Retry attempt ${attempt + 1} after ${delay + jitter}ms delay`);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
throw lastError;
|
|
1794
|
+
}
|
|
1795
|
+
async function parseJwt(token, validation) {
|
|
1796
|
+
try {
|
|
1797
|
+
const parts = token.split(".");
|
|
1798
|
+
if (parts.length !== 3)
|
|
1799
|
+
throw new Error("Invalid JWT");
|
|
1800
|
+
const header = JSON.parse(atob(parts[0].replace(/-/g, "+").replace(/_/g, "/")));
|
|
1801
|
+
const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
1802
|
+
const signature = parts[2].replace(/-/g, "+").replace(/_/g, "/");
|
|
1803
|
+
const publicKey = await getPublicKey(header.kid);
|
|
1804
|
+
const encoder = new TextEncoder;
|
|
1805
|
+
const data = encoder.encode(`${parts[0]}.${parts[1]}`);
|
|
1806
|
+
const isValid = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)), data);
|
|
1807
|
+
if (!isValid)
|
|
1808
|
+
throw new Error("Invalid JWT signature");
|
|
1809
|
+
const idTokenClaimsValidator = validation?.idTokenClaims ?? idTokenClaimsSchema("builtin");
|
|
1810
|
+
const claimsResult = await idTokenClaimsValidator["~standard"].validate(payload);
|
|
1811
|
+
if ("issues" in claimsResult) {
|
|
1812
|
+
console.error("ID token claims validation failed:", claimsResult.issues);
|
|
1813
|
+
throw new Error(`ID token claims validation failed: ${claimsResult.issues?.map((i) => i.message).join("; ")}`);
|
|
1814
|
+
}
|
|
1815
|
+
return claimsResult.value;
|
|
1816
|
+
} catch (e) {
|
|
1817
|
+
console.error("Error verifying JWT:", e);
|
|
1818
|
+
throw e;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
function generateRandomString(length = 32) {
|
|
1822
|
+
const array = new Uint8Array(length);
|
|
1823
|
+
crypto.getRandomValues(array);
|
|
1824
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("").substring(0, length);
|
|
1825
|
+
}
|
|
1826
|
+
async function pkceChallengeFromVerifier(verifier) {
|
|
1827
|
+
const encoder = new TextEncoder;
|
|
1828
|
+
const data = encoder.encode(verifier);
|
|
1829
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1830
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1831
|
+
const hashBase64 = btoa(String.fromCharCode(...hashArray)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1832
|
+
return hashBase64;
|
|
1833
|
+
}
|
|
1834
|
+
async function getPublicKey(kid) {
|
|
1835
|
+
const jwks = await fetchJwks();
|
|
1836
|
+
const key = jwks.keys.find((k) => k.kid === kid);
|
|
1837
|
+
if (!key)
|
|
1838
|
+
throw new Error("Public key not found");
|
|
1839
|
+
const publicKey = await crypto.subtle.importKey("jwk", {
|
|
1840
|
+
kty: key.kty,
|
|
1841
|
+
n: key.n,
|
|
1842
|
+
e: key.e
|
|
1843
|
+
}, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]);
|
|
1844
|
+
return publicKey;
|
|
1845
|
+
}
|
|
1846
|
+
function createJwtCookies(token, expires) {
|
|
1847
|
+
const control = {
|
|
1848
|
+
expires_in: token.expires_in,
|
|
1849
|
+
refresh_expires_in: token.refresh_expires_in,
|
|
1850
|
+
scope: token.scope,
|
|
1851
|
+
session_state: token.session_state,
|
|
1852
|
+
token_type: token.token_type,
|
|
1853
|
+
expires: expires.toISOString()
|
|
1854
|
+
};
|
|
1855
|
+
return [
|
|
1856
|
+
["Set-Cookie", createCookie("access", token.access_token, expires)],
|
|
1857
|
+
["Set-Cookie", createCookie("id", token.id_token, expires)],
|
|
1858
|
+
["Set-Cookie", createCookie("refresh", token.refresh_token ?? "", expires)],
|
|
1859
|
+
["Set-Cookie", createCookie("control", control, expires)]
|
|
1860
|
+
];
|
|
1861
|
+
}
|
|
1862
|
+
async function getTokenFromCookies(req) {
|
|
1863
|
+
const access_token = getCookie("access", req);
|
|
1864
|
+
const id_token = getCookie("id", req);
|
|
1865
|
+
const refresh_token = getCookie("refresh", req);
|
|
1866
|
+
const control = getCookie("control", req, true);
|
|
1867
|
+
if (!access_token || !id_token || !refresh_token || !control) {
|
|
1868
|
+
return { tokens: undefined, refreshHeaders: [] };
|
|
1869
|
+
}
|
|
1870
|
+
let tokenResponse = {
|
|
1871
|
+
access_token,
|
|
1872
|
+
id_token,
|
|
1873
|
+
refresh_token,
|
|
1874
|
+
...control
|
|
1875
|
+
};
|
|
1876
|
+
if (control.expires && refresh_token && Date.now() > new Date(control.expires).getTime()) {
|
|
1877
|
+
tokenResponse = await refreshToken(refresh_token);
|
|
1878
|
+
const user = await parseUser(tokenResponse);
|
|
1879
|
+
const refreshHeaders = createJwtCookies(tokenResponse, user.sso.expires);
|
|
1880
|
+
return { tokens: tokenResponse, refreshHeaders };
|
|
1881
|
+
}
|
|
1882
|
+
return { tokens: tokenResponse, refreshHeaders: [] };
|
|
1883
|
+
}
|
|
1884
|
+
async function getJwt(request) {
|
|
1885
|
+
const { tokens } = await getTokenFromCookies(request);
|
|
1886
|
+
if (!tokens)
|
|
1887
|
+
return;
|
|
1888
|
+
return tokens.access_token;
|
|
1889
|
+
}
|
|
1890
|
+
function createCookie(name, value, expires) {
|
|
1891
|
+
if (!configWithDefaults) {
|
|
1892
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1893
|
+
}
|
|
1894
|
+
name = `${configWithDefaults.cookies_prefix}.${name}`;
|
|
1895
|
+
if (typeof value !== "string") {
|
|
1896
|
+
value = btoa(JSON.stringify(value));
|
|
1897
|
+
}
|
|
1898
|
+
let exp;
|
|
1899
|
+
if (expires instanceof Date) {
|
|
1900
|
+
exp = `Expires=${expires.toUTCString()}`;
|
|
1901
|
+
} else if (typeof expires === "number") {
|
|
1902
|
+
exp = `Max-Age=${expires}`;
|
|
1903
|
+
} else {
|
|
1904
|
+
throw new Error("Invalid expires type", expires);
|
|
1905
|
+
}
|
|
1906
|
+
if (value.length > 4000) {
|
|
1907
|
+
throw new Error(`Error setting cookie: ${name}. Cookie length is: ${value.length}`);
|
|
1908
|
+
}
|
|
1909
|
+
return `${name}=${value}; ${exp}; Path=${configWithDefaults.cookies_path}; HttpOnly;${configWithDefaults.cookies_secure ? " Secure;" : ""} SameSite=${configWithDefaults.cookies_same_site};`;
|
|
1910
|
+
}
|
|
1911
|
+
function clearCookie(name) {
|
|
1912
|
+
if (!configWithDefaults) {
|
|
1913
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1914
|
+
}
|
|
1915
|
+
return `${configWithDefaults.cookies_prefix}.${name}=; Max-Age=0; Path=${configWithDefaults.cookies_path}; HttpOnly;${configWithDefaults.cookies_secure ? " Secure;" : ""} SameSite=${configWithDefaults.cookies_same_site};`;
|
|
1916
|
+
}
|
|
1917
|
+
function getCookie(name, req, parse = false) {
|
|
1918
|
+
if (!configWithDefaults) {
|
|
1919
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1920
|
+
}
|
|
1921
|
+
const header = req.headers.get("cookie");
|
|
1922
|
+
if (!header)
|
|
1923
|
+
return null;
|
|
1924
|
+
const cookie = header.split(";").find((row) => row.trim().startsWith(`${configWithDefaults.cookies_prefix}.${name}=`));
|
|
1925
|
+
if (!cookie)
|
|
1926
|
+
return null;
|
|
1927
|
+
const val = cookie.split("=")[1].trim();
|
|
1928
|
+
if (!parse)
|
|
1929
|
+
return val;
|
|
1930
|
+
const str = atob(val);
|
|
1931
|
+
return JSON.parse(str);
|
|
1932
|
+
}
|
|
1933
|
+
async function handler(request, handlerConfig) {
|
|
1934
|
+
const {
|
|
1935
|
+
loginUrl,
|
|
1936
|
+
userUrl,
|
|
1937
|
+
errorUrl,
|
|
1938
|
+
landingUrl,
|
|
1939
|
+
tokenUrl,
|
|
1940
|
+
refreshUrl,
|
|
1941
|
+
logoutUrl,
|
|
1942
|
+
logoutBackChannelUrl,
|
|
1943
|
+
jwksUrl,
|
|
1944
|
+
validation
|
|
1945
|
+
} = { ...handlerDefaults, ...handlerConfig };
|
|
1946
|
+
if (!configWithDefaults) {
|
|
1947
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1948
|
+
}
|
|
1949
|
+
if (!loginUrl) {
|
|
1950
|
+
console.error("loginUrl is required");
|
|
1951
|
+
}
|
|
1952
|
+
const path = new URL(request.url).pathname;
|
|
1953
|
+
let redirectUriPath;
|
|
1954
|
+
try {
|
|
1955
|
+
redirectUriPath = new URL(configWithDefaults.redirect_uri).pathname;
|
|
1956
|
+
} catch {
|
|
1957
|
+
try {
|
|
1958
|
+
const requestUrl = new URL(request.url);
|
|
1959
|
+
const redirectUri = configWithDefaults.redirect_uri.startsWith("//") ? configWithDefaults.redirect_uri.slice(1) : configWithDefaults.redirect_uri;
|
|
1960
|
+
redirectUriPath = new URL(redirectUri, requestUrl.origin).pathname;
|
|
1961
|
+
} catch {
|
|
1962
|
+
redirectUriPath = configWithDefaults.redirect_uri.startsWith("/") ? configWithDefaults.redirect_uri : `/${configWithDefaults.redirect_uri}`;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
if (redirectUriPath === path) {
|
|
1966
|
+
return callbackHandler(request, validation);
|
|
1967
|
+
}
|
|
1968
|
+
if (loginUrl === path) {
|
|
1969
|
+
return initiateLogin({
|
|
1970
|
+
landingUrl: landingUrl || "/",
|
|
1971
|
+
errorUrl
|
|
1972
|
+
}, request.url);
|
|
1973
|
+
}
|
|
1974
|
+
if (userUrl === path) {
|
|
1975
|
+
const { tokens, refreshHeaders } = await getTokenFromCookies(request);
|
|
1976
|
+
if (!tokens) {
|
|
1977
|
+
return new Response("User not logged in", { status: 401 });
|
|
1978
|
+
}
|
|
1979
|
+
const user = await parseUser(tokens);
|
|
1980
|
+
return new Response(JSON.stringify(user), {
|
|
1981
|
+
headers: [["Content-Type", "application/json"], ...refreshHeaders]
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
if (tokenUrl === path) {
|
|
1985
|
+
const { tokens, refreshHeaders } = await getTokenFromCookies(request);
|
|
1986
|
+
if (!tokens) {
|
|
1987
|
+
return new Response("User not logged in", { status: 401 });
|
|
1988
|
+
}
|
|
1989
|
+
return new Response(JSON.stringify({
|
|
1990
|
+
token: tokens.access_token,
|
|
1991
|
+
expires: tokens.expires
|
|
1992
|
+
}), {
|
|
1993
|
+
headers: [["Content-Type", "application/json"], ...refreshHeaders]
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
if (refreshUrl === path) {
|
|
1997
|
+
const refresh_token = getCookie("refresh", request);
|
|
1998
|
+
if (!refresh_token) {
|
|
1999
|
+
return new Response("User not logged in", { status: 401 });
|
|
2000
|
+
}
|
|
2001
|
+
const newTokenResponse = await refreshToken(refresh_token);
|
|
2002
|
+
const user = await parseUser(newTokenResponse);
|
|
2003
|
+
const refreshHeaders = createJwtCookies(newTokenResponse, user.sso.expires);
|
|
2004
|
+
return new Response("Refresh Complete", {
|
|
2005
|
+
status: 200,
|
|
2006
|
+
headers: refreshHeaders
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
if (logoutUrl === path) {
|
|
2010
|
+
return logout(request, { landingUrl: landingUrl || "/" });
|
|
2011
|
+
}
|
|
2012
|
+
if (logoutBackChannelUrl === path) {
|
|
2013
|
+
return logoutBackChannel(request);
|
|
2014
|
+
}
|
|
2015
|
+
if (jwksUrl === path) {
|
|
2016
|
+
const jwks = await fetchJwks();
|
|
2017
|
+
return new Response(JSON.stringify(jwks), {
|
|
2018
|
+
headers: [["Content-Type", "application/json"]]
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
return new Response("Not Found", { status: 404 });
|
|
2022
|
+
}
|
|
2023
|
+
if (!configWithDefaults) {
|
|
2024
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
...configWithDefaults,
|
|
2028
|
+
getUser,
|
|
2029
|
+
getRequiredUser,
|
|
2030
|
+
getJwt,
|
|
2031
|
+
initiateLogin,
|
|
2032
|
+
logout,
|
|
2033
|
+
callbackHandler,
|
|
2034
|
+
handler
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// packages/react/src/vault.ts
|
|
2039
|
+
function vault(url) {
|
|
2040
|
+
async function getFullSecret(path, token) {
|
|
2041
|
+
const resp = await fetch(`${url}/${path}`, { headers: { "X-Vault-Token": token } });
|
|
2042
|
+
if (resp.status !== 200) {
|
|
2043
|
+
throw new Error(`Vault returned invalid status, ${resp.status}: '${resp.statusText}' from URL: ${url}`);
|
|
2044
|
+
}
|
|
2045
|
+
try {
|
|
2046
|
+
const secret = await resp.json();
|
|
2047
|
+
return secret.data;
|
|
2048
|
+
} catch (cause) {
|
|
2049
|
+
throw new Error("Error retrieving secret", { cause });
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return {
|
|
2053
|
+
url,
|
|
2054
|
+
getFullSecret,
|
|
2055
|
+
getSecret: async (path, token) => {
|
|
2056
|
+
return (await getFullSecret(path, token)).data;
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// packages/react/src/types/workload-schema.ts
|
|
2062
|
+
function jwtAssertionClaimsSchema(vendor) {
|
|
2063
|
+
return {
|
|
2064
|
+
"~standard": {
|
|
2065
|
+
version: 1,
|
|
2066
|
+
vendor,
|
|
2067
|
+
validate: (value) => {
|
|
2068
|
+
if (typeof value !== "object" || value === null) {
|
|
2069
|
+
return {
|
|
2070
|
+
issues: [
|
|
2071
|
+
{
|
|
2072
|
+
message: "Expected an object"
|
|
2073
|
+
}
|
|
2074
|
+
]
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
const claims = value;
|
|
2078
|
+
const issues = [];
|
|
2079
|
+
const result = { ...claims };
|
|
2080
|
+
const requiredStringFields = ["iss", "sub"];
|
|
2081
|
+
for (const field of requiredStringFields) {
|
|
2082
|
+
if (field in claims) {
|
|
2083
|
+
if (typeof claims[field] !== "string") {
|
|
2084
|
+
issues.push({
|
|
2085
|
+
message: `${field} must be a string`,
|
|
2086
|
+
path: [field]
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
} else {
|
|
2090
|
+
issues.push({
|
|
2091
|
+
message: `${field} is required`,
|
|
2092
|
+
path: [field]
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
if ("aud" in claims && claims.aud !== undefined) {
|
|
2097
|
+
const aud = claims.aud;
|
|
2098
|
+
if (typeof aud !== "string" && !Array.isArray(aud)) {
|
|
2099
|
+
issues.push({
|
|
2100
|
+
message: "aud must be a string or array of strings",
|
|
2101
|
+
path: ["aud"]
|
|
2102
|
+
});
|
|
2103
|
+
} else if (Array.isArray(aud) && !aud.every((a) => typeof a === "string")) {
|
|
2104
|
+
issues.push({
|
|
2105
|
+
message: "aud array must contain only strings",
|
|
2106
|
+
path: ["aud"]
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
const optionalStringFields = ["jti", "scope"];
|
|
2111
|
+
for (const field of optionalStringFields) {
|
|
2112
|
+
if (field in claims && claims[field] !== undefined) {
|
|
2113
|
+
if (typeof claims[field] !== "string") {
|
|
2114
|
+
issues.push({
|
|
2115
|
+
message: `${field} must be a string`,
|
|
2116
|
+
path: [field]
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
const requiredNumberFields = ["exp", "iat"];
|
|
2122
|
+
for (const field of requiredNumberFields) {
|
|
2123
|
+
if (field in claims) {
|
|
2124
|
+
if (typeof claims[field] !== "number") {
|
|
2125
|
+
issues.push({
|
|
2126
|
+
message: `${field} must be a number`,
|
|
2127
|
+
path: [field]
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
} else {
|
|
2131
|
+
issues.push({
|
|
2132
|
+
message: `${field} is required`,
|
|
2133
|
+
path: [field]
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (issues.length > 0) {
|
|
2138
|
+
return { issues };
|
|
2139
|
+
}
|
|
2140
|
+
return { value: result };
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
function workloadTokenResponseSchema(vendor) {
|
|
2146
|
+
return {
|
|
2147
|
+
"~standard": {
|
|
2148
|
+
version: 1,
|
|
2149
|
+
vendor,
|
|
2150
|
+
validate: (value) => {
|
|
2151
|
+
if (typeof value !== "object" || value === null) {
|
|
2152
|
+
return {
|
|
2153
|
+
issues: [
|
|
2154
|
+
{
|
|
2155
|
+
message: "Expected an object"
|
|
2156
|
+
}
|
|
2157
|
+
]
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
const response = value;
|
|
2161
|
+
const issues = [];
|
|
2162
|
+
const result = {};
|
|
2163
|
+
if ("access_token" in response) {
|
|
2164
|
+
if (typeof response.access_token === "string") {
|
|
2165
|
+
result.access_token = response.access_token;
|
|
2166
|
+
} else {
|
|
2167
|
+
issues.push({
|
|
2168
|
+
message: "access_token must be a string",
|
|
2169
|
+
path: ["access_token"]
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
} else {
|
|
2173
|
+
issues.push({
|
|
2174
|
+
message: "access_token is required",
|
|
2175
|
+
path: ["access_token"]
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
if ("token_type" in response) {
|
|
2179
|
+
if (typeof response.token_type === "string") {
|
|
2180
|
+
result.token_type = response.token_type;
|
|
2181
|
+
} else {
|
|
2182
|
+
issues.push({
|
|
2183
|
+
message: "token_type must be a string",
|
|
2184
|
+
path: ["token_type"]
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
} else {
|
|
2188
|
+
issues.push({
|
|
2189
|
+
message: "token_type is required",
|
|
2190
|
+
path: ["token_type"]
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
if ("scope" in response) {
|
|
2194
|
+
if (typeof response.scope === "string" || response.scope === undefined) {
|
|
2195
|
+
result.scope = response.scope;
|
|
2196
|
+
} else {
|
|
2197
|
+
issues.push({
|
|
2198
|
+
message: "scope must be a string",
|
|
2199
|
+
path: ["scope"]
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
if ("refresh_token" in response) {
|
|
2204
|
+
if (typeof response.refresh_token === "string" || response.refresh_token === undefined) {
|
|
2205
|
+
result.refresh_token = response.refresh_token;
|
|
2206
|
+
} else {
|
|
2207
|
+
issues.push({
|
|
2208
|
+
message: "refresh_token must be a string",
|
|
2209
|
+
path: ["refresh_token"]
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
if ("expires" in response) {
|
|
2214
|
+
if (typeof response.expires === "string" || response.expires === undefined) {
|
|
2215
|
+
result.expires = response.expires;
|
|
2216
|
+
} else {
|
|
2217
|
+
issues.push({
|
|
2218
|
+
message: "expires must be a string",
|
|
2219
|
+
path: ["expires"]
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
if ("expires_in" in response) {
|
|
2224
|
+
if (typeof response.expires_in === "number" || response.expires_in === undefined) {
|
|
2225
|
+
result.expires_in = response.expires_in;
|
|
2226
|
+
} else {
|
|
2227
|
+
issues.push({
|
|
2228
|
+
message: "expires_in must be a number",
|
|
2229
|
+
path: ["expires_in"]
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (issues.length > 0) {
|
|
2234
|
+
return { issues };
|
|
2235
|
+
}
|
|
2236
|
+
return { value: result };
|
|
548
2237
|
}
|
|
549
|
-
console.warn("No error page was found in the cookies. The user will be shown a default error page.");
|
|
550
|
-
return new Response("An error occurred during authentication, please return to the application homepage and try again.", {
|
|
551
|
-
status: 500
|
|
552
|
-
});
|
|
553
2238
|
}
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// packages/react/src/workload-token-store.ts
|
|
2243
|
+
class InMemoryWorkloadTokenStore {
|
|
2244
|
+
tokens = new Map;
|
|
2245
|
+
async set(token) {
|
|
2246
|
+
this.tokens.set(token.workload_id, token);
|
|
554
2247
|
}
|
|
555
|
-
async
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
2248
|
+
async get(workload_id) {
|
|
2249
|
+
const token = this.tokens.get(workload_id);
|
|
2250
|
+
if (!token)
|
|
2251
|
+
return null;
|
|
2252
|
+
if (Date.now() > token.expires_at.getTime()) {
|
|
2253
|
+
this.tokens.delete(workload_id);
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
return token;
|
|
2257
|
+
}
|
|
2258
|
+
async delete(workload_id) {
|
|
2259
|
+
this.tokens.delete(workload_id);
|
|
2260
|
+
}
|
|
2261
|
+
async isValid(workload_id) {
|
|
2262
|
+
const token = await this.get(workload_id);
|
|
2263
|
+
return token !== null;
|
|
2264
|
+
}
|
|
2265
|
+
async cleanup() {
|
|
2266
|
+
const now = Date.now();
|
|
2267
|
+
for (const [workload_id, token] of this.tokens.entries()) {
|
|
2268
|
+
if (now > token.expires_at.getTime()) {
|
|
2269
|
+
this.tokens.delete(workload_id);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// packages/react/src/workload.ts
|
|
2276
|
+
var jwksCache2 = new Map;
|
|
2277
|
+
function isJwtBearerConfig(config) {
|
|
2278
|
+
return "workload_id" in config || "private_key" in config;
|
|
2279
|
+
}
|
|
2280
|
+
function isClientCredentialsConfig(config) {
|
|
2281
|
+
return "client_id" in config || "client_secret" in config;
|
|
2282
|
+
}
|
|
2283
|
+
function isServerOnlyConfig(config) {
|
|
2284
|
+
return "jwks_uri" in config && !("workload_id" in config) && !("client_id" in config);
|
|
2285
|
+
}
|
|
2286
|
+
function validateWorkloadConfig(config) {
|
|
2287
|
+
const hasJwtBearer = "workload_id" in config && "private_key" in config;
|
|
2288
|
+
const hasClientCreds = "client_id" in config && "client_secret" in config;
|
|
2289
|
+
const hasServerOnly = "jwks_uri" in config;
|
|
2290
|
+
if (!hasJwtBearer && !hasClientCreds && !hasServerOnly) {
|
|
2291
|
+
throw new Error("Invalid WorkloadConfig: must provide either (workload_id + private_key) for JWT Bearer Grant, " + "(client_id + client_secret) for OAuth2 Client Credentials, or (jwks_uri) for server-only token validation");
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function workload(config) {
|
|
2295
|
+
validateWorkloadConfig(config);
|
|
2296
|
+
let configWithDefaults;
|
|
2297
|
+
if (isJwtBearerConfig(config)) {
|
|
2298
|
+
configWithDefaults = {
|
|
2299
|
+
...config,
|
|
2300
|
+
token_url: must(config.token_url, "Missing 'token_url' from Workload Config"),
|
|
2301
|
+
workload_id: must(config.workload_id, "Missing 'workload_id' from Workload Config"),
|
|
2302
|
+
audience: must(config.audience, "Missing 'audience' from Workload Config"),
|
|
2303
|
+
scope: config.scope ?? "",
|
|
2304
|
+
algorithm: config.algorithm ?? "RS256",
|
|
2305
|
+
token_lifetime: config.token_lifetime ?? 300,
|
|
2306
|
+
refresh_threshold: config.refresh_threshold ?? 60,
|
|
2307
|
+
auto_refresh: config.auto_refresh !== undefined ? config.auto_refresh : true,
|
|
2308
|
+
token_store: config.token_store ?? new InMemoryWorkloadTokenStore
|
|
2309
|
+
};
|
|
2310
|
+
} else if (isClientCredentialsConfig(config)) {
|
|
2311
|
+
configWithDefaults = {
|
|
2312
|
+
...config,
|
|
2313
|
+
token_url: must(config.token_url, "Missing 'token_url' from Workload Config"),
|
|
2314
|
+
client_id: must(config.client_id, "Missing 'client_id' from Workload Config"),
|
|
2315
|
+
client_secret: must(config.client_secret, "Missing 'client_secret' from Workload Config"),
|
|
2316
|
+
scope: config.scope ?? "",
|
|
2317
|
+
token_lifetime: config.token_lifetime ?? 300,
|
|
2318
|
+
refresh_threshold: config.refresh_threshold ?? 60,
|
|
2319
|
+
auto_refresh: config.auto_refresh !== undefined ? config.auto_refresh : true,
|
|
2320
|
+
token_store: config.token_store ?? new InMemoryWorkloadTokenStore
|
|
2321
|
+
};
|
|
2322
|
+
} else {
|
|
2323
|
+
configWithDefaults = config;
|
|
2324
|
+
}
|
|
2325
|
+
const initialized = true;
|
|
2326
|
+
function ensureInitialized() {
|
|
2327
|
+
if (!initialized) {
|
|
2328
|
+
throw new Error("Enterprise Standard Workload Manager not initialized");
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
function generateJTI() {
|
|
2332
|
+
const array = new Uint8Array(16);
|
|
2333
|
+
crypto.getRandomValues(array);
|
|
2334
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2335
|
+
}
|
|
2336
|
+
function base64UrlEncode(data) {
|
|
2337
|
+
let base64;
|
|
2338
|
+
if (typeof data === "string") {
|
|
2339
|
+
base64 = btoa(data);
|
|
2340
|
+
} else {
|
|
2341
|
+
base64 = btoa(String.fromCharCode(...data));
|
|
2342
|
+
}
|
|
2343
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2344
|
+
}
|
|
2345
|
+
function base64UrlDecode(base64url) {
|
|
2346
|
+
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
2347
|
+
while (base64.length % 4) {
|
|
2348
|
+
base64 += "=";
|
|
2349
|
+
}
|
|
2350
|
+
return atob(base64);
|
|
2351
|
+
}
|
|
2352
|
+
function getAlgorithmParams(alg) {
|
|
2353
|
+
if (alg.startsWith("RS")) {
|
|
2354
|
+
const hash = alg === "RS256" ? "SHA-256" : alg === "RS384" ? "SHA-384" : "SHA-512";
|
|
2355
|
+
return { name: "RSASSA-PKCS1-v1_5", hash };
|
|
2356
|
+
} else if (alg.startsWith("ES")) {
|
|
2357
|
+
const namedCurve = alg === "ES256" ? "P-256" : alg === "ES384" ? "P-384" : "P-521";
|
|
2358
|
+
const hash = alg === "ES256" ? "SHA-256" : alg === "ES384" ? "SHA-384" : "SHA-512";
|
|
2359
|
+
return { name: "ECDSA", namedCurve, hash };
|
|
2360
|
+
}
|
|
2361
|
+
throw new Error(`Unsupported algorithm: ${alg}`);
|
|
2362
|
+
}
|
|
2363
|
+
async function importPrivateKey(pemKey, algorithm) {
|
|
2364
|
+
const pemContents = pemKey.replace(/-----BEGIN PRIVATE KEY-----/, "").replace(/-----END PRIVATE KEY-----/, "").replace(/\s/g, "");
|
|
2365
|
+
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
2366
|
+
const algorithmParams = getAlgorithmParams(algorithm);
|
|
2367
|
+
return crypto.subtle.importKey("pkcs8", binaryDer, algorithmParams, false, ["sign"]);
|
|
2368
|
+
}
|
|
2369
|
+
async function signJWT(data, privateKeyPEM, algorithm) {
|
|
2370
|
+
const privateKey = await importPrivateKey(privateKeyPEM, algorithm);
|
|
2371
|
+
const encoder = new TextEncoder;
|
|
2372
|
+
const dataBuffer = encoder.encode(data);
|
|
2373
|
+
const algorithmParams = getAlgorithmParams(algorithm);
|
|
2374
|
+
const signatureBuffer = await crypto.subtle.sign(algorithmParams, privateKey, dataBuffer);
|
|
2375
|
+
return base64UrlEncode(new Uint8Array(signatureBuffer));
|
|
2376
|
+
}
|
|
2377
|
+
async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000, maxDelay = 30000) {
|
|
2378
|
+
let lastError = new Error("Placeholder Error");
|
|
2379
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
2380
|
+
try {
|
|
2381
|
+
return await operation();
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2384
|
+
if (lastError.message.includes("400") || lastError.message.includes("401") || lastError.message.includes("403") || lastError.message.includes("404")) {
|
|
2385
|
+
throw lastError;
|
|
2386
|
+
}
|
|
2387
|
+
if (attempt < maxRetries) {
|
|
2388
|
+
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
2389
|
+
const jitter = Math.random() * delay * 0.1;
|
|
2390
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
570
2391
|
}
|
|
571
|
-
],
|
|
572
|
-
avatarUrl: idToken.picture,
|
|
573
|
-
sso: {
|
|
574
|
-
profile: {
|
|
575
|
-
...idToken,
|
|
576
|
-
iss: idToken.iss || configWithDefaults.authority,
|
|
577
|
-
aud: idToken.aud || configWithDefaults.client_id
|
|
578
|
-
},
|
|
579
|
-
tenant: {
|
|
580
|
-
id: idToken.idp || idToken.iss || configWithDefaults.authority,
|
|
581
|
-
name: idToken.iss || configWithDefaults.authority
|
|
582
|
-
},
|
|
583
|
-
scope: token.scope,
|
|
584
|
-
tokenType: token.token_type,
|
|
585
|
-
sessionState: token.session_state,
|
|
586
|
-
expires
|
|
587
2392
|
}
|
|
2393
|
+
}
|
|
2394
|
+
throw lastError;
|
|
2395
|
+
}
|
|
2396
|
+
async function generateJWTAssertion(scope) {
|
|
2397
|
+
ensureInitialized();
|
|
2398
|
+
if (!isJwtBearerConfig(config)) {
|
|
2399
|
+
throw new Error("generateJWTAssertion is only available in JWT Bearer Grant mode");
|
|
2400
|
+
}
|
|
2401
|
+
const cfg = configWithDefaults;
|
|
2402
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2403
|
+
const claims = {
|
|
2404
|
+
iss: cfg.workload_id,
|
|
2405
|
+
sub: cfg.workload_id,
|
|
2406
|
+
aud: cfg.audience ?? "",
|
|
2407
|
+
exp: now + cfg.token_lifetime,
|
|
2408
|
+
iat: now,
|
|
2409
|
+
jti: generateJTI(),
|
|
2410
|
+
scope: scope ?? cfg.scope
|
|
588
2411
|
};
|
|
2412
|
+
const header = {
|
|
2413
|
+
alg: cfg.algorithm,
|
|
2414
|
+
typ: "JWT",
|
|
2415
|
+
kid: cfg.key_id
|
|
2416
|
+
};
|
|
2417
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
2418
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(claims));
|
|
2419
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
2420
|
+
const signature = await signJWT(signatureInput, cfg.private_key, cfg.algorithm);
|
|
2421
|
+
return `${signatureInput}.${signature}`;
|
|
589
2422
|
}
|
|
590
|
-
async function
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
try {
|
|
2423
|
+
async function acquireTokenJwtBearer(scope, validation) {
|
|
2424
|
+
const cfg = configWithDefaults;
|
|
2425
|
+
return retryWithBackoff(async () => {
|
|
2426
|
+
const tokenUrl = cfg.token_url;
|
|
2427
|
+
const assertion = await generateJWTAssertion(scope);
|
|
2428
|
+
const body = new URLSearchParams;
|
|
2429
|
+
body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
|
|
2430
|
+
body.append("assertion", assertion);
|
|
2431
|
+
if (scope)
|
|
2432
|
+
body.append("scope", scope);
|
|
601
2433
|
const response = await fetch(tokenUrl, {
|
|
602
2434
|
method: "POST",
|
|
603
2435
|
headers: {
|
|
@@ -608,30 +2440,41 @@ function sso(config) {
|
|
|
608
2440
|
});
|
|
609
2441
|
const data = await response.json();
|
|
610
2442
|
if (!response.ok) {
|
|
611
|
-
console.error("Token
|
|
612
|
-
throw new Error(`Token
|
|
2443
|
+
console.error("Token acquisition error:", data);
|
|
2444
|
+
throw new Error(`Token acquisition failed: ${data.error || response.statusText} - ${data.error_description || ""}`.trim());
|
|
613
2445
|
}
|
|
614
|
-
const
|
|
615
|
-
const
|
|
616
|
-
if ("issues" in
|
|
617
|
-
console.error("Token response validation failed:",
|
|
618
|
-
throw new Error(`Token response validation failed: ${
|
|
2446
|
+
const validator = validation?.tokenResponse ?? workloadTokenResponseSchema("builtin");
|
|
2447
|
+
const result = await validator["~standard"].validate(data);
|
|
2448
|
+
if ("issues" in result) {
|
|
2449
|
+
console.error("Token response validation failed:", result.issues);
|
|
2450
|
+
throw new Error(`Token response validation failed: ${result.issues?.map((i) => i.message).join("; ")}`);
|
|
619
2451
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
2452
|
+
if (cfg.token_store) {
|
|
2453
|
+
const expiresAt = new Date(Date.now() + (result.value.expires_in ?? 300) * 1000);
|
|
2454
|
+
const cachedToken = {
|
|
2455
|
+
workload_id: cfg.workload_id,
|
|
2456
|
+
access_token: result.value.access_token,
|
|
2457
|
+
token_type: result.value.token_type,
|
|
2458
|
+
scope: result.value.scope,
|
|
2459
|
+
expires_at: expiresAt,
|
|
2460
|
+
created_at: new Date,
|
|
2461
|
+
refresh_token: result.value.refresh_token
|
|
2462
|
+
};
|
|
2463
|
+
await cfg.token_store.set(cachedToken);
|
|
2464
|
+
}
|
|
2465
|
+
return result.value;
|
|
2466
|
+
});
|
|
625
2467
|
}
|
|
626
|
-
async function
|
|
2468
|
+
async function acquireTokenClientCredentials(scope, validation) {
|
|
2469
|
+
const cfg = configWithDefaults;
|
|
627
2470
|
return retryWithBackoff(async () => {
|
|
628
|
-
|
|
629
|
-
throw new Error("SSO Manager not initialized");
|
|
630
|
-
const tokenUrl = configWithDefaults.token_url;
|
|
2471
|
+
const tokenUrl = cfg.token_url;
|
|
631
2472
|
const body = new URLSearchParams;
|
|
632
|
-
body.append("grant_type", "
|
|
633
|
-
body.append("
|
|
634
|
-
body.append("
|
|
2473
|
+
body.append("grant_type", "client_credentials");
|
|
2474
|
+
body.append("client_id", cfg.client_id);
|
|
2475
|
+
body.append("client_secret", cfg.client_secret);
|
|
2476
|
+
if (scope)
|
|
2477
|
+
body.append("scope", scope);
|
|
635
2478
|
const response = await fetch(tokenUrl, {
|
|
636
2479
|
method: "POST",
|
|
637
2480
|
headers: {
|
|
@@ -642,24 +2485,110 @@ function sso(config) {
|
|
|
642
2485
|
});
|
|
643
2486
|
const data = await response.json();
|
|
644
2487
|
if (!response.ok) {
|
|
645
|
-
console.error("Token
|
|
646
|
-
throw new Error(`Token
|
|
2488
|
+
console.error("Token acquisition error:", data);
|
|
2489
|
+
throw new Error(`Token acquisition failed: ${data.error || response.statusText} - ${data.error_description || ""}`.trim());
|
|
647
2490
|
}
|
|
648
|
-
|
|
2491
|
+
const validator = validation?.tokenResponse ?? workloadTokenResponseSchema("builtin");
|
|
2492
|
+
const result = await validator["~standard"].validate(data);
|
|
2493
|
+
if ("issues" in result) {
|
|
2494
|
+
console.error("Token response validation failed:", result.issues);
|
|
2495
|
+
throw new Error(`Token response validation failed: ${result.issues?.map((i) => i.message).join("; ")}`);
|
|
2496
|
+
}
|
|
2497
|
+
if (cfg.token_store) {
|
|
2498
|
+
const expiresAt = new Date(Date.now() + (result.value.expires_in ?? 300) * 1000);
|
|
2499
|
+
const cachedToken = {
|
|
2500
|
+
workload_id: cfg.client_id,
|
|
2501
|
+
access_token: result.value.access_token,
|
|
2502
|
+
token_type: result.value.token_type,
|
|
2503
|
+
scope: result.value.scope,
|
|
2504
|
+
expires_at: expiresAt,
|
|
2505
|
+
created_at: new Date,
|
|
2506
|
+
refresh_token: result.value.refresh_token
|
|
2507
|
+
};
|
|
2508
|
+
await cfg.token_store.set(cachedToken);
|
|
2509
|
+
}
|
|
2510
|
+
return result.value;
|
|
649
2511
|
});
|
|
650
2512
|
}
|
|
2513
|
+
async function getToken(scope) {
|
|
2514
|
+
ensureInitialized();
|
|
2515
|
+
if (isServerOnlyConfig(config)) {
|
|
2516
|
+
throw new Error("Cannot acquire tokens: Workload is configured in server-only mode (validation only). " + "To acquire tokens, configure client_id + client_secret for OAuth2 Client Credentials, " + "or workload_id + private_key for JWT Bearer Grant.");
|
|
2517
|
+
}
|
|
2518
|
+
if (!configWithDefaults.token_url) {
|
|
2519
|
+
throw new Error("Cannot acquire tokens: Missing token_url in WorkloadConfig. " + "Client role requires token_url to be configured in vault.");
|
|
2520
|
+
}
|
|
2521
|
+
if (isJwtBearerConfig(configWithDefaults)) {
|
|
2522
|
+
if (!configWithDefaults.private_key) {
|
|
2523
|
+
throw new Error("Cannot acquire tokens: Missing private_key in WorkloadConfig. " + "JWT Bearer Grant client role requires private_key to be configured in vault.");
|
|
2524
|
+
}
|
|
2525
|
+
} else if (isClientCredentialsConfig(configWithDefaults)) {
|
|
2526
|
+
if (!configWithDefaults.client_secret) {
|
|
2527
|
+
throw new Error("Cannot acquire tokens: Missing client_secret in WorkloadConfig. " + "OAuth2 Client Credentials client role requires client_secret to be configured in vault.");
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
const requestedScope = scope ?? configWithDefaults.scope ?? "";
|
|
2531
|
+
let cacheKey;
|
|
2532
|
+
if (isJwtBearerConfig(configWithDefaults)) {
|
|
2533
|
+
cacheKey = configWithDefaults.workload_id;
|
|
2534
|
+
} else if (isClientCredentialsConfig(configWithDefaults)) {
|
|
2535
|
+
cacheKey = configWithDefaults.client_id;
|
|
2536
|
+
} else {
|
|
2537
|
+
throw new Error("Invalid WorkloadConfig");
|
|
2538
|
+
}
|
|
2539
|
+
const cfg = configWithDefaults;
|
|
2540
|
+
if (cfg.token_store) {
|
|
2541
|
+
const cachedToken = await cfg.token_store.get(cacheKey);
|
|
2542
|
+
if (cachedToken) {
|
|
2543
|
+
const now = Date.now();
|
|
2544
|
+
const expiresAt = cachedToken.expires_at.getTime();
|
|
2545
|
+
const refreshThreshold = cfg.refresh_threshold * 1000;
|
|
2546
|
+
if (now + refreshThreshold < expiresAt) {
|
|
2547
|
+
return cachedToken.access_token;
|
|
2548
|
+
}
|
|
2549
|
+
if (cfg.auto_refresh) {
|
|
2550
|
+
try {
|
|
2551
|
+
const newToken = isJwtBearerConfig(config) ? await acquireTokenJwtBearer(requestedScope) : await acquireTokenClientCredentials(requestedScope);
|
|
2552
|
+
return newToken.access_token;
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
if (now < expiresAt) {
|
|
2555
|
+
console.warn("Token refresh failed, using cached token:", error);
|
|
2556
|
+
return cachedToken.access_token;
|
|
2557
|
+
}
|
|
2558
|
+
throw error;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
const tokenResponse = isJwtBearerConfig(config) ? await acquireTokenJwtBearer(requestedScope) : await acquireTokenClientCredentials(requestedScope);
|
|
2564
|
+
return tokenResponse.access_token;
|
|
2565
|
+
}
|
|
2566
|
+
async function refreshToken() {
|
|
2567
|
+
ensureInitialized();
|
|
2568
|
+
if (isServerOnlyConfig(config)) {
|
|
2569
|
+
throw new Error("Cannot refresh tokens: Workload is configured in server-only mode (validation only).");
|
|
2570
|
+
}
|
|
2571
|
+
const cfg = configWithDefaults;
|
|
2572
|
+
return isJwtBearerConfig(cfg) ? await acquireTokenJwtBearer(cfg.scope) : await acquireTokenClientCredentials(cfg.scope);
|
|
2573
|
+
}
|
|
651
2574
|
async function revokeToken(token) {
|
|
2575
|
+
ensureInitialized();
|
|
652
2576
|
try {
|
|
653
|
-
if (!
|
|
654
|
-
throw new Error("SSO Manager not initialized");
|
|
655
|
-
if (!configWithDefaults.revocation_endpoint) {
|
|
2577
|
+
if (!config.revocation_endpoint) {
|
|
656
2578
|
return;
|
|
657
2579
|
}
|
|
658
2580
|
const body = new URLSearchParams;
|
|
659
2581
|
body.append("token", token);
|
|
660
|
-
body.append("token_type_hint", "
|
|
661
|
-
|
|
662
|
-
|
|
2582
|
+
body.append("token_type_hint", "access_token");
|
|
2583
|
+
if (isJwtBearerConfig(config)) {
|
|
2584
|
+
const cfg = configWithDefaults;
|
|
2585
|
+
body.append("client_id", cfg.workload_id);
|
|
2586
|
+
} else if (isClientCredentialsConfig(config)) {
|
|
2587
|
+
const cfg = configWithDefaults;
|
|
2588
|
+
body.append("client_id", cfg.client_id);
|
|
2589
|
+
body.append("client_secret", cfg.client_secret);
|
|
2590
|
+
}
|
|
2591
|
+
const response = await fetch(config.revocation_endpoint, {
|
|
663
2592
|
method: "POST",
|
|
664
2593
|
headers: {
|
|
665
2594
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
@@ -671,273 +2600,498 @@ function sso(config) {
|
|
|
671
2600
|
} else {
|
|
672
2601
|
console.log("Token revoked successfully");
|
|
673
2602
|
}
|
|
2603
|
+
if (config.token_store) {
|
|
2604
|
+
let cacheKey;
|
|
2605
|
+
if (isJwtBearerConfig(config)) {
|
|
2606
|
+
cacheKey = configWithDefaults.workload_id;
|
|
2607
|
+
} else if (isClientCredentialsConfig(config)) {
|
|
2608
|
+
cacheKey = configWithDefaults.client_id;
|
|
2609
|
+
} else {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
await config.token_store.delete(cacheKey);
|
|
2613
|
+
}
|
|
674
2614
|
} catch (error) {
|
|
675
2615
|
console.warn("Error revoking token:", error);
|
|
676
2616
|
}
|
|
677
2617
|
}
|
|
678
2618
|
async function fetchJwks() {
|
|
679
|
-
|
|
680
|
-
const
|
|
2619
|
+
ensureInitialized();
|
|
2620
|
+
const url = config.jwks_uri;
|
|
2621
|
+
if (!url) {
|
|
2622
|
+
throw new Error("Cannot validate tokens: Missing jwks_uri in WorkloadConfig. " + "Server role requires jwks_uri to be configured in vault to fetch public keys for token validation.");
|
|
2623
|
+
}
|
|
2624
|
+
const cached = jwksCache2.get(url);
|
|
681
2625
|
if (cached)
|
|
682
2626
|
return cached;
|
|
683
2627
|
return retryWithBackoff(async () => {
|
|
684
|
-
if (!configWithDefaults)
|
|
685
|
-
throw new Error("SSO Manager not initialized");
|
|
686
2628
|
const response = await fetch(url);
|
|
687
2629
|
if (!response.ok)
|
|
688
2630
|
throw new Error("Failed to fetch JWKS");
|
|
689
2631
|
const jwks = await response.json();
|
|
690
|
-
|
|
2632
|
+
jwksCache2.set(url, jwks);
|
|
691
2633
|
return jwks;
|
|
692
2634
|
});
|
|
693
2635
|
}
|
|
694
|
-
async function
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
throw error;
|
|
703
|
-
}
|
|
704
|
-
if (attempt === maxRetries) {
|
|
705
|
-
throw lastError;
|
|
706
|
-
}
|
|
707
|
-
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
708
|
-
const jitter = Math.random() * 0.1 * delay;
|
|
709
|
-
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
710
|
-
console.warn(`Retry attempt ${attempt + 1} after ${delay + jitter}ms delay`);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
throw lastError;
|
|
2636
|
+
async function getPublicKey(kid) {
|
|
2637
|
+
const jwks = await fetchJwks();
|
|
2638
|
+
const key = jwks.keys.find((k) => k.kid === kid);
|
|
2639
|
+
if (!key)
|
|
2640
|
+
throw new Error("Public key not found");
|
|
2641
|
+
const defaultAlg = isJwtBearerConfig(config) ? configWithDefaults.algorithm : "RS256";
|
|
2642
|
+
const algorithmParams = getAlgorithmParams(key.alg || defaultAlg);
|
|
2643
|
+
return crypto.subtle.importKey("jwk", key, algorithmParams, false, ["verify"]);
|
|
714
2644
|
}
|
|
715
|
-
async function
|
|
2645
|
+
async function parseJWT(token, validation) {
|
|
2646
|
+
ensureInitialized();
|
|
716
2647
|
try {
|
|
717
2648
|
const parts = token.split(".");
|
|
718
2649
|
if (parts.length !== 3)
|
|
719
2650
|
throw new Error("Invalid JWT");
|
|
720
|
-
const header = JSON.parse(
|
|
721
|
-
const payload = JSON.parse(
|
|
722
|
-
const signature = parts[2].replace(/-/g, "+").replace(/_/g, "/");
|
|
2651
|
+
const header = JSON.parse(base64UrlDecode(parts[0]));
|
|
2652
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
723
2653
|
const publicKey = await getPublicKey(header.kid);
|
|
2654
|
+
const signature = parts[2];
|
|
724
2655
|
const encoder = new TextEncoder;
|
|
725
2656
|
const data = encoder.encode(`${parts[0]}.${parts[1]}`);
|
|
726
|
-
const
|
|
2657
|
+
const signatureBytes = Uint8Array.from(base64UrlDecode(signature), (c) => c.charCodeAt(0));
|
|
2658
|
+
const algorithmParams = getAlgorithmParams(header.alg);
|
|
2659
|
+
const isValid = await crypto.subtle.verify(algorithmParams, publicKey, signatureBytes, data);
|
|
727
2660
|
if (!isValid)
|
|
728
2661
|
throw new Error("Invalid JWT signature");
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
if ("issues" in
|
|
732
|
-
console.error("
|
|
733
|
-
throw new Error(`
|
|
2662
|
+
const validator = validation?.jwtAssertionClaims ?? jwtAssertionClaimsSchema("builtin");
|
|
2663
|
+
const result = await validator["~standard"].validate(payload);
|
|
2664
|
+
if ("issues" in result) {
|
|
2665
|
+
console.error("JWT claims validation failed:", result.issues);
|
|
2666
|
+
throw new Error(`JWT claims validation failed: ${result.issues?.map((i) => i.message).join("; ")}`);
|
|
734
2667
|
}
|
|
735
|
-
return
|
|
736
|
-
} catch (
|
|
737
|
-
console.error("Error verifying JWT:",
|
|
738
|
-
throw
|
|
2668
|
+
return result.value;
|
|
2669
|
+
} catch (error) {
|
|
2670
|
+
console.error("Error verifying JWT:", error);
|
|
2671
|
+
throw error;
|
|
739
2672
|
}
|
|
740
2673
|
}
|
|
741
|
-
function
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
["Set-Cookie", createCookie("access", token.access_token, expires)],
|
|
777
|
-
["Set-Cookie", createCookie("id", token.id_token, expires)],
|
|
778
|
-
["Set-Cookie", createCookie("refresh", token.refresh_token ?? "", expires)],
|
|
779
|
-
["Set-Cookie", createCookie("control", control, expires)]
|
|
780
|
-
];
|
|
781
|
-
}
|
|
782
|
-
async function getTokenFromCookies(req) {
|
|
783
|
-
const access_token = getCookie("access", req);
|
|
784
|
-
const id_token = getCookie("id", req);
|
|
785
|
-
const refresh_token = getCookie("refresh", req);
|
|
786
|
-
const control = getCookie("control", req, true);
|
|
787
|
-
if (!access_token || !id_token || !refresh_token || !control) {
|
|
788
|
-
return { tokens: undefined, refreshHeaders: [] };
|
|
789
|
-
}
|
|
790
|
-
let tokenResponse = {
|
|
791
|
-
access_token,
|
|
792
|
-
id_token,
|
|
793
|
-
refresh_token,
|
|
794
|
-
...control
|
|
795
|
-
};
|
|
796
|
-
if (control.expires && refresh_token && Date.now() > new Date(control.expires).getTime()) {
|
|
797
|
-
tokenResponse = await refreshToken(refresh_token);
|
|
798
|
-
const user = await parseUser(tokenResponse);
|
|
799
|
-
const refreshHeaders = createJwtCookies(tokenResponse, user.sso.expires);
|
|
800
|
-
return { tokens: tokenResponse, refreshHeaders };
|
|
2674
|
+
async function validateToken(token, validation) {
|
|
2675
|
+
ensureInitialized();
|
|
2676
|
+
try {
|
|
2677
|
+
const claims = await parseJWT(token, validation);
|
|
2678
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2679
|
+
if (claims.exp && claims.exp < now) {
|
|
2680
|
+
return { valid: false, error: "Token expired" };
|
|
2681
|
+
}
|
|
2682
|
+
if (isJwtBearerConfig(config)) {
|
|
2683
|
+
if (config.audience && claims.aud !== config.audience) {
|
|
2684
|
+
return { valid: false, error: "Invalid audience" };
|
|
2685
|
+
}
|
|
2686
|
+
} else if (isClientCredentialsConfig(config)) {
|
|
2687
|
+
if (config.issuer && claims.iss !== config.issuer) {
|
|
2688
|
+
return { valid: false, error: "Invalid issuer" };
|
|
2689
|
+
}
|
|
2690
|
+
if (config.audience && claims.aud !== config.audience) {
|
|
2691
|
+
return { valid: false, error: "Invalid audience" };
|
|
2692
|
+
}
|
|
2693
|
+
} else {
|
|
2694
|
+
const serverConfig = config;
|
|
2695
|
+
if (serverConfig.issuer && claims.iss !== serverConfig.issuer) {
|
|
2696
|
+
return { valid: false, error: "Invalid issuer" };
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return {
|
|
2700
|
+
valid: true,
|
|
2701
|
+
claims,
|
|
2702
|
+
expiresAt: claims.exp ? new Date(claims.exp * 1000) : undefined
|
|
2703
|
+
};
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
return {
|
|
2706
|
+
valid: false,
|
|
2707
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2708
|
+
};
|
|
801
2709
|
}
|
|
802
|
-
return { tokens: tokenResponse, refreshHeaders: [] };
|
|
803
2710
|
}
|
|
804
|
-
async function
|
|
805
|
-
|
|
806
|
-
if (!
|
|
2711
|
+
async function getWorkload(request) {
|
|
2712
|
+
ensureInitialized();
|
|
2713
|
+
if (!config.jwks_uri) {
|
|
2714
|
+
throw new Error("Cannot validate tokens: Missing jwks_uri in WorkloadConfig. " + "Server role requires jwks_uri to be configured in vault to fetch public keys for token validation.");
|
|
2715
|
+
}
|
|
2716
|
+
const authHeader = request.headers.get("Authorization");
|
|
2717
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
807
2718
|
return;
|
|
808
|
-
|
|
2719
|
+
}
|
|
2720
|
+
const token = authHeader.substring(7);
|
|
2721
|
+
const result = await validateToken(token, configWithDefaults.validation);
|
|
2722
|
+
if (!result.valid || !result.claims) {
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
return {
|
|
2726
|
+
workload_id: result.claims.sub,
|
|
2727
|
+
client_id: typeof result.claims.client_id === "string" ? result.claims.client_id : undefined,
|
|
2728
|
+
scope: result.claims.scope,
|
|
2729
|
+
claims: result.claims
|
|
2730
|
+
};
|
|
809
2731
|
}
|
|
810
|
-
function
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
2732
|
+
async function handler(request) {
|
|
2733
|
+
ensureInitialized();
|
|
2734
|
+
const tokenUrl = configWithDefaults.tokenUrl;
|
|
2735
|
+
const validateUrl = configWithDefaults.validateUrl;
|
|
2736
|
+
const jwksUrl = configWithDefaults.jwksUrl;
|
|
2737
|
+
const refreshUrl = configWithDefaults.refreshUrl;
|
|
2738
|
+
const validation = configWithDefaults.validation;
|
|
2739
|
+
const path = new URL(request.url).pathname;
|
|
2740
|
+
if (tokenUrl === path && request.method === "GET") {
|
|
2741
|
+
const url = new URL(request.url);
|
|
2742
|
+
const scope = url.searchParams.get("scope") || undefined;
|
|
2743
|
+
const token = await getToken(scope);
|
|
2744
|
+
return new Response(JSON.stringify({ access_token: token, token_type: "Bearer" }), {
|
|
2745
|
+
headers: [["Content-Type", "application/json"]]
|
|
2746
|
+
});
|
|
814
2747
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
2748
|
+
if (validateUrl === path && request.method === "POST") {
|
|
2749
|
+
const authHeader = request.headers.get("Authorization");
|
|
2750
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
2751
|
+
return new Response(JSON.stringify({ valid: false, error: "Missing Authorization header" }), {
|
|
2752
|
+
status: 401,
|
|
2753
|
+
headers: [["Content-Type", "application/json"]]
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
const token = authHeader.substring(7);
|
|
2757
|
+
const result = await validateToken(token, validation);
|
|
2758
|
+
return new Response(JSON.stringify(result), {
|
|
2759
|
+
status: result.valid ? 200 : 401,
|
|
2760
|
+
headers: [["Content-Type", "application/json"]]
|
|
2761
|
+
});
|
|
822
2762
|
}
|
|
823
|
-
if (
|
|
824
|
-
|
|
2763
|
+
if (jwksUrl === path && request.method === "GET") {
|
|
2764
|
+
const jwks = await fetchJwks();
|
|
2765
|
+
return new Response(JSON.stringify(jwks), {
|
|
2766
|
+
headers: [["Content-Type", "application/json"]]
|
|
2767
|
+
});
|
|
825
2768
|
}
|
|
826
|
-
|
|
2769
|
+
if (refreshUrl === path && request.method === "POST") {
|
|
2770
|
+
const tokenResponse = await refreshToken();
|
|
2771
|
+
return new Response(JSON.stringify(tokenResponse), {
|
|
2772
|
+
headers: [["Content-Type", "application/json"]]
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
return new Response("Not Found", { status: 404 });
|
|
827
2776
|
}
|
|
828
|
-
|
|
829
|
-
|
|
2777
|
+
return {
|
|
2778
|
+
...configWithDefaults,
|
|
2779
|
+
getToken,
|
|
2780
|
+
refreshToken,
|
|
2781
|
+
generateJWTAssertion,
|
|
2782
|
+
revokeToken,
|
|
2783
|
+
validateToken,
|
|
2784
|
+
getWorkload,
|
|
2785
|
+
parseJWT,
|
|
2786
|
+
handler
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// packages/react/src/group-store.ts
|
|
2791
|
+
class InMemoryGroupStore {
|
|
2792
|
+
groups = new Map;
|
|
2793
|
+
externalIdIndex = new Map;
|
|
2794
|
+
displayNameIndex = new Map;
|
|
2795
|
+
async get(id) {
|
|
2796
|
+
return this.groups.get(id) ?? null;
|
|
830
2797
|
}
|
|
831
|
-
|
|
832
|
-
const
|
|
833
|
-
if (!
|
|
2798
|
+
async getByExternalId(externalId) {
|
|
2799
|
+
const id = this.externalIdIndex.get(externalId);
|
|
2800
|
+
if (!id)
|
|
834
2801
|
return null;
|
|
835
|
-
|
|
836
|
-
|
|
2802
|
+
return this.groups.get(id) ?? null;
|
|
2803
|
+
}
|
|
2804
|
+
async getByDisplayName(displayName) {
|
|
2805
|
+
const id = this.displayNameIndex.get(displayName.toLowerCase());
|
|
2806
|
+
if (!id)
|
|
837
2807
|
return null;
|
|
838
|
-
|
|
839
|
-
if (!parse)
|
|
840
|
-
return val;
|
|
841
|
-
const str = atob(val);
|
|
842
|
-
return JSON.parse(str);
|
|
2808
|
+
return this.groups.get(id) ?? null;
|
|
843
2809
|
}
|
|
844
|
-
async
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
2810
|
+
async list() {
|
|
2811
|
+
return Array.from(this.groups.values());
|
|
2812
|
+
}
|
|
2813
|
+
async upsert(group) {
|
|
2814
|
+
const existing = this.groups.get(group.id);
|
|
2815
|
+
if (existing) {
|
|
2816
|
+
if (existing.externalId && existing.externalId !== group.externalId) {
|
|
2817
|
+
this.externalIdIndex.delete(existing.externalId);
|
|
2818
|
+
}
|
|
2819
|
+
if (existing.displayName.toLowerCase() !== group.displayName.toLowerCase()) {
|
|
2820
|
+
this.displayNameIndex.delete(existing.displayName.toLowerCase());
|
|
2821
|
+
}
|
|
852
2822
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
errorUrl
|
|
857
|
-
});
|
|
2823
|
+
this.groups.set(group.id, group);
|
|
2824
|
+
if (group.externalId) {
|
|
2825
|
+
this.externalIdIndex.set(group.externalId, group.id);
|
|
858
2826
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
2827
|
+
this.displayNameIndex.set(group.displayName.toLowerCase(), group.id);
|
|
2828
|
+
}
|
|
2829
|
+
async delete(id) {
|
|
2830
|
+
const group = this.groups.get(id);
|
|
2831
|
+
if (group) {
|
|
2832
|
+
if (group.externalId) {
|
|
2833
|
+
this.externalIdIndex.delete(group.externalId);
|
|
863
2834
|
}
|
|
864
|
-
|
|
865
|
-
return new Response(JSON.stringify(user), {
|
|
866
|
-
headers: [["Content-Type", "application/json"], ...refreshHeaders]
|
|
867
|
-
});
|
|
2835
|
+
this.displayNameIndex.delete(group.displayName.toLowerCase());
|
|
868
2836
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
token: tokens.access_token,
|
|
876
|
-
expires: tokens.expires
|
|
877
|
-
}), {
|
|
878
|
-
headers: [["Content-Type", "application/json"], ...refreshHeaders]
|
|
879
|
-
});
|
|
2837
|
+
this.groups.delete(id);
|
|
2838
|
+
}
|
|
2839
|
+
async addMember(groupId, member) {
|
|
2840
|
+
const group = this.groups.get(groupId);
|
|
2841
|
+
if (!group) {
|
|
2842
|
+
throw new Error(`Group ${groupId} not found`);
|
|
880
2843
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const newTokenResponse = await refreshToken(refresh_token);
|
|
887
|
-
const user = await parseUser(newTokenResponse);
|
|
888
|
-
const refreshHeaders = createJwtCookies(newTokenResponse, user.sso.expires);
|
|
889
|
-
return new Response("Refresh Complete", {
|
|
890
|
-
status: 200,
|
|
891
|
-
headers: refreshHeaders
|
|
892
|
-
});
|
|
2844
|
+
const members = group.members ?? [];
|
|
2845
|
+
if (!members.some((m) => m.value === member.value)) {
|
|
2846
|
+
members.push(member);
|
|
2847
|
+
group.members = members;
|
|
2848
|
+
group.updatedAt = new Date;
|
|
893
2849
|
}
|
|
894
|
-
|
|
895
|
-
|
|
2850
|
+
}
|
|
2851
|
+
async removeMember(groupId, memberId) {
|
|
2852
|
+
const group = this.groups.get(groupId);
|
|
2853
|
+
if (!group) {
|
|
2854
|
+
throw new Error(`Group ${groupId} not found`);
|
|
896
2855
|
}
|
|
897
|
-
if (
|
|
898
|
-
|
|
2856
|
+
if (group.members) {
|
|
2857
|
+
group.members = group.members.filter((m) => m.value !== memberId);
|
|
2858
|
+
group.updatedAt = new Date;
|
|
899
2859
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
// packages/react/src/tenant.ts
|
|
2863
|
+
class TenantRequestError extends Error {
|
|
2864
|
+
constructor(message) {
|
|
2865
|
+
super(message);
|
|
2866
|
+
this.name = "TenantRequestError";
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
function serializeESConfig(configOrES) {
|
|
2870
|
+
if (configOrES && typeof configOrES === "object" && "sso" in configOrES && "workload" in configOrES && "iam" in configOrES) {
|
|
2871
|
+
const config2 = {
|
|
2872
|
+
defaultInstance: configOrES.defaultInstance,
|
|
2873
|
+
sso: configOrES.sso ? serializeESConfig(configOrES.sso) : undefined,
|
|
2874
|
+
iam: configOrES.iam ? serializeESConfig(configOrES.iam) : undefined,
|
|
2875
|
+
workload: configOrES.workload ? serializeESConfig(configOrES.workload) : undefined,
|
|
2876
|
+
validation: configOrES.validation
|
|
2877
|
+
};
|
|
2878
|
+
return config2;
|
|
2879
|
+
}
|
|
2880
|
+
const config = configOrES;
|
|
2881
|
+
if (config === null || config === undefined) {
|
|
2882
|
+
return config;
|
|
2883
|
+
}
|
|
2884
|
+
if (typeof config !== "object") {
|
|
2885
|
+
return config;
|
|
2886
|
+
}
|
|
2887
|
+
if (Array.isArray(config)) {
|
|
2888
|
+
return config.map((item) => serializeESConfig(item));
|
|
2889
|
+
}
|
|
2890
|
+
const isStore = typeof config.get === "function" || typeof config.set === "function" || typeof config.create === "function" || typeof config.delete === "function" || typeof config.upsert === "function" || typeof config.list === "function";
|
|
2891
|
+
const isValidator = config["~standard"] !== undefined;
|
|
2892
|
+
if (typeof config === "function" || isStore || isValidator) {
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
if (config instanceof Date) {
|
|
2896
|
+
return config.toISOString();
|
|
2897
|
+
}
|
|
2898
|
+
const serialized = {};
|
|
2899
|
+
for (const key in config) {
|
|
2900
|
+
if (Object.prototype.hasOwnProperty.call(config, key)) {
|
|
2901
|
+
if (key === "session_store" || key === "user_store" || key === "token_store" || key === "group_store" || key === "validation" || key === "vault" || key === "getUser" || key === "getRequiredUser" || key === "getJwt" || key === "initiateLogin" || key === "logout" || key === "callbackHandler" || key === "handler" || key === "getToken" || key === "refreshToken" || key === "generateJWTAssertion" || key === "revokeToken" || key === "validateToken" || key === "getWorkload" || key === "parseJWT" || key === "createUser" || key === "getBaseUrl" || key === "groups_outbound" || key === "groups_inbound") {
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2904
|
+
const value = serializeESConfig(config[key]);
|
|
2905
|
+
if (value !== undefined) {
|
|
2906
|
+
serialized[key] = value;
|
|
2907
|
+
}
|
|
905
2908
|
}
|
|
906
|
-
return new Response("Not Found", { status: 404 });
|
|
907
2909
|
}
|
|
2910
|
+
return serialized;
|
|
2911
|
+
}
|
|
2912
|
+
async function parseTenantRequest(request) {
|
|
2913
|
+
if (request.method !== "POST") {
|
|
2914
|
+
throw new TenantRequestError("Only POST method is supported");
|
|
2915
|
+
}
|
|
2916
|
+
let body;
|
|
2917
|
+
try {
|
|
2918
|
+
body = await request.json();
|
|
2919
|
+
} catch (_error) {
|
|
2920
|
+
throw new TenantRequestError("Invalid JSON in request body");
|
|
2921
|
+
}
|
|
2922
|
+
if (typeof body !== "object" || body === null) {
|
|
2923
|
+
throw new TenantRequestError("Request body must be an object");
|
|
2924
|
+
}
|
|
2925
|
+
const tenantRequest = body;
|
|
2926
|
+
if (!tenantRequest.appId) {
|
|
2927
|
+
throw new TenantRequestError("Missing required field: appId");
|
|
2928
|
+
}
|
|
2929
|
+
if (!tenantRequest.companyId) {
|
|
2930
|
+
throw new TenantRequestError("Missing required field: companyId");
|
|
2931
|
+
}
|
|
2932
|
+
if (!tenantRequest.companyName) {
|
|
2933
|
+
throw new TenantRequestError("Missing required field: companyName");
|
|
2934
|
+
}
|
|
2935
|
+
if (!tenantRequest.environmentType) {
|
|
2936
|
+
throw new TenantRequestError("Missing required field: environmentType");
|
|
2937
|
+
}
|
|
2938
|
+
if (!tenantRequest.email) {
|
|
2939
|
+
throw new TenantRequestError("Missing required field: email");
|
|
2940
|
+
}
|
|
2941
|
+
if (!tenantRequest.webhookUrl) {
|
|
2942
|
+
throw new TenantRequestError("Missing required field: webhookUrl");
|
|
2943
|
+
}
|
|
2944
|
+
const validEnvironmentTypes = ["POC", "DEV", "QA", "PROD"];
|
|
2945
|
+
if (!validEnvironmentTypes.includes(tenantRequest.environmentType)) {
|
|
2946
|
+
throw new TenantRequestError(`Invalid environmentType: ${tenantRequest.environmentType}. Must be one of: ${validEnvironmentTypes.join(", ")}`);
|
|
2947
|
+
}
|
|
2948
|
+
try {
|
|
2949
|
+
new URL(tenantRequest.webhookUrl);
|
|
2950
|
+
} catch {
|
|
2951
|
+
throw new TenantRequestError("Invalid webhookUrl: must be a valid URL");
|
|
2952
|
+
}
|
|
2953
|
+
const appId = tenantRequest.appId;
|
|
2954
|
+
const companyId = tenantRequest.companyId;
|
|
2955
|
+
const companyName = tenantRequest.companyName;
|
|
2956
|
+
const environmentType = tenantRequest.environmentType;
|
|
2957
|
+
const email = tenantRequest.email;
|
|
2958
|
+
const webhookUrl = tenantRequest.webhookUrl;
|
|
908
2959
|
return {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
handler
|
|
2960
|
+
appId,
|
|
2961
|
+
companyId,
|
|
2962
|
+
companyName,
|
|
2963
|
+
environmentType,
|
|
2964
|
+
email,
|
|
2965
|
+
webhookUrl
|
|
916
2966
|
};
|
|
917
2967
|
}
|
|
2968
|
+
async function sendTenantWebhook(webhookUrl, payload) {
|
|
2969
|
+
try {
|
|
2970
|
+
const response = await fetch(webhookUrl, {
|
|
2971
|
+
method: "POST",
|
|
2972
|
+
headers: {
|
|
2973
|
+
"Content-Type": "application/json"
|
|
2974
|
+
},
|
|
2975
|
+
body: JSON.stringify(payload)
|
|
2976
|
+
});
|
|
2977
|
+
if (!response.ok) {
|
|
2978
|
+
console.error(`Failed to send webhook update: ${response.status} ${response.statusText}`);
|
|
2979
|
+
}
|
|
2980
|
+
} catch (error) {
|
|
2981
|
+
console.error("Failed to send webhook update:", error);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
918
2984
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
2985
|
+
class InMemoryTenantStore {
|
|
2986
|
+
tenants = new Map;
|
|
2987
|
+
companyIdIndex = new Map;
|
|
2988
|
+
async get(appId) {
|
|
2989
|
+
return this.tenants.get(appId) ?? null;
|
|
2990
|
+
}
|
|
2991
|
+
async getByCompanyId(companyId) {
|
|
2992
|
+
const appIds = this.companyIdIndex.get(companyId);
|
|
2993
|
+
if (!appIds || appIds.size === 0)
|
|
2994
|
+
return [];
|
|
2995
|
+
const tenants = [];
|
|
2996
|
+
for (const appId of appIds) {
|
|
2997
|
+
const tenant = this.tenants.get(appId);
|
|
2998
|
+
if (tenant) {
|
|
2999
|
+
tenants.push(tenant);
|
|
3000
|
+
}
|
|
925
3001
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
3002
|
+
return tenants;
|
|
3003
|
+
}
|
|
3004
|
+
async list() {
|
|
3005
|
+
return Array.from(this.tenants.values());
|
|
3006
|
+
}
|
|
3007
|
+
async upsert(tenant) {
|
|
3008
|
+
const existing = this.tenants.get(tenant.appId);
|
|
3009
|
+
if (existing && existing.companyId !== tenant.companyId) {
|
|
3010
|
+
const oldAppIds = this.companyIdIndex.get(existing.companyId);
|
|
3011
|
+
if (oldAppIds) {
|
|
3012
|
+
oldAppIds.delete(tenant.appId);
|
|
3013
|
+
if (oldAppIds.size === 0) {
|
|
3014
|
+
this.companyIdIndex.delete(existing.companyId);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
this.tenants.set(tenant.appId, tenant);
|
|
3019
|
+
let appIds = this.companyIdIndex.get(tenant.companyId);
|
|
3020
|
+
if (!appIds) {
|
|
3021
|
+
appIds = new Set;
|
|
3022
|
+
this.companyIdIndex.set(tenant.companyId, appIds);
|
|
931
3023
|
}
|
|
3024
|
+
appIds.add(tenant.appId);
|
|
3025
|
+
return tenant;
|
|
932
3026
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
3027
|
+
async delete(appId) {
|
|
3028
|
+
const tenant = this.tenants.get(appId);
|
|
3029
|
+
if (tenant) {
|
|
3030
|
+
const appIds = this.companyIdIndex.get(tenant.companyId);
|
|
3031
|
+
if (appIds) {
|
|
3032
|
+
appIds.delete(appId);
|
|
3033
|
+
if (appIds.size === 0) {
|
|
3034
|
+
this.companyIdIndex.delete(tenant.companyId);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
938
3037
|
}
|
|
939
|
-
|
|
3038
|
+
this.tenants.delete(appId);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
// packages/react/src/workload-server.ts
|
|
3042
|
+
function getWorkloadInstance(config) {
|
|
3043
|
+
const es = getES(config?.es);
|
|
3044
|
+
if (!es.workload) {
|
|
3045
|
+
console.error("Workload authentication not configured in EnterpriseStandard");
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
return es.workload;
|
|
3049
|
+
}
|
|
3050
|
+
function unavailable() {
|
|
3051
|
+
return new Response(JSON.stringify({ error: "Workload authentication unavailable" }), {
|
|
3052
|
+
status: 503,
|
|
3053
|
+
statusText: "Workload authentication unavailable",
|
|
3054
|
+
headers: { "Content-Type": "application/json" }
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
async function getWorkload(request, config) {
|
|
3058
|
+
const workloadAuth = getWorkloadInstance(config);
|
|
3059
|
+
if (!workloadAuth) {
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
return workloadAuth.getWorkload(request);
|
|
3063
|
+
}
|
|
3064
|
+
async function getWorkloadToken(scope, config) {
|
|
3065
|
+
const workloadAuth = getWorkloadInstance(config);
|
|
3066
|
+
if (!workloadAuth)
|
|
3067
|
+
throw unavailable();
|
|
3068
|
+
return workloadAuth.getToken(scope);
|
|
3069
|
+
}
|
|
3070
|
+
async function validateWorkloadToken(request, config) {
|
|
3071
|
+
const workloadAuth = getWorkloadInstance(config);
|
|
3072
|
+
if (!workloadAuth) {
|
|
3073
|
+
return { valid: false, error: "Workload authentication unavailable" };
|
|
3074
|
+
}
|
|
3075
|
+
const authHeader = request.headers.get("Authorization");
|
|
3076
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
3077
|
+
return { valid: false, error: "Missing or invalid Authorization header" };
|
|
3078
|
+
}
|
|
3079
|
+
const token = authHeader.substring(7);
|
|
3080
|
+
return workloadAuth.validateToken(token);
|
|
3081
|
+
}
|
|
3082
|
+
async function revokeWorkloadToken(token, config) {
|
|
3083
|
+
const workloadAuth = getWorkloadInstance(config);
|
|
3084
|
+
if (!workloadAuth)
|
|
3085
|
+
throw unavailable();
|
|
3086
|
+
return workloadAuth.revokeToken(token);
|
|
3087
|
+
}
|
|
3088
|
+
async function workloadHandler(request, config) {
|
|
3089
|
+
const workloadAuth = getWorkloadInstance(config);
|
|
3090
|
+
if (!workloadAuth)
|
|
3091
|
+
throw unavailable();
|
|
3092
|
+
return workloadAuth.handler(request);
|
|
940
3093
|
}
|
|
3094
|
+
|
|
941
3095
|
// packages/react/src/server.ts
|
|
942
3096
|
function getSSO(config) {
|
|
943
3097
|
const es = getES(config?.es);
|
|
@@ -947,7 +3101,7 @@ function getSSO(config) {
|
|
|
947
3101
|
}
|
|
948
3102
|
return es.sso;
|
|
949
3103
|
}
|
|
950
|
-
function
|
|
3104
|
+
function unavailable2() {
|
|
951
3105
|
new Response(JSON.stringify({ error: "SSO Unavailable" }), {
|
|
952
3106
|
status: 503,
|
|
953
3107
|
statusText: "SSO Unavailable",
|
|
@@ -960,26 +3114,26 @@ async function getUser(request, config) {
|
|
|
960
3114
|
async function getRequiredUser(request, config) {
|
|
961
3115
|
const sso2 = getSSO(config);
|
|
962
3116
|
if (!sso2)
|
|
963
|
-
throw
|
|
3117
|
+
throw unavailable2();
|
|
964
3118
|
return sso2.getRequiredUser(request);
|
|
965
3119
|
}
|
|
966
3120
|
async function initiateLogin(config) {
|
|
967
3121
|
const sso2 = getSSO(config);
|
|
968
3122
|
if (!sso2)
|
|
969
|
-
throw
|
|
3123
|
+
throw unavailable2();
|
|
970
3124
|
return sso2.initiateLogin(config);
|
|
971
3125
|
}
|
|
972
3126
|
async function callback(request, config) {
|
|
973
3127
|
const sso2 = getSSO(config);
|
|
974
3128
|
if (!sso2)
|
|
975
|
-
throw
|
|
3129
|
+
throw unavailable2();
|
|
976
3130
|
return sso2.callbackHandler(request);
|
|
977
3131
|
}
|
|
978
3132
|
async function handler(request, config) {
|
|
979
3133
|
const sso2 = getSSO(config);
|
|
980
3134
|
if (!sso2)
|
|
981
|
-
throw
|
|
982
|
-
return sso2.handler(request
|
|
3135
|
+
throw unavailable2();
|
|
3136
|
+
return sso2.handler(request);
|
|
983
3137
|
}
|
|
984
3138
|
// packages/react/src/session-store.ts
|
|
985
3139
|
class InMemorySessionStore {
|
|
@@ -1301,41 +3455,147 @@ async function logout(logoutUrl) {
|
|
|
1301
3455
|
};
|
|
1302
3456
|
}
|
|
1303
3457
|
}
|
|
3458
|
+
// packages/react/src/user-store.ts
|
|
3459
|
+
class InMemoryUserStore {
|
|
3460
|
+
users = new Map;
|
|
3461
|
+
emailIndex = new Map;
|
|
3462
|
+
userNameIndex = new Map;
|
|
3463
|
+
async get(sub) {
|
|
3464
|
+
return this.users.get(sub) ?? null;
|
|
3465
|
+
}
|
|
3466
|
+
async getByEmail(email) {
|
|
3467
|
+
const sub = this.emailIndex.get(email.toLowerCase());
|
|
3468
|
+
if (!sub)
|
|
3469
|
+
return null;
|
|
3470
|
+
return this.users.get(sub) ?? null;
|
|
3471
|
+
}
|
|
3472
|
+
async getByUserName(userName) {
|
|
3473
|
+
const sub = this.userNameIndex.get(userName.toLowerCase());
|
|
3474
|
+
if (!sub)
|
|
3475
|
+
return null;
|
|
3476
|
+
return this.users.get(sub) ?? null;
|
|
3477
|
+
}
|
|
3478
|
+
async upsert(user) {
|
|
3479
|
+
const existing = this.users.get(user.id);
|
|
3480
|
+
if (existing) {
|
|
3481
|
+
if (existing.email && existing.email.toLowerCase() !== user.email?.toLowerCase()) {
|
|
3482
|
+
this.emailIndex.delete(existing.email.toLowerCase());
|
|
3483
|
+
}
|
|
3484
|
+
if (existing.userName && existing.userName.toLowerCase() !== user.userName?.toLowerCase()) {
|
|
3485
|
+
this.userNameIndex.delete(existing.userName.toLowerCase());
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
this.users.set(user.id, user);
|
|
3489
|
+
if (user.email) {
|
|
3490
|
+
this.emailIndex.set(user.email.toLowerCase(), user.id);
|
|
3491
|
+
}
|
|
3492
|
+
if (user.userName) {
|
|
3493
|
+
this.userNameIndex.set(user.userName.toLowerCase(), user.id);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
async delete(sub) {
|
|
3497
|
+
const user = this.users.get(sub);
|
|
3498
|
+
if (user) {
|
|
3499
|
+
if (user.email) {
|
|
3500
|
+
this.emailIndex.delete(user.email.toLowerCase());
|
|
3501
|
+
}
|
|
3502
|
+
if (user.userName) {
|
|
3503
|
+
this.userNameIndex.delete(user.userName.toLowerCase());
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
this.users.delete(sub);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
1304
3509
|
|
|
1305
3510
|
// packages/react/src/index.ts
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
3511
|
+
function extractSsoValidation(validation) {
|
|
3512
|
+
if (!validation)
|
|
3513
|
+
return;
|
|
3514
|
+
const val = validation;
|
|
3515
|
+
if (val.callbackParams || val.idTokenClaims || val.tokenResponse) {
|
|
3516
|
+
return val;
|
|
3517
|
+
}
|
|
3518
|
+
if (typeof val === "object" && "sso" in val) {
|
|
3519
|
+
return val.sso;
|
|
3520
|
+
}
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
function extractWorkloadValidation(validation) {
|
|
3524
|
+
if (!validation)
|
|
3525
|
+
return;
|
|
3526
|
+
const val = validation;
|
|
3527
|
+
if (val.jwtAssertionClaims || val.tokenResponse) {
|
|
3528
|
+
return val;
|
|
3529
|
+
}
|
|
3530
|
+
if (typeof val === "object" && "workload" in val) {
|
|
3531
|
+
return val.workload;
|
|
3532
|
+
}
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
async function enterpriseStandard(appId, initConfig) {
|
|
3536
|
+
const ioniteUrl = process.env.IONITE_URL ?? "https://ionite.com";
|
|
3537
|
+
let vaultUrl = process.env.ES_VAULT_URL;
|
|
3538
|
+
const vaultToken = process.env.ES_VAULT_TOKEN;
|
|
3539
|
+
const vaultPath = process.env.ES_VAULT_PATH;
|
|
3540
|
+
let secret;
|
|
3541
|
+
if (appId?.startsWith("IONITE_PUBLIC_DEMO_") && !vaultUrl) {
|
|
1312
3542
|
vaultUrl = "https://vault-ionite.ionite.dev/v1/secret/data";
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
path: `public/IONITE_PUBLIC_DEMO_SSO_${port}`,
|
|
1317
|
-
token: "hvs.VGhD2hmXDH9PmZjTacZx0G5K"
|
|
1318
|
-
}
|
|
3543
|
+
secret = {
|
|
3544
|
+
path: `public/${appId}`,
|
|
3545
|
+
token: "hvs.VGhD2hmXDH9PmZjTacZx0G5K"
|
|
1319
3546
|
};
|
|
1320
|
-
} else if (
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
} else {
|
|
1326
|
-
|
|
3547
|
+
} else if (vaultUrl && vaultToken && vaultPath) {
|
|
3548
|
+
secret = {
|
|
3549
|
+
path: vaultPath,
|
|
3550
|
+
token: vaultToken
|
|
3551
|
+
};
|
|
3552
|
+
} else if (!vaultUrl || !vaultToken || !vaultPath) {
|
|
3553
|
+
console.log("NODE_ENV", "development");
|
|
3554
|
+
const cmd = `${process.versions.bun ? "bun" : "npm"} ionite login --app ${appId}`;
|
|
3555
|
+
throw new Error(`@enterprisestandard configuration missing.
|
|
3556
|
+
For development, login with the ionite CLI using "${cmd}" or visit ${ioniteUrl}/api/applications/apiKeys/create?appId=${appId}. If this is a non-development environment, ensure that you are deployed with the correct tenant pattern.`);
|
|
1327
3557
|
}
|
|
1328
3558
|
const defaultInstance2 = getDefaultInstance();
|
|
1329
3559
|
const vaultClient = vault(vaultUrl);
|
|
3560
|
+
const ssoValidation = extractSsoValidation(initConfig?.validation);
|
|
3561
|
+
const workloadValidation = extractWorkloadValidation(initConfig?.validation);
|
|
3562
|
+
let vaultData = {};
|
|
3563
|
+
if (secret) {
|
|
3564
|
+
vaultData = await vaultClient.getSecret(secret.path, secret.token);
|
|
3565
|
+
}
|
|
3566
|
+
const workloadConfig = {
|
|
3567
|
+
...vaultData.workload,
|
|
3568
|
+
...initConfig?.workload,
|
|
3569
|
+
jwks_uri: initConfig?.workload?.jwks_uri ?? vaultData.workload?.jwks_uri,
|
|
3570
|
+
token_url: initConfig?.workload?.token_url ?? vaultData.workload?.token_url,
|
|
3571
|
+
validation: initConfig?.workload?.validation ?? workloadValidation
|
|
3572
|
+
};
|
|
3573
|
+
const workloadInstance = workload(workloadConfig);
|
|
3574
|
+
const ssoConfig = {
|
|
3575
|
+
...vaultData.sso,
|
|
3576
|
+
...initConfig?.sso,
|
|
3577
|
+
validation: initConfig?.sso?.validation ?? ssoValidation
|
|
3578
|
+
};
|
|
3579
|
+
const ssoInstance = sso(ssoConfig);
|
|
3580
|
+
const iamConfig = {
|
|
3581
|
+
...vaultData.iam,
|
|
3582
|
+
...initConfig?.iam
|
|
3583
|
+
};
|
|
3584
|
+
const iamInstance = iam(iamConfig, workloadInstance);
|
|
3585
|
+
const mergedConfig = {
|
|
3586
|
+
defaultInstance: initConfig?.defaultInstance,
|
|
3587
|
+
sso: ssoConfig,
|
|
3588
|
+
iam: iamConfig,
|
|
3589
|
+
workload: workloadConfig,
|
|
3590
|
+
validation: initConfig?.validation
|
|
3591
|
+
};
|
|
1330
3592
|
const result = {
|
|
1331
|
-
|
|
3593
|
+
...mergedConfig,
|
|
1332
3594
|
defaultInstance: initConfig?.defaultInstance || initConfig?.defaultInstance !== false && !defaultInstance2,
|
|
1333
3595
|
vault: vaultClient,
|
|
1334
|
-
sso:
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
}) : undefined,
|
|
1338
|
-
iam: secrets.iam ? await iam(await vaultClient.getSecret(secrets.iam.path, secrets.iam.token)) : undefined
|
|
3596
|
+
sso: ssoInstance,
|
|
3597
|
+
iam: iamInstance,
|
|
3598
|
+
workload: workloadInstance
|
|
1339
3599
|
};
|
|
1340
3600
|
if (result.defaultInstance) {
|
|
1341
3601
|
if (defaultInstance2) {
|
|
@@ -1346,23 +3606,46 @@ async function enterpriseStandard(appKey, initConfig) {
|
|
|
1346
3606
|
return result;
|
|
1347
3607
|
}
|
|
1348
3608
|
export {
|
|
3609
|
+
workloadTokenResponseSchema,
|
|
3610
|
+
workloadHandler,
|
|
3611
|
+
workload,
|
|
3612
|
+
vault,
|
|
3613
|
+
validateWorkloadToken,
|
|
3614
|
+
userSchema,
|
|
1349
3615
|
useUser,
|
|
1350
3616
|
useToken,
|
|
1351
3617
|
tokenResponseSchema,
|
|
3618
|
+
sso,
|
|
3619
|
+
serializeESConfig,
|
|
3620
|
+
sendTenantWebhook,
|
|
3621
|
+
revokeWorkloadToken,
|
|
3622
|
+
parseTenantRequest,
|
|
1352
3623
|
oidcCallbackSchema,
|
|
1353
3624
|
logout,
|
|
3625
|
+
jwtAssertionClaimsSchema,
|
|
1354
3626
|
initiateLogin,
|
|
1355
3627
|
idTokenClaimsSchema,
|
|
3628
|
+
iam,
|
|
1356
3629
|
handler,
|
|
3630
|
+
groupResourceSchema,
|
|
3631
|
+
getWorkloadToken,
|
|
3632
|
+
getWorkload,
|
|
1357
3633
|
getUser,
|
|
1358
3634
|
getRequiredUser,
|
|
3635
|
+
getES,
|
|
3636
|
+
getDefaultInstance,
|
|
1359
3637
|
enterpriseStandard,
|
|
1360
3638
|
callback,
|
|
3639
|
+
TenantRequestError,
|
|
1361
3640
|
SignedOut,
|
|
1362
3641
|
SignedIn,
|
|
1363
3642
|
SignInLoading,
|
|
1364
3643
|
SSOProvider,
|
|
1365
|
-
|
|
3644
|
+
InMemoryWorkloadTokenStore,
|
|
3645
|
+
InMemoryUserStore,
|
|
3646
|
+
InMemoryTenantStore,
|
|
3647
|
+
InMemorySessionStore,
|
|
3648
|
+
InMemoryGroupStore
|
|
1366
3649
|
};
|
|
1367
3650
|
|
|
1368
|
-
//# debugId=
|
|
3651
|
+
//# debugId=755D0ECE5A3DFE8164756E2164756E21
|