@enterprisestandard/react 0.0.4 → 0.0.5-beta.20260114.1
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 -12
- package/dist/iam.d.ts.map +1 -1
- package/dist/index.d.ts +44 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3164 -572
- package/dist/index.js.map +29 -0
- package/dist/server.d.ts +6 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/session-store.d.ts +179 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/sso.d.ts +74 -16
- 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/{oidc-schema.d.ts → types/oidc-schema.d.ts} +42 -0
- package/dist/types/oidc-schema.d.ts.map +1 -0
- package/dist/types/scim-schema.d.ts +419 -0
- 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 +2 -5
- package/dist/enterprise-user.d.ts +0 -125
- package/dist/enterprise-user.d.ts.map +0 -1
- package/dist/oidc-schema.d.ts.map +0 -1
- package/dist/standard-schema.d.ts.map +0 -1
- /package/dist/{standard-schema.d.ts → types/standard-schema.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,239 +1,2435 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
return {
|
|
4
|
-
url: config.url,
|
|
5
|
-
userEndpoint: config.userEndpoint,
|
|
6
|
-
groupEndpoint: config.groupEndpoint
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// src/utils.ts
|
|
11
|
-
var defaultInstance;
|
|
12
|
-
function must(value, message = "Assertion failed. Required value is null or undefined.") {
|
|
1
|
+
// packages/react/src/types/scim-schema.ts
|
|
2
|
+
function validateString(value, fieldName, required, issues, path) {
|
|
13
3
|
if (value === undefined || value === null) {
|
|
14
|
-
|
|
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;
|
|
15
18
|
}
|
|
16
19
|
return value;
|
|
17
20
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
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;
|
|
20
33
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
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;
|
|
23
60
|
}
|
|
24
|
-
function
|
|
25
|
-
if (
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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;
|
|
30
95
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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;
|
|
47
117
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
});
|
|
56
127
|
}
|
|
57
128
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
65
139
|
});
|
|
140
|
+
return;
|
|
66
141
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
71
152
|
}
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
codeVerifier,
|
|
86
|
-
landingUrl,
|
|
87
|
-
errorUrl
|
|
88
|
-
};
|
|
89
|
-
return new Response("Redirecting to SSO Provider", {
|
|
90
|
-
status: 302,
|
|
91
|
-
headers: {
|
|
92
|
-
Location: url.toString(),
|
|
93
|
-
"Set-Cookie": createCookie("state", val, 86400)
|
|
94
|
-
}
|
|
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"])
|
|
95
166
|
});
|
|
96
167
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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;
|
|
105
191
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const redirectTo = url.searchParams.get("redirect");
|
|
115
|
-
if (redirectTo) {
|
|
116
|
-
return new Response("Logged out", {
|
|
117
|
-
status: 302,
|
|
118
|
-
headers: [["Location", redirectTo], ...clearHeaders]
|
|
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"])
|
|
119
200
|
});
|
|
120
201
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
127
224
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
status: 200,
|
|
139
|
-
headers: [["Content-Type", "text/html"], ...clearHeaders]
|
|
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"])
|
|
140
235
|
});
|
|
141
236
|
}
|
|
142
237
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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"]
|
|
170
269
|
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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) {
|
|
286
|
+
return {
|
|
287
|
+
"~standard": {
|
|
288
|
+
version: 1,
|
|
289
|
+
vendor,
|
|
290
|
+
validate: (value) => {
|
|
291
|
+
if (typeof value !== "object" || value === null) {
|
|
292
|
+
return {
|
|
293
|
+
issues: [
|
|
294
|
+
{
|
|
295
|
+
message: "Expected an object"
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
};
|
|
181
299
|
}
|
|
182
|
-
|
|
183
|
-
|
|
300
|
+
const user = value;
|
|
301
|
+
const issues = [];
|
|
302
|
+
const result = {};
|
|
303
|
+
const userName = validateString(user.userName, "userName", true, issues, ["userName"]);
|
|
304
|
+
if (!userName) {
|
|
305
|
+
return { issues };
|
|
306
|
+
}
|
|
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]);
|
|
331
|
+
}
|
|
332
|
+
if (user.schemas !== undefined) {
|
|
333
|
+
if (Array.isArray(user.schemas)) {
|
|
334
|
+
result.schemas = user.schemas.filter((s) => typeof s === "string");
|
|
335
|
+
} else {
|
|
336
|
+
issues.push({
|
|
337
|
+
message: "schemas must be an array",
|
|
338
|
+
path: ["schemas"]
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
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
|
+
};
|
|
352
|
+
} else {
|
|
353
|
+
issues.push({
|
|
354
|
+
message: "meta must be an object",
|
|
355
|
+
path: ["meta"]
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (issues.length > 0) {
|
|
360
|
+
return { issues };
|
|
361
|
+
}
|
|
362
|
+
return { value: result };
|
|
184
363
|
}
|
|
185
|
-
console.warn("No error page was found in the cookies. The user will be shown a default error page.");
|
|
186
|
-
return new Response("An error occurred during authentication, please return to the application homepage and try again.", {
|
|
187
|
-
status: 500
|
|
188
|
-
});
|
|
189
364
|
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function validateMembers(value, issues, basePath) {
|
|
368
|
+
if (value === undefined || value === null) {
|
|
369
|
+
return;
|
|
190
370
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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) {
|
|
404
|
+
return {
|
|
405
|
+
"~standard": {
|
|
406
|
+
version: 1,
|
|
407
|
+
vendor,
|
|
408
|
+
validate: (value) => {
|
|
409
|
+
if (typeof value !== "object" || value === null) {
|
|
410
|
+
return {
|
|
411
|
+
issues: [
|
|
412
|
+
{
|
|
413
|
+
message: "Expected an object"
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
};
|
|
206
417
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
418
|
+
const group = value;
|
|
419
|
+
const issues = [];
|
|
420
|
+
const result = {};
|
|
421
|
+
const displayName = validateString(group.displayName, "displayName", true, issues, ["displayName"]);
|
|
422
|
+
if (!displayName) {
|
|
423
|
+
return { issues };
|
|
424
|
+
}
|
|
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");
|
|
432
|
+
} else {
|
|
433
|
+
issues.push({
|
|
434
|
+
message: "schemas must be an array",
|
|
435
|
+
path: ["schemas"]
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
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
|
+
};
|
|
449
|
+
} else {
|
|
450
|
+
issues.push({
|
|
451
|
+
message: "meta must be an object",
|
|
452
|
+
path: ["meta"]
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (issues.length > 0) {
|
|
457
|
+
return { issues };
|
|
458
|
+
}
|
|
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"
|
|
214
531
|
},
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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"
|
|
218
583
|
},
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
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
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
223
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()
|
|
802
|
+
}
|
|
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");
|
|
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 });
|
|
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
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// packages/react/src/types/oidc-schema.ts
|
|
999
|
+
function oidcCallbackSchema(vendor) {
|
|
1000
|
+
return {
|
|
1001
|
+
"~standard": {
|
|
1002
|
+
version: 1,
|
|
1003
|
+
vendor,
|
|
1004
|
+
validate: (value) => {
|
|
1005
|
+
if (typeof value !== "object" || value === null) {
|
|
1006
|
+
return {
|
|
1007
|
+
issues: [
|
|
1008
|
+
{
|
|
1009
|
+
message: "Expected an object"
|
|
1010
|
+
}
|
|
1011
|
+
]
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
const params = value;
|
|
1015
|
+
const issues = [];
|
|
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 {
|
|
1065
|
+
issues.push({
|
|
1066
|
+
message: "error_description must be a string",
|
|
1067
|
+
path: ["error_description"]
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
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 {
|
|
1075
|
+
issues.push({
|
|
1076
|
+
message: "error_uri must be a string",
|
|
1077
|
+
path: ["error_uri"]
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
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
|
+
}
|
|
1092
|
+
if (issues.length > 0) {
|
|
1093
|
+
return { issues };
|
|
1094
|
+
}
|
|
1095
|
+
return { value: result };
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
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) {
|
|
1333
|
+
if (!configWithDefaults) {
|
|
1334
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
const { tokens } = await getTokenFromCookies(request);
|
|
1338
|
+
if (!tokens)
|
|
1339
|
+
return;
|
|
1340
|
+
return await parseUser(tokens);
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
console.error("Error parsing user from cookies:", error);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
async function getRequiredUser(request) {
|
|
1347
|
+
const user = await getUser(request);
|
|
1348
|
+
if (user)
|
|
1349
|
+
return user;
|
|
1350
|
+
throw new Response("Unauthorized", {
|
|
1351
|
+
status: 401,
|
|
1352
|
+
statusText: "Unauthorized"
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
async function initiateLogin({ landingUrl, errorUrl }, requestUrl) {
|
|
1356
|
+
if (!configWithDefaults) {
|
|
1357
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1358
|
+
}
|
|
1359
|
+
const state = generateRandomString();
|
|
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
|
+
}
|
|
1381
|
+
const url = new URL(configWithDefaults.authorization_url);
|
|
1382
|
+
url.searchParams.append("client_id", configWithDefaults.client_id);
|
|
1383
|
+
url.searchParams.append("redirect_uri", normalizedRedirectUri);
|
|
1384
|
+
url.searchParams.append("response_type", "code");
|
|
1385
|
+
url.searchParams.append("scope", configWithDefaults.scope);
|
|
1386
|
+
url.searchParams.append("state", state);
|
|
1387
|
+
const codeChallenge = await pkceChallengeFromVerifier(codeVerifier);
|
|
1388
|
+
url.searchParams.append("code_challenge", codeChallenge);
|
|
1389
|
+
url.searchParams.append("code_challenge_method", "S256");
|
|
1390
|
+
const val = {
|
|
1391
|
+
state,
|
|
1392
|
+
codeVerifier,
|
|
1393
|
+
landingUrl,
|
|
1394
|
+
errorUrl
|
|
1395
|
+
};
|
|
1396
|
+
return new Response("Redirecting to SSO Provider", {
|
|
1397
|
+
status: 302,
|
|
1398
|
+
headers: {
|
|
1399
|
+
Location: url.toString(),
|
|
1400
|
+
"Set-Cookie": createCookie("state", val, 86400)
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
async function logout(request, _config) {
|
|
1405
|
+
if (!configWithDefaults) {
|
|
1406
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1407
|
+
}
|
|
1408
|
+
try {
|
|
1409
|
+
const refreshToken2 = getCookie("refresh", request);
|
|
1410
|
+
if (refreshToken2) {
|
|
1411
|
+
await revokeToken(refreshToken2);
|
|
1412
|
+
}
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
console.warn("Failed to revoke token:", error);
|
|
1415
|
+
}
|
|
1416
|
+
if (configWithDefaults.session_store) {
|
|
1417
|
+
try {
|
|
1418
|
+
const user = await getUser(request);
|
|
1419
|
+
if (user?.sso?.profile.sid) {
|
|
1420
|
+
const sid = user.sso.profile.sid;
|
|
1421
|
+
await configWithDefaults.session_store.delete(sid);
|
|
1422
|
+
console.log(`Session ${sid} deleted from store`);
|
|
1423
|
+
}
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
console.warn("Failed to delete session:", error);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const clearHeaders = [
|
|
1429
|
+
["Set-Cookie", clearCookie("access")],
|
|
1430
|
+
["Set-Cookie", clearCookie("id")],
|
|
1431
|
+
["Set-Cookie", clearCookie("refresh")],
|
|
1432
|
+
["Set-Cookie", clearCookie("control")],
|
|
1433
|
+
["Set-Cookie", clearCookie("state")]
|
|
1434
|
+
];
|
|
1435
|
+
const url = new URL(request.url);
|
|
1436
|
+
const redirectTo = url.searchParams.get("redirect");
|
|
1437
|
+
if (redirectTo) {
|
|
1438
|
+
return new Response("Logged out", {
|
|
1439
|
+
status: 302,
|
|
1440
|
+
headers: [["Location", redirectTo], ...clearHeaders]
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
const accept = request.headers.get("accept");
|
|
1444
|
+
const isAjax = accept?.includes("application/json") || accept?.includes("text/javascript");
|
|
1445
|
+
if (isAjax) {
|
|
1446
|
+
return new Response(JSON.stringify({ success: true, message: "Logged out" }), {
|
|
1447
|
+
status: 200,
|
|
1448
|
+
headers: [["Content-Type", "application/json"], ...clearHeaders]
|
|
1449
|
+
});
|
|
1450
|
+
} else {
|
|
1451
|
+
return new Response(`
|
|
1452
|
+
<!DOCTYPE html><html lang="en"><body>
|
|
1453
|
+
<h1>Logout Complete</h1>
|
|
1454
|
+
<div style="display: none">
|
|
1455
|
+
It is not recommended to show the default logout page. Include '?redirect=/someHomePage' or logout asynchronously.
|
|
1456
|
+
Check the <a href="https://EnterpriseStandard.com/sso#logout">Enterprise Standard Packages</a> for more information.
|
|
1457
|
+
</div>
|
|
1458
|
+
</body></html>
|
|
1459
|
+
`, {
|
|
1460
|
+
status: 200,
|
|
1461
|
+
headers: [["Content-Type", "text/html"], ...clearHeaders]
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
async function logoutBackChannel(request) {
|
|
1466
|
+
if (!configWithDefaults) {
|
|
1467
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1468
|
+
}
|
|
1469
|
+
if (!configWithDefaults.session_store) {
|
|
1470
|
+
throw new Error("Back-Channel Logout requires session_store configuration");
|
|
1471
|
+
}
|
|
1472
|
+
try {
|
|
1473
|
+
const contentType = request.headers.get("content-type");
|
|
1474
|
+
if (!contentType || !contentType.includes("application/x-www-form-urlencoded")) {
|
|
1475
|
+
return new Response("Invalid Content-Type, expected application/x-www-form-urlencoded", {
|
|
1476
|
+
status: 400
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
const body = await request.text();
|
|
1480
|
+
const params = new URLSearchParams(body);
|
|
1481
|
+
const logoutToken = params.get("logout_token");
|
|
1482
|
+
if (!logoutToken) {
|
|
1483
|
+
return new Response("Missing logout_token parameter", { status: 400 });
|
|
1484
|
+
}
|
|
1485
|
+
const claims = await parseJwt(logoutToken);
|
|
1486
|
+
const sid = claims.sid;
|
|
1487
|
+
if (!sid) {
|
|
1488
|
+
console.warn("Back-Channel Logout: logout_token missing sid claim");
|
|
1489
|
+
return new Response("Invalid logout_token: missing sid claim", { status: 400 });
|
|
1490
|
+
}
|
|
1491
|
+
await configWithDefaults.session_store.delete(sid);
|
|
1492
|
+
console.log(`Back-Channel Logout: successfully deleted session ${sid}`);
|
|
1493
|
+
return new Response("OK", { status: 200 });
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
console.error("Error during back-channel logout:", error);
|
|
1496
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
async function callbackHandler(request, validation) {
|
|
1500
|
+
if (!configWithDefaults) {
|
|
1501
|
+
throw new Error("Enterprise Standard SSO Manager not initialized");
|
|
1502
|
+
}
|
|
1503
|
+
const url = new URL(request.url);
|
|
1504
|
+
const params = new URLSearchParams(url.search);
|
|
1505
|
+
const callbackParamsValidator = validation?.callbackParams ?? oidcCallbackSchema("builtin");
|
|
1506
|
+
const paramsObject = Object.fromEntries(params.entries());
|
|
1507
|
+
const paramsResult = await callbackParamsValidator["~standard"].validate(paramsObject);
|
|
1508
|
+
if ("issues" in paramsResult) {
|
|
1509
|
+
return new Response(JSON.stringify({
|
|
1510
|
+
error: "validation_failed",
|
|
1511
|
+
message: "OIDC callback parameters validation failed",
|
|
1512
|
+
issues: paramsResult.issues?.map((i) => ({
|
|
1513
|
+
path: i.path?.join("."),
|
|
1514
|
+
message: i.message
|
|
1515
|
+
}))
|
|
1516
|
+
}), {
|
|
1517
|
+
status: 400,
|
|
1518
|
+
headers: { "Content-Type": "application/json" }
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
const { code: codeFromUrl, state: stateFromUrl } = paramsResult.value;
|
|
1522
|
+
try {
|
|
1523
|
+
const cookie = getCookie("state", request, true);
|
|
1524
|
+
const { codeVerifier, state, landingUrl } = cookie ?? {};
|
|
1525
|
+
must(codeVerifier, 'OIDC "codeVerifier" was not present in cookies, ensure that the SSO login was initiated correctly');
|
|
1526
|
+
must(state, 'OIDC "stateVerifier" was not present in cookies, ensure that the SSO login was initiated correctly');
|
|
1527
|
+
must(landingUrl, 'OIDC "landingUrl" was not present in cookies');
|
|
1528
|
+
if (stateFromUrl !== state) {
|
|
1529
|
+
throw new Error('SSO State Verifier failed, the "state" request parameter does not equal the "state" in the SSO cookie');
|
|
1530
|
+
}
|
|
1531
|
+
const tokenResponse = await exchangeCodeForToken(codeFromUrl, codeVerifier, validation, request.url);
|
|
1532
|
+
const user = await parseUser(tokenResponse, validation);
|
|
1533
|
+
if (configWithDefaults.session_store) {
|
|
1534
|
+
try {
|
|
1535
|
+
const sid = user.sso.profile.sid;
|
|
1536
|
+
const sub = user.id;
|
|
1537
|
+
if (sid && sub) {
|
|
1538
|
+
const session = {
|
|
1539
|
+
sid,
|
|
1540
|
+
sub,
|
|
1541
|
+
createdAt: new Date,
|
|
1542
|
+
lastActivityAt: new Date
|
|
1543
|
+
};
|
|
1544
|
+
await configWithDefaults.session_store.create(session);
|
|
1545
|
+
} else {
|
|
1546
|
+
console.warn("Session creation skipped: missing sid or sub in ID token claims");
|
|
1547
|
+
}
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
console.warn("Failed to create session:", error);
|
|
1550
|
+
}
|
|
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
|
+
}
|
|
1577
|
+
return new Response("Authentication successful, redirecting", {
|
|
1578
|
+
status: 302,
|
|
1579
|
+
headers: [
|
|
1580
|
+
["Location", landingUrl],
|
|
1581
|
+
["Set-Cookie", clearCookie("state")],
|
|
1582
|
+
...createJwtCookies(tokenResponse, user.sso.expires)
|
|
1583
|
+
]
|
|
1584
|
+
});
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
console.error("Error during sign-in callback:", error);
|
|
1587
|
+
try {
|
|
1588
|
+
const cookie = getCookie("state", request, true);
|
|
1589
|
+
const { errorUrl } = cookie ?? {};
|
|
1590
|
+
if (errorUrl) {
|
|
1591
|
+
return new Response("Redirecting to error url", {
|
|
1592
|
+
status: 302,
|
|
1593
|
+
headers: [["Location", errorUrl]]
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
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 };
|
|
2237
|
+
}
|
|
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);
|
|
2247
|
+
}
|
|
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));
|
|
2391
|
+
}
|
|
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
|
|
2411
|
+
};
|
|
2412
|
+
const header = {
|
|
2413
|
+
alg: cfg.algorithm,
|
|
2414
|
+
typ: "JWT",
|
|
2415
|
+
kid: cfg.key_id
|
|
224
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}`;
|
|
225
2422
|
}
|
|
226
|
-
async function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
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);
|
|
237
2433
|
const response = await fetch(tokenUrl, {
|
|
238
2434
|
method: "POST",
|
|
239
2435
|
headers: {
|
|
@@ -244,24 +2440,41 @@ function sso(config) {
|
|
|
244
2440
|
});
|
|
245
2441
|
const data = await response.json();
|
|
246
2442
|
if (!response.ok) {
|
|
247
|
-
console.error("Token
|
|
248
|
-
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());
|
|
249
2445
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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("; ")}`);
|
|
2451
|
+
}
|
|
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
|
+
});
|
|
255
2467
|
}
|
|
256
|
-
async function
|
|
2468
|
+
async function acquireTokenClientCredentials(scope, validation) {
|
|
2469
|
+
const cfg = configWithDefaults;
|
|
257
2470
|
return retryWithBackoff(async () => {
|
|
258
|
-
|
|
259
|
-
throw new Error("SSO Manager not initialized");
|
|
260
|
-
const tokenUrl = configWithDefaults.token_url;
|
|
2471
|
+
const tokenUrl = cfg.token_url;
|
|
261
2472
|
const body = new URLSearchParams;
|
|
262
|
-
body.append("grant_type", "
|
|
263
|
-
body.append("
|
|
264
|
-
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);
|
|
265
2478
|
const response = await fetch(tokenUrl, {
|
|
266
2479
|
method: "POST",
|
|
267
2480
|
headers: {
|
|
@@ -272,22 +2485,110 @@ function sso(config) {
|
|
|
272
2485
|
});
|
|
273
2486
|
const data = await response.json();
|
|
274
2487
|
if (!response.ok) {
|
|
275
|
-
console.error("Token
|
|
276
|
-
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());
|
|
277
2490
|
}
|
|
278
|
-
|
|
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;
|
|
279
2511
|
});
|
|
280
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
|
+
}
|
|
281
2574
|
async function revokeToken(token) {
|
|
2575
|
+
ensureInitialized();
|
|
282
2576
|
try {
|
|
283
|
-
if (!
|
|
284
|
-
|
|
285
|
-
|
|
2577
|
+
if (!config.revocation_endpoint) {
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
286
2580
|
const body = new URLSearchParams;
|
|
287
2581
|
body.append("token", token);
|
|
288
|
-
body.append("token_type_hint", "
|
|
289
|
-
|
|
290
|
-
|
|
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, {
|
|
291
2592
|
method: "POST",
|
|
292
2593
|
headers: {
|
|
293
2594
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
@@ -299,368 +2600,499 @@ function sso(config) {
|
|
|
299
2600
|
} else {
|
|
300
2601
|
console.log("Token revoked successfully");
|
|
301
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
|
+
}
|
|
302
2614
|
} catch (error) {
|
|
303
2615
|
console.warn("Error revoking token:", error);
|
|
304
2616
|
}
|
|
305
2617
|
}
|
|
306
2618
|
async function fetchJwks() {
|
|
307
|
-
|
|
308
|
-
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);
|
|
309
2625
|
if (cached)
|
|
310
2626
|
return cached;
|
|
311
2627
|
return retryWithBackoff(async () => {
|
|
312
|
-
if (!configWithDefaults)
|
|
313
|
-
throw new Error("SSO Manager not initialized");
|
|
314
2628
|
const response = await fetch(url);
|
|
315
2629
|
if (!response.ok)
|
|
316
2630
|
throw new Error("Failed to fetch JWKS");
|
|
317
2631
|
const jwks = await response.json();
|
|
318
|
-
|
|
2632
|
+
jwksCache2.set(url, jwks);
|
|
319
2633
|
return jwks;
|
|
320
2634
|
});
|
|
321
2635
|
}
|
|
322
|
-
async function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
throw error;
|
|
331
|
-
}
|
|
332
|
-
if (attempt === maxRetries) {
|
|
333
|
-
throw lastError;
|
|
334
|
-
}
|
|
335
|
-
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
336
|
-
const jitter = Math.random() * 0.1 * delay;
|
|
337
|
-
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
338
|
-
console.warn(`Retry attempt ${attempt + 1} after ${delay + jitter}ms delay`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
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"]);
|
|
342
2644
|
}
|
|
343
|
-
async function
|
|
2645
|
+
async function parseJWT(token, validation) {
|
|
2646
|
+
ensureInitialized();
|
|
344
2647
|
try {
|
|
345
2648
|
const parts = token.split(".");
|
|
346
2649
|
if (parts.length !== 3)
|
|
347
2650
|
throw new Error("Invalid JWT");
|
|
348
|
-
const header = JSON.parse(
|
|
349
|
-
const payload = JSON.parse(
|
|
350
|
-
const signature = parts[2].replace(/-/g, "+").replace(/_/g, "/");
|
|
2651
|
+
const header = JSON.parse(base64UrlDecode(parts[0]));
|
|
2652
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
351
2653
|
const publicKey = await getPublicKey(header.kid);
|
|
2654
|
+
const signature = parts[2];
|
|
352
2655
|
const encoder = new TextEncoder;
|
|
353
2656
|
const data = encoder.encode(`${parts[0]}.${parts[1]}`);
|
|
354
|
-
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);
|
|
355
2660
|
if (!isValid)
|
|
356
2661
|
throw new Error("Invalid JWT signature");
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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("; ")}`);
|
|
2667
|
+
}
|
|
2668
|
+
return result.value;
|
|
2669
|
+
} catch (error) {
|
|
2670
|
+
console.error("Error verifying JWT:", error);
|
|
2671
|
+
throw error;
|
|
361
2672
|
}
|
|
362
2673
|
}
|
|
363
|
-
function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
["Set-Cookie", createCookie("access", token.access_token, expires)],
|
|
399
|
-
["Set-Cookie", createCookie("id", token.id_token, expires)],
|
|
400
|
-
["Set-Cookie", createCookie("refresh", token.refresh_token ?? "", expires)],
|
|
401
|
-
["Set-Cookie", createCookie("control", control, expires)]
|
|
402
|
-
];
|
|
403
|
-
}
|
|
404
|
-
async function getTokenFromCookies(req) {
|
|
405
|
-
const access_token = getCookie("access", req);
|
|
406
|
-
const id_token = getCookie("id", req);
|
|
407
|
-
const refresh_token = getCookie("refresh", req);
|
|
408
|
-
const control = getCookie("control", req, true);
|
|
409
|
-
if (!access_token || !id_token || !refresh_token || !control) {
|
|
410
|
-
return { tokens: undefined, refreshHeaders: [] };
|
|
411
|
-
}
|
|
412
|
-
let tokenResponse = {
|
|
413
|
-
access_token,
|
|
414
|
-
id_token,
|
|
415
|
-
refresh_token,
|
|
416
|
-
...control
|
|
417
|
-
};
|
|
418
|
-
if (control.expires && refresh_token && Date.now() > new Date(control.expires).getTime()) {
|
|
419
|
-
tokenResponse = await refreshToken(refresh_token);
|
|
420
|
-
const user = await parseUser(tokenResponse);
|
|
421
|
-
const refreshHeaders = createJwtCookies(tokenResponse, user.sso.expires);
|
|
422
|
-
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
|
+
};
|
|
423
2709
|
}
|
|
424
|
-
return { tokens: tokenResponse, refreshHeaders: [] };
|
|
425
|
-
}
|
|
426
|
-
async function getJwt(request) {
|
|
427
|
-
const { tokens } = await getTokenFromCookies(request);
|
|
428
|
-
if (!tokens)
|
|
429
|
-
return;
|
|
430
|
-
return tokens.access_token;
|
|
431
2710
|
}
|
|
432
|
-
function
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
|
|
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.");
|
|
436
2715
|
}
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
} else if (typeof expires === "number") {
|
|
441
|
-
exp = `Max-Age=${expires}`;
|
|
442
|
-
} else {
|
|
443
|
-
throw new Error("Invalid expires type", expires);
|
|
2716
|
+
const authHeader = request.headers.get("Authorization");
|
|
2717
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
2718
|
+
return;
|
|
444
2719
|
}
|
|
445
|
-
|
|
446
|
-
|
|
2720
|
+
const token = authHeader.substring(7);
|
|
2721
|
+
const result = await validateToken(token, configWithDefaults.validation);
|
|
2722
|
+
if (!result.valid || !result.claims) {
|
|
2723
|
+
return;
|
|
447
2724
|
}
|
|
448
|
-
return
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const header = req.headers.get("cookie");
|
|
455
|
-
if (!header)
|
|
456
|
-
return null;
|
|
457
|
-
const cookie = header.split(";").find((row) => row.trim().startsWith(`${configWithDefaults.cookies_prefix}.${name}=`));
|
|
458
|
-
if (!cookie)
|
|
459
|
-
return null;
|
|
460
|
-
const val = cookie.split("=")[1].trim();
|
|
461
|
-
if (!parse)
|
|
462
|
-
return val;
|
|
463
|
-
const str = atob(val);
|
|
464
|
-
return JSON.parse(str);
|
|
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
|
+
};
|
|
465
2731
|
}
|
|
466
|
-
async function handler(request
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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;
|
|
471
2739
|
const path = new URL(request.url).pathname;
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return
|
|
477
|
-
|
|
478
|
-
errorUrl
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
if (userUrl === path) {
|
|
482
|
-
const { tokens, refreshHeaders } = await getTokenFromCookies(request);
|
|
483
|
-
if (!tokens) {
|
|
484
|
-
return new Response("User not logged in", { status: 401 });
|
|
485
|
-
}
|
|
486
|
-
const user = await parseUser(tokens);
|
|
487
|
-
return new Response(JSON.stringify(user), {
|
|
488
|
-
headers: [["Content-Type", "application/json"], ...refreshHeaders]
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
if (tokenUrl === path) {
|
|
492
|
-
const { tokens, refreshHeaders } = await getTokenFromCookies(request);
|
|
493
|
-
if (!tokens) {
|
|
494
|
-
return new Response("User not logged in", { status: 401 });
|
|
495
|
-
}
|
|
496
|
-
return new Response(JSON.stringify({
|
|
497
|
-
token: tokens.access_token,
|
|
498
|
-
expires: tokens.expires
|
|
499
|
-
}), {
|
|
500
|
-
headers: [["Content-Type", "application/json"], ...refreshHeaders]
|
|
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"]]
|
|
501
2746
|
});
|
|
502
2747
|
}
|
|
503
|
-
if (
|
|
504
|
-
const
|
|
505
|
-
if (!
|
|
506
|
-
return new Response(
|
|
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
|
+
});
|
|
507
2755
|
}
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
headers: refreshHeaders
|
|
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"]]
|
|
514
2761
|
});
|
|
515
2762
|
}
|
|
516
|
-
if (
|
|
517
|
-
return logout(request, { landingUrl: landingUrl || "/" });
|
|
518
|
-
}
|
|
519
|
-
if (jwksUrl === path) {
|
|
2763
|
+
if (jwksUrl === path && request.method === "GET") {
|
|
520
2764
|
const jwks = await fetchJwks();
|
|
521
2765
|
return new Response(JSON.stringify(jwks), {
|
|
522
2766
|
headers: [["Content-Type", "application/json"]]
|
|
523
2767
|
});
|
|
524
2768
|
}
|
|
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
|
+
}
|
|
525
2775
|
return new Response("Not Found", { status: 404 });
|
|
526
2776
|
}
|
|
527
2777
|
return {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
2778
|
+
...configWithDefaults,
|
|
2779
|
+
getToken,
|
|
2780
|
+
refreshToken,
|
|
2781
|
+
generateJWTAssertion,
|
|
2782
|
+
revokeToken,
|
|
2783
|
+
validateToken,
|
|
2784
|
+
getWorkload,
|
|
2785
|
+
parseJWT,
|
|
534
2786
|
handler
|
|
535
2787
|
};
|
|
536
2788
|
}
|
|
537
2789
|
|
|
538
|
-
// src/
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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;
|
|
2797
|
+
}
|
|
2798
|
+
async getByExternalId(externalId) {
|
|
2799
|
+
const id = this.externalIdIndex.get(externalId);
|
|
2800
|
+
if (!id)
|
|
2801
|
+
return null;
|
|
2802
|
+
return this.groups.get(id) ?? null;
|
|
2803
|
+
}
|
|
2804
|
+
async getByDisplayName(displayName) {
|
|
2805
|
+
const id = this.displayNameIndex.get(displayName.toLowerCase());
|
|
2806
|
+
if (!id)
|
|
2807
|
+
return null;
|
|
2808
|
+
return this.groups.get(id) ?? null;
|
|
2809
|
+
}
|
|
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
|
+
}
|
|
544
2822
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
} catch (cause) {
|
|
549
|
-
throw new Error("Error retrieving secret", { cause });
|
|
2823
|
+
this.groups.set(group.id, group);
|
|
2824
|
+
if (group.externalId) {
|
|
2825
|
+
this.externalIdIndex.set(group.externalId, group.id);
|
|
550
2826
|
}
|
|
2827
|
+
this.displayNameIndex.set(group.displayName.toLowerCase(), group.id);
|
|
551
2828
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
2829
|
+
async delete(id) {
|
|
2830
|
+
const group = this.groups.get(id);
|
|
2831
|
+
if (group) {
|
|
2832
|
+
if (group.externalId) {
|
|
2833
|
+
this.externalIdIndex.delete(group.externalId);
|
|
2834
|
+
}
|
|
2835
|
+
this.displayNameIndex.delete(group.displayName.toLowerCase());
|
|
2836
|
+
}
|
|
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`);
|
|
557
2843
|
}
|
|
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;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
async removeMember(groupId, memberId) {
|
|
2852
|
+
const group = this.groups.get(groupId);
|
|
2853
|
+
if (!group) {
|
|
2854
|
+
throw new Error(`Group ${groupId} not found`);
|
|
2855
|
+
}
|
|
2856
|
+
if (group.members) {
|
|
2857
|
+
group.members = group.members.filter((m) => m.value !== memberId);
|
|
2858
|
+
group.updatedAt = new Date;
|
|
2859
|
+
}
|
|
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
|
+
}
|
|
2908
|
+
}
|
|
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;
|
|
2959
|
+
return {
|
|
2960
|
+
appId,
|
|
2961
|
+
companyId,
|
|
2962
|
+
companyName,
|
|
2963
|
+
environmentType,
|
|
2964
|
+
email,
|
|
2965
|
+
webhookUrl
|
|
558
2966
|
};
|
|
559
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
|
+
}
|
|
560
2984
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
message: "code is required",
|
|
592
|
-
path: ["code"]
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
if ("state" in params) {
|
|
596
|
-
if (typeof params.state === "string" || params.state === undefined) {
|
|
597
|
-
result.state = params.state;
|
|
598
|
-
} else {
|
|
599
|
-
issues.push({
|
|
600
|
-
message: "state must be a string",
|
|
601
|
-
path: ["state"]
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
if ("session_state" in params) {
|
|
606
|
-
if (typeof params.session_state === "string" || params.session_state === undefined) {
|
|
607
|
-
result.session_state = params.session_state;
|
|
608
|
-
} else {
|
|
609
|
-
issues.push({
|
|
610
|
-
message: "session_state must be a string",
|
|
611
|
-
path: ["session_state"]
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if ("error" in params) {
|
|
616
|
-
if (typeof params.error === "string") {
|
|
617
|
-
result.error = params.error;
|
|
618
|
-
} else {
|
|
619
|
-
issues.push({
|
|
620
|
-
message: "error must be a string",
|
|
621
|
-
path: ["error"]
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
if ("error_description" in params) {
|
|
625
|
-
if (typeof params.error_description === "string" || params.error_description === undefined) {
|
|
626
|
-
result.error_description = params.error_description;
|
|
627
|
-
} else {
|
|
628
|
-
issues.push({
|
|
629
|
-
message: "error_description must be a string",
|
|
630
|
-
path: ["error_description"]
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
if ("error_uri" in params) {
|
|
635
|
-
if (typeof params.error_uri === "string" || params.error_uri === undefined) {
|
|
636
|
-
result.error_uri = params.error_uri;
|
|
637
|
-
} else {
|
|
638
|
-
issues.push({
|
|
639
|
-
message: "error_uri must be a string",
|
|
640
|
-
path: ["error_uri"]
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
if ("iss" in params) {
|
|
646
|
-
if (typeof params.iss === "string" || params.iss === undefined) {
|
|
647
|
-
result.iss = params.iss;
|
|
648
|
-
} else {
|
|
649
|
-
issues.push({
|
|
650
|
-
message: "iss must be a string",
|
|
651
|
-
path: ["iss"]
|
|
652
|
-
});
|
|
653
|
-
}
|
|
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
|
+
}
|
|
3001
|
+
}
|
|
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);
|
|
654
3015
|
}
|
|
655
|
-
|
|
656
|
-
|
|
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);
|
|
3023
|
+
}
|
|
3024
|
+
appIds.add(tenant.appId);
|
|
3025
|
+
return tenant;
|
|
3026
|
+
}
|
|
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);
|
|
657
3035
|
}
|
|
658
|
-
return { value: result };
|
|
659
3036
|
}
|
|
660
3037
|
}
|
|
661
|
-
|
|
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);
|
|
662
3093
|
}
|
|
663
|
-
|
|
3094
|
+
|
|
3095
|
+
// packages/react/src/server.ts
|
|
664
3096
|
function getSSO(config) {
|
|
665
3097
|
const es = getES(config?.es);
|
|
666
3098
|
if (!es.sso) {
|
|
@@ -669,7 +3101,7 @@ function getSSO(config) {
|
|
|
669
3101
|
}
|
|
670
3102
|
return es.sso;
|
|
671
3103
|
}
|
|
672
|
-
function
|
|
3104
|
+
function unavailable2() {
|
|
673
3105
|
new Response(JSON.stringify({ error: "SSO Unavailable" }), {
|
|
674
3106
|
status: 503,
|
|
675
3107
|
statusText: "SSO Unavailable",
|
|
@@ -682,60 +3114,84 @@ async function getUser(request, config) {
|
|
|
682
3114
|
async function getRequiredUser(request, config) {
|
|
683
3115
|
const sso2 = getSSO(config);
|
|
684
3116
|
if (!sso2)
|
|
685
|
-
throw
|
|
3117
|
+
throw unavailable2();
|
|
686
3118
|
return sso2.getRequiredUser(request);
|
|
687
3119
|
}
|
|
688
3120
|
async function initiateLogin(config) {
|
|
689
3121
|
const sso2 = getSSO(config);
|
|
690
3122
|
if (!sso2)
|
|
691
|
-
throw
|
|
3123
|
+
throw unavailable2();
|
|
692
3124
|
return sso2.initiateLogin(config);
|
|
693
3125
|
}
|
|
694
3126
|
async function callback(request, config) {
|
|
695
3127
|
const sso2 = getSSO(config);
|
|
696
3128
|
if (!sso2)
|
|
697
|
-
throw
|
|
3129
|
+
throw unavailable2();
|
|
698
3130
|
return sso2.callbackHandler(request);
|
|
699
3131
|
}
|
|
700
3132
|
async function handler(request, config) {
|
|
701
3133
|
const sso2 = getSSO(config);
|
|
702
3134
|
if (!sso2)
|
|
703
|
-
throw
|
|
704
|
-
return sso2.handler(request
|
|
3135
|
+
throw unavailable2();
|
|
3136
|
+
return sso2.handler(request);
|
|
3137
|
+
}
|
|
3138
|
+
// packages/react/src/session-store.ts
|
|
3139
|
+
class InMemorySessionStore {
|
|
3140
|
+
sessions = new Map;
|
|
3141
|
+
async create(session) {
|
|
3142
|
+
if (this.sessions.has(session.sid)) {
|
|
3143
|
+
throw new Error(`Session with sid ${session.sid} already exists`);
|
|
3144
|
+
}
|
|
3145
|
+
this.sessions.set(session.sid, session);
|
|
3146
|
+
}
|
|
3147
|
+
async get(sid) {
|
|
3148
|
+
return this.sessions.get(sid) ?? null;
|
|
3149
|
+
}
|
|
3150
|
+
async update(sid, data) {
|
|
3151
|
+
const session = this.sessions.get(sid);
|
|
3152
|
+
if (!session) {
|
|
3153
|
+
throw new Error(`Session with sid ${sid} not found`);
|
|
3154
|
+
}
|
|
3155
|
+
const updated = { ...session, ...data };
|
|
3156
|
+
this.sessions.set(sid, updated);
|
|
3157
|
+
}
|
|
3158
|
+
async delete(sid) {
|
|
3159
|
+
this.sessions.delete(sid);
|
|
3160
|
+
}
|
|
705
3161
|
}
|
|
706
|
-
// src/ui/sign-in-loading.tsx
|
|
707
|
-
import {
|
|
3162
|
+
// packages/react/src/ui/sign-in-loading.tsx
|
|
3163
|
+
import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
|
|
708
3164
|
function SignInLoading({ complete = false, children }) {
|
|
709
3165
|
const { isLoading } = useUser();
|
|
710
3166
|
if (isLoading && !complete)
|
|
711
|
-
return /* @__PURE__ */
|
|
3167
|
+
return /* @__PURE__ */ jsxDEV(Fragment, {
|
|
712
3168
|
children
|
|
713
|
-
});
|
|
3169
|
+
}, undefined, false, undefined, this);
|
|
714
3170
|
return null;
|
|
715
3171
|
}
|
|
716
|
-
// src/ui/signed-in.tsx
|
|
717
|
-
import {
|
|
3172
|
+
// packages/react/src/ui/signed-in.tsx
|
|
3173
|
+
import { jsxDEV as jsxDEV2, Fragment as Fragment2 } from "react/jsx-dev-runtime";
|
|
718
3174
|
function SignedIn({ children }) {
|
|
719
3175
|
const { user } = useUser();
|
|
720
3176
|
if (user)
|
|
721
|
-
return /* @__PURE__ */
|
|
3177
|
+
return /* @__PURE__ */ jsxDEV2(Fragment2, {
|
|
722
3178
|
children
|
|
723
|
-
});
|
|
3179
|
+
}, undefined, false, undefined, this);
|
|
724
3180
|
return null;
|
|
725
3181
|
}
|
|
726
|
-
// src/ui/signed-out.tsx
|
|
727
|
-
import {
|
|
3182
|
+
// packages/react/src/ui/signed-out.tsx
|
|
3183
|
+
import { jsxDEV as jsxDEV3, Fragment as Fragment3 } from "react/jsx-dev-runtime";
|
|
728
3184
|
function SignedOut({ children }) {
|
|
729
3185
|
const { user, isLoading } = useUser();
|
|
730
3186
|
if (user || isLoading)
|
|
731
3187
|
return null;
|
|
732
|
-
return /* @__PURE__ */
|
|
3188
|
+
return /* @__PURE__ */ jsxDEV3(Fragment3, {
|
|
733
3189
|
children
|
|
734
|
-
});
|
|
3190
|
+
}, undefined, false, undefined, this);
|
|
735
3191
|
}
|
|
736
|
-
// src/ui/sso-provider.tsx
|
|
3192
|
+
// packages/react/src/ui/sso-provider.tsx
|
|
737
3193
|
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
738
|
-
import {
|
|
3194
|
+
import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
|
|
739
3195
|
var CTX = createContext(undefined);
|
|
740
3196
|
var generateStorageKey = (tenantId) => {
|
|
741
3197
|
return `es-sso-user-${tenantId.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`;
|
|
@@ -875,10 +3331,10 @@ function SSOProvider({
|
|
|
875
3331
|
tokenUrl,
|
|
876
3332
|
refreshUrl
|
|
877
3333
|
};
|
|
878
|
-
return /* @__PURE__ */
|
|
3334
|
+
return /* @__PURE__ */ jsxDEV4(CTX.Provider, {
|
|
879
3335
|
value: contextValue,
|
|
880
3336
|
children
|
|
881
|
-
});
|
|
3337
|
+
}, undefined, false, undefined, this);
|
|
882
3338
|
}
|
|
883
3339
|
function useUser() {
|
|
884
3340
|
const context = useContext(CTX);
|
|
@@ -999,38 +3455,146 @@ async function logout(logoutUrl) {
|
|
|
999
3455
|
};
|
|
1000
3456
|
}
|
|
1001
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
|
+
}
|
|
1002
3509
|
|
|
1003
|
-
// src/index.ts
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
3510
|
+
// packages/react/src/index.ts
|
|
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) {
|
|
1010
3542
|
vaultUrl = "https://vault-ionite.ionite.dev/v1/secret/data";
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
token: "hvs.VGhD2hmXDH9PmZjTacZx0G5K"
|
|
1015
|
-
}
|
|
3543
|
+
secret = {
|
|
3544
|
+
path: `public/${appId}`,
|
|
3545
|
+
token: "hvs.VGhD2hmXDH9PmZjTacZx0G5K"
|
|
1016
3546
|
};
|
|
1017
|
-
} else if (
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
} else {
|
|
1023
|
-
|
|
3547
|
+
} else if (vaultUrl && vaultToken && vaultPath) {
|
|
3548
|
+
secret = {
|
|
3549
|
+
path: vaultPath,
|
|
3550
|
+
token: vaultToken
|
|
3551
|
+
};
|
|
3552
|
+
} else if (!vaultUrl || !vaultToken || !vaultPath) {
|
|
3553
|
+
const cmd = `${process.versions.bun ? "bun" : "npm"} ionite login --app ${appId}`;
|
|
3554
|
+
throw new Error(`@enterprisestandard configuration missing.
|
|
3555
|
+
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.`);
|
|
1024
3556
|
}
|
|
1025
3557
|
const defaultInstance2 = getDefaultInstance();
|
|
1026
|
-
const vaultClient =
|
|
3558
|
+
const vaultClient = vault(vaultUrl);
|
|
3559
|
+
const ssoValidation = extractSsoValidation(initConfig?.validation);
|
|
3560
|
+
const workloadValidation = extractWorkloadValidation(initConfig?.validation);
|
|
3561
|
+
let vaultData = {};
|
|
3562
|
+
if (secret) {
|
|
3563
|
+
vaultData = await vaultClient.getSecret(secret.path, secret.token);
|
|
3564
|
+
}
|
|
3565
|
+
const workloadConfig = {
|
|
3566
|
+
...vaultData.workload,
|
|
3567
|
+
...initConfig?.workload,
|
|
3568
|
+
jwks_uri: initConfig?.workload?.jwks_uri ?? vaultData.workload?.jwks_uri,
|
|
3569
|
+
token_url: initConfig?.workload?.token_url ?? vaultData.workload?.token_url,
|
|
3570
|
+
validation: initConfig?.workload?.validation ?? workloadValidation
|
|
3571
|
+
};
|
|
3572
|
+
const workloadInstance = workload(workloadConfig);
|
|
3573
|
+
const ssoConfig = {
|
|
3574
|
+
...vaultData.sso,
|
|
3575
|
+
...initConfig?.sso,
|
|
3576
|
+
validation: initConfig?.sso?.validation ?? ssoValidation
|
|
3577
|
+
};
|
|
3578
|
+
const ssoInstance = sso(ssoConfig);
|
|
3579
|
+
const iamConfig = {
|
|
3580
|
+
...vaultData.iam,
|
|
3581
|
+
...initConfig?.iam
|
|
3582
|
+
};
|
|
3583
|
+
const iamInstance = iam(iamConfig, workloadInstance);
|
|
3584
|
+
const mergedConfig = {
|
|
3585
|
+
defaultInstance: initConfig?.defaultInstance,
|
|
3586
|
+
sso: ssoConfig,
|
|
3587
|
+
iam: iamConfig,
|
|
3588
|
+
workload: workloadConfig,
|
|
3589
|
+
validation: initConfig?.validation
|
|
3590
|
+
};
|
|
1027
3591
|
const result = {
|
|
1028
|
-
|
|
1029
|
-
ioniteUrl,
|
|
3592
|
+
...mergedConfig,
|
|
1030
3593
|
defaultInstance: initConfig?.defaultInstance || initConfig?.defaultInstance !== false && !defaultInstance2,
|
|
1031
3594
|
vault: vaultClient,
|
|
1032
|
-
sso:
|
|
1033
|
-
iam:
|
|
3595
|
+
sso: ssoInstance,
|
|
3596
|
+
iam: iamInstance,
|
|
3597
|
+
workload: workloadInstance
|
|
1034
3598
|
};
|
|
1035
3599
|
if (result.defaultInstance) {
|
|
1036
3600
|
if (defaultInstance2) {
|
|
@@ -1041,18 +3605,46 @@ async function enterpriseStandard(appId, appKey, initConfig) {
|
|
|
1041
3605
|
return result;
|
|
1042
3606
|
}
|
|
1043
3607
|
export {
|
|
3608
|
+
workloadTokenResponseSchema,
|
|
3609
|
+
workloadHandler,
|
|
3610
|
+
workload,
|
|
3611
|
+
vault,
|
|
3612
|
+
validateWorkloadToken,
|
|
3613
|
+
userSchema,
|
|
1044
3614
|
useUser,
|
|
1045
3615
|
useToken,
|
|
3616
|
+
tokenResponseSchema,
|
|
3617
|
+
sso,
|
|
3618
|
+
serializeESConfig,
|
|
3619
|
+
sendTenantWebhook,
|
|
3620
|
+
revokeWorkloadToken,
|
|
3621
|
+
parseTenantRequest,
|
|
1046
3622
|
oidcCallbackSchema,
|
|
1047
3623
|
logout,
|
|
3624
|
+
jwtAssertionClaimsSchema,
|
|
1048
3625
|
initiateLogin,
|
|
3626
|
+
idTokenClaimsSchema,
|
|
3627
|
+
iam,
|
|
1049
3628
|
handler,
|
|
3629
|
+
groupResourceSchema,
|
|
3630
|
+
getWorkloadToken,
|
|
3631
|
+
getWorkload,
|
|
1050
3632
|
getUser,
|
|
1051
3633
|
getRequiredUser,
|
|
3634
|
+
getES,
|
|
3635
|
+
getDefaultInstance,
|
|
1052
3636
|
enterpriseStandard,
|
|
1053
3637
|
callback,
|
|
3638
|
+
TenantRequestError,
|
|
1054
3639
|
SignedOut,
|
|
1055
3640
|
SignedIn,
|
|
1056
3641
|
SignInLoading,
|
|
1057
|
-
SSOProvider
|
|
3642
|
+
SSOProvider,
|
|
3643
|
+
InMemoryWorkloadTokenStore,
|
|
3644
|
+
InMemoryUserStore,
|
|
3645
|
+
InMemoryTenantStore,
|
|
3646
|
+
InMemorySessionStore,
|
|
3647
|
+
InMemoryGroupStore
|
|
1058
3648
|
};
|
|
3649
|
+
|
|
3650
|
+
//# debugId=BB2BA44916C99DF364756E2164756E21
|