@enterprisestandard/react 0.0.3-beta.3 → 0.0.5
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/enterprise-user.d.ts +2 -0
- package/dist/enterprise-user.d.ts.map +1 -0
- package/dist/iam.d.ts +5 -11
- package/dist/iam.d.ts.map +1 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +587 -150
- package/dist/oidc-schema.d.ts +43 -0
- package/dist/oidc-schema.d.ts.map +1 -0
- package/dist/scim-schema.d.ts +356 -0
- package/dist/scim-schema.d.ts.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/session-store.d.ts +179 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/sso.d.ts +30 -15
- package/dist/sso.d.ts.map +1 -0
- package/dist/standard-schema.d.ts +1 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/ui/sign-in-loading.d.ts +1 -0
- package/dist/ui/sign-in-loading.d.ts.map +1 -0
- package/dist/ui/signed-in.d.ts +1 -0
- package/dist/ui/signed-in.d.ts.map +1 -0
- package/dist/ui/signed-out.d.ts +1 -0
- package/dist/ui/signed-out.d.ts.map +1 -0
- package/dist/ui/sso-provider.d.ts +5 -0
- package/dist/ui/sso-provider.d.ts.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/vault.d.ts +4 -3
- package/dist/vault.d.ts.map +1 -0
- package/package.json +14 -5
package/dist/index.js
CHANGED
|
@@ -1,9 +1,287 @@
|
|
|
1
1
|
// src/iam.ts
|
|
2
2
|
async function iam(config) {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/oidc-schema.ts
|
|
7
|
+
function oidcCallbackSchema(vendor) {
|
|
3
8
|
return {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
"~standard": {
|
|
10
|
+
version: 1,
|
|
11
|
+
vendor,
|
|
12
|
+
validate: (value) => {
|
|
13
|
+
if (typeof value !== "object" || value === null) {
|
|
14
|
+
return {
|
|
15
|
+
issues: [
|
|
16
|
+
{
|
|
17
|
+
message: "Expected an object"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const params = value;
|
|
23
|
+
const issues = [];
|
|
24
|
+
const result = {};
|
|
25
|
+
if ("code" in params) {
|
|
26
|
+
if (typeof params.code === "string") {
|
|
27
|
+
result.code = params.code;
|
|
28
|
+
} else {
|
|
29
|
+
issues.push({
|
|
30
|
+
message: "code must be a string",
|
|
31
|
+
path: ["code"]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
} else if (!("error" in params)) {
|
|
35
|
+
issues.push({
|
|
36
|
+
message: "code is required",
|
|
37
|
+
path: ["code"]
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if ("state" in params) {
|
|
41
|
+
if (typeof params.state === "string" || params.state === undefined) {
|
|
42
|
+
result.state = params.state;
|
|
43
|
+
} else {
|
|
44
|
+
issues.push({
|
|
45
|
+
message: "state must be a string",
|
|
46
|
+
path: ["state"]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if ("session_state" in params) {
|
|
51
|
+
if (typeof params.session_state === "string" || params.session_state === undefined) {
|
|
52
|
+
result.session_state = params.session_state;
|
|
53
|
+
} else {
|
|
54
|
+
issues.push({
|
|
55
|
+
message: "session_state must be a string",
|
|
56
|
+
path: ["session_state"]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if ("error" in params) {
|
|
61
|
+
if (typeof params.error === "string") {
|
|
62
|
+
result.error = params.error;
|
|
63
|
+
} else {
|
|
64
|
+
issues.push({
|
|
65
|
+
message: "error must be a string",
|
|
66
|
+
path: ["error"]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if ("error_description" in params) {
|
|
70
|
+
if (typeof params.error_description === "string" || params.error_description === undefined) {
|
|
71
|
+
result.error_description = params.error_description;
|
|
72
|
+
} else {
|
|
73
|
+
issues.push({
|
|
74
|
+
message: "error_description must be a string",
|
|
75
|
+
path: ["error_description"]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if ("error_uri" in params) {
|
|
80
|
+
if (typeof params.error_uri === "string" || params.error_uri === undefined) {
|
|
81
|
+
result.error_uri = params.error_uri;
|
|
82
|
+
} else {
|
|
83
|
+
issues.push({
|
|
84
|
+
message: "error_uri must be a string",
|
|
85
|
+
path: ["error_uri"]
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if ("iss" in params) {
|
|
91
|
+
if (typeof params.iss === "string" || params.iss === undefined) {
|
|
92
|
+
result.iss = params.iss;
|
|
93
|
+
} else {
|
|
94
|
+
issues.push({
|
|
95
|
+
message: "iss must be a string",
|
|
96
|
+
path: ["iss"]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (issues.length > 0) {
|
|
101
|
+
return { issues };
|
|
102
|
+
}
|
|
103
|
+
return { value: result };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function tokenResponseSchema(vendor) {
|
|
109
|
+
return {
|
|
110
|
+
"~standard": {
|
|
111
|
+
version: 1,
|
|
112
|
+
vendor,
|
|
113
|
+
validate: (value) => {
|
|
114
|
+
if (typeof value !== "object" || value === null) {
|
|
115
|
+
return {
|
|
116
|
+
issues: [
|
|
117
|
+
{
|
|
118
|
+
message: "Expected an object"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const response = value;
|
|
124
|
+
const issues = [];
|
|
125
|
+
const result = {};
|
|
126
|
+
if ("access_token" in response) {
|
|
127
|
+
if (typeof response.access_token === "string") {
|
|
128
|
+
result.access_token = response.access_token;
|
|
129
|
+
} else {
|
|
130
|
+
issues.push({
|
|
131
|
+
message: "access_token must be a string",
|
|
132
|
+
path: ["access_token"]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
issues.push({
|
|
137
|
+
message: "access_token is required",
|
|
138
|
+
path: ["access_token"]
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if ("id_token" in response) {
|
|
142
|
+
if (typeof response.id_token === "string") {
|
|
143
|
+
result.id_token = response.id_token;
|
|
144
|
+
} else {
|
|
145
|
+
issues.push({
|
|
146
|
+
message: "id_token must be a string",
|
|
147
|
+
path: ["id_token"]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
issues.push({
|
|
152
|
+
message: "id_token is required",
|
|
153
|
+
path: ["id_token"]
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if ("token_type" in response) {
|
|
157
|
+
if (typeof response.token_type === "string") {
|
|
158
|
+
result.token_type = response.token_type;
|
|
159
|
+
} else {
|
|
160
|
+
issues.push({
|
|
161
|
+
message: "token_type must be a string",
|
|
162
|
+
path: ["token_type"]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
issues.push({
|
|
167
|
+
message: "token_type is required",
|
|
168
|
+
path: ["token_type"]
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if ("refresh_token" in response) {
|
|
172
|
+
if (typeof response.refresh_token === "string" || response.refresh_token === undefined) {
|
|
173
|
+
result.refresh_token = response.refresh_token;
|
|
174
|
+
} else {
|
|
175
|
+
issues.push({
|
|
176
|
+
message: "refresh_token must be a string",
|
|
177
|
+
path: ["refresh_token"]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if ("scope" in response) {
|
|
182
|
+
if (typeof response.scope === "string" || response.scope === undefined) {
|
|
183
|
+
result.scope = response.scope;
|
|
184
|
+
} else {
|
|
185
|
+
issues.push({
|
|
186
|
+
message: "scope must be a string",
|
|
187
|
+
path: ["scope"]
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if ("session_state" in response) {
|
|
192
|
+
if (typeof response.session_state === "string" || response.session_state === undefined) {
|
|
193
|
+
result.session_state = response.session_state;
|
|
194
|
+
} else {
|
|
195
|
+
issues.push({
|
|
196
|
+
message: "session_state must be a string",
|
|
197
|
+
path: ["session_state"]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if ("expires" in response) {
|
|
202
|
+
if (typeof response.expires === "string" || response.expires === undefined) {
|
|
203
|
+
result.expires = response.expires;
|
|
204
|
+
} else {
|
|
205
|
+
issues.push({
|
|
206
|
+
message: "expires must be a string",
|
|
207
|
+
path: ["expires"]
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if ("expires_in" in response) {
|
|
212
|
+
if (typeof response.expires_in === "number" || response.expires_in === undefined) {
|
|
213
|
+
result.expires_in = response.expires_in;
|
|
214
|
+
} else {
|
|
215
|
+
issues.push({
|
|
216
|
+
message: "expires_in must be a number",
|
|
217
|
+
path: ["expires_in"]
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if ("refresh_expires_in" in response) {
|
|
222
|
+
if (typeof response.refresh_expires_in === "number" || response.refresh_expires_in === undefined) {
|
|
223
|
+
result.refresh_expires_in = response.refresh_expires_in;
|
|
224
|
+
} else {
|
|
225
|
+
issues.push({
|
|
226
|
+
message: "refresh_expires_in must be a number",
|
|
227
|
+
path: ["refresh_expires_in"]
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (issues.length > 0) {
|
|
232
|
+
return { issues };
|
|
233
|
+
}
|
|
234
|
+
return { value: result };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function idTokenClaimsSchema(vendor) {
|
|
240
|
+
return {
|
|
241
|
+
"~standard": {
|
|
242
|
+
version: 1,
|
|
243
|
+
vendor,
|
|
244
|
+
validate: (value) => {
|
|
245
|
+
if (typeof value !== "object" || value === null) {
|
|
246
|
+
return {
|
|
247
|
+
issues: [
|
|
248
|
+
{
|
|
249
|
+
message: "Expected an object"
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const claims = value;
|
|
255
|
+
const issues = [];
|
|
256
|
+
const result = { ...claims };
|
|
257
|
+
const stringFields = ["iss", "aud", "sub", "sid", "name", "email", "preferred_username", "picture"];
|
|
258
|
+
for (const field of stringFields) {
|
|
259
|
+
if (field in claims && claims[field] !== undefined) {
|
|
260
|
+
if (typeof claims[field] !== "string") {
|
|
261
|
+
issues.push({
|
|
262
|
+
message: `${field} must be a string`,
|
|
263
|
+
path: [field]
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const numberFields = ["exp", "iat"];
|
|
269
|
+
for (const field of numberFields) {
|
|
270
|
+
if (field in claims && claims[field] !== undefined) {
|
|
271
|
+
if (typeof claims[field] !== "number") {
|
|
272
|
+
issues.push({
|
|
273
|
+
message: `${field} must be a number`,
|
|
274
|
+
path: [field]
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (issues.length > 0) {
|
|
280
|
+
return { issues };
|
|
281
|
+
}
|
|
282
|
+
return { value: result };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
7
285
|
};
|
|
8
286
|
}
|
|
9
287
|
|
|
@@ -34,10 +312,17 @@ var jwksCache = new Map;
|
|
|
34
312
|
function sso(config) {
|
|
35
313
|
const configWithDefaults = {
|
|
36
314
|
...config,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
315
|
+
authority: must(config.authority, "Missing 'authority' from SSO Config"),
|
|
316
|
+
token_url: must(config.token_url, "Missing 'token_url' from SSO Config"),
|
|
317
|
+
authorization_url: must(config.authorization_url, "Missing 'authorization_url' from SSO Config"),
|
|
318
|
+
client_id: must(config.client_id, "Missing 'client_id' from SSO Config"),
|
|
319
|
+
redirect_uri: must(config.redirect_uri, "Missing 'redirect_uri' from SSO Config"),
|
|
320
|
+
scope: must(config.scope, "Missing 'scope' from SSO Config"),
|
|
321
|
+
response_type: config.response_type ?? "code",
|
|
322
|
+
cookies_secure: config.cookies_secure !== undefined ? config.cookies_secure : true,
|
|
323
|
+
cookies_same_site: config.cookies_same_site !== undefined ? config.cookies_same_site : "Strict",
|
|
324
|
+
cookies_prefix: config.cookies_prefix ?? `es.sso.${config.client_id}`,
|
|
325
|
+
cookies_path: config.cookies_path ?? "/"
|
|
41
326
|
};
|
|
42
327
|
async function getUser(request) {
|
|
43
328
|
if (!configWithDefaults) {
|
|
@@ -93,16 +378,123 @@ function sso(config) {
|
|
|
93
378
|
}
|
|
94
379
|
});
|
|
95
380
|
}
|
|
96
|
-
async function
|
|
381
|
+
async function logout(request, _config) {
|
|
382
|
+
try {
|
|
383
|
+
const refreshToken2 = getCookie("refresh", request);
|
|
384
|
+
if (refreshToken2) {
|
|
385
|
+
await revokeToken(refreshToken2);
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.warn("Failed to revoke token:", error);
|
|
389
|
+
}
|
|
390
|
+
if (config.session_store) {
|
|
391
|
+
try {
|
|
392
|
+
const user = await getUser(request);
|
|
393
|
+
if (user?.sso?.profile.sid) {
|
|
394
|
+
const sid = user.sso.profile.sid;
|
|
395
|
+
await config.session_store.delete(sid);
|
|
396
|
+
console.log(`Session ${sid} deleted from store`);
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.warn("Failed to delete session:", error);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const clearHeaders = [
|
|
403
|
+
["Set-Cookie", clearCookie("access")],
|
|
404
|
+
["Set-Cookie", clearCookie("id")],
|
|
405
|
+
["Set-Cookie", clearCookie("refresh")],
|
|
406
|
+
["Set-Cookie", clearCookie("control")],
|
|
407
|
+
["Set-Cookie", clearCookie("state")]
|
|
408
|
+
];
|
|
409
|
+
const url = new URL(request.url);
|
|
410
|
+
const redirectTo = url.searchParams.get("redirect");
|
|
411
|
+
if (redirectTo) {
|
|
412
|
+
return new Response("Logged out", {
|
|
413
|
+
status: 302,
|
|
414
|
+
headers: [["Location", redirectTo], ...clearHeaders]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const accept = request.headers.get("accept");
|
|
418
|
+
const isAjax = accept?.includes("application/json") || accept?.includes("text/javascript");
|
|
419
|
+
if (isAjax) {
|
|
420
|
+
return new Response(JSON.stringify({ success: true, message: "Logged out" }), {
|
|
421
|
+
status: 200,
|
|
422
|
+
headers: [["Content-Type", "application/json"], ...clearHeaders]
|
|
423
|
+
});
|
|
424
|
+
} else {
|
|
425
|
+
return new Response(`
|
|
426
|
+
<!DOCTYPE html><html lang="en"><body>
|
|
427
|
+
<h1>Logout Complete</h1>
|
|
428
|
+
<div style="display: none">
|
|
429
|
+
It is not recommended to show the default logout page. Include '?redirect=/someHomePage' or logout asynchronously.
|
|
430
|
+
Check the <a href="https://EnterpriseStandard.com/sso#logout">Enterprise Standard Packages</a> for more information.
|
|
431
|
+
</div>
|
|
432
|
+
</body></html>
|
|
433
|
+
`, {
|
|
434
|
+
status: 200,
|
|
435
|
+
headers: [["Content-Type", "text/html"], ...clearHeaders]
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async function logoutBackChannel(request) {
|
|
440
|
+
if (!configWithDefaults.session_store) {
|
|
441
|
+
return new Response("Back-Channel Logout requires session_store configuration", {
|
|
442
|
+
status: 400,
|
|
443
|
+
statusText: "Bad Request"
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const contentType = request.headers.get("content-type");
|
|
448
|
+
if (!contentType || !contentType.includes("application/x-www-form-urlencoded")) {
|
|
449
|
+
return new Response("Invalid Content-Type, expected application/x-www-form-urlencoded", {
|
|
450
|
+
status: 400
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
const body = await request.text();
|
|
454
|
+
const params = new URLSearchParams(body);
|
|
455
|
+
const logoutToken = params.get("logout_token");
|
|
456
|
+
if (!logoutToken) {
|
|
457
|
+
return new Response("Missing logout_token parameter", { status: 400 });
|
|
458
|
+
}
|
|
459
|
+
const claims = await parseJwt(logoutToken);
|
|
460
|
+
const sid = claims.sid;
|
|
461
|
+
if (!sid) {
|
|
462
|
+
console.warn("Back-Channel Logout: logout_token missing sid claim");
|
|
463
|
+
return new Response("Invalid logout_token: missing sid claim", { status: 400 });
|
|
464
|
+
}
|
|
465
|
+
await configWithDefaults.session_store.delete(sid);
|
|
466
|
+
console.log(`Back-Channel Logout: successfully deleted session ${sid}`);
|
|
467
|
+
return new Response("OK", { status: 200 });
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error("Error during back-channel logout:", error);
|
|
470
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function callbackHandler(request, validation) {
|
|
97
474
|
if (!configWithDefaults) {
|
|
98
475
|
console.error("SSO Manager not initialized");
|
|
99
476
|
return Promise.resolve(new Response("SSO Manager not initialized", { status: 503 }));
|
|
100
477
|
}
|
|
101
478
|
const url = new URL(request.url);
|
|
102
479
|
const params = new URLSearchParams(url.search);
|
|
480
|
+
const callbackParamsValidator = validation?.callbackParams ?? oidcCallbackSchema("builtin");
|
|
481
|
+
const paramsObject = Object.fromEntries(params.entries());
|
|
482
|
+
const paramsResult = await callbackParamsValidator["~standard"].validate(paramsObject);
|
|
483
|
+
if ("issues" in paramsResult) {
|
|
484
|
+
return new Response(JSON.stringify({
|
|
485
|
+
error: "validation_failed",
|
|
486
|
+
message: "OIDC callback parameters validation failed",
|
|
487
|
+
issues: paramsResult.issues?.map((i) => ({
|
|
488
|
+
path: i.path?.join("."),
|
|
489
|
+
message: i.message
|
|
490
|
+
}))
|
|
491
|
+
}), {
|
|
492
|
+
status: 400,
|
|
493
|
+
headers: { "Content-Type": "application/json" }
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
const { code: codeFromUrl, state: stateFromUrl } = paramsResult.value;
|
|
103
497
|
try {
|
|
104
|
-
const codeFromUrl = must(params.get("code"), 'OIDC "code" was not passed as a search param, ensure that the SSO login completed successfully');
|
|
105
|
-
const stateFromUrl = must(params.get("state"), 'OIDC "state" was not passed as a search param, ensure that the SSO login completed successfully');
|
|
106
498
|
const cookie = getCookie("state", request, true);
|
|
107
499
|
const { codeVerifier, state, landingUrl } = cookie ?? {};
|
|
108
500
|
must(codeVerifier, 'OIDC "codeVerifier" was not present in cookies, ensure that the SSO login was initiated correctly');
|
|
@@ -111,8 +503,27 @@ function sso(config) {
|
|
|
111
503
|
if (stateFromUrl !== state) {
|
|
112
504
|
throw new Error('SSO State Verifier failed, the "state" request parameter does not equal the "state" in the SSO cookie');
|
|
113
505
|
}
|
|
114
|
-
const tokenResponse = await exchangeCodeForToken(codeFromUrl, codeVerifier);
|
|
115
|
-
const user = await parseUser(tokenResponse);
|
|
506
|
+
const tokenResponse = await exchangeCodeForToken(codeFromUrl, codeVerifier, validation);
|
|
507
|
+
const user = await parseUser(tokenResponse, validation);
|
|
508
|
+
if (config.session_store) {
|
|
509
|
+
try {
|
|
510
|
+
const sid = user.sso.profile.sid;
|
|
511
|
+
const sub = user.id;
|
|
512
|
+
if (sid && sub) {
|
|
513
|
+
const session = {
|
|
514
|
+
sid,
|
|
515
|
+
sub,
|
|
516
|
+
createdAt: new Date,
|
|
517
|
+
lastActivityAt: new Date
|
|
518
|
+
};
|
|
519
|
+
await config.session_store.create(session);
|
|
520
|
+
} else {
|
|
521
|
+
console.warn("Session creation skipped: missing sid or sub in ID token claims");
|
|
522
|
+
}
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.warn("Failed to create session:", error);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
116
527
|
return new Response("Authentication successful, redirecting", {
|
|
117
528
|
status: 302,
|
|
118
529
|
headers: [
|
|
@@ -141,13 +552,14 @@ function sso(config) {
|
|
|
141
552
|
});
|
|
142
553
|
}
|
|
143
554
|
}
|
|
144
|
-
async function parseUser(token) {
|
|
555
|
+
async function parseUser(token, validation) {
|
|
145
556
|
if (!configWithDefaults)
|
|
146
557
|
throw new Error("SSO Manager not initialized");
|
|
147
|
-
const idToken = await parseJwt(token.id_token);
|
|
558
|
+
const idToken = await parseJwt(token.id_token, validation);
|
|
148
559
|
const expiresIn = Number(token.refresh_expires_in ?? token.expires_in ?? 3600);
|
|
149
560
|
const expires = token.expires ? new Date(token.expires) : new Date(Date.now() + expiresIn * 1000);
|
|
150
561
|
return {
|
|
562
|
+
id: idToken.sub,
|
|
151
563
|
userName: idToken.preferred_username || "",
|
|
152
564
|
name: idToken.name || "",
|
|
153
565
|
email: idToken.email || "",
|
|
@@ -175,7 +587,7 @@ function sso(config) {
|
|
|
175
587
|
}
|
|
176
588
|
};
|
|
177
589
|
}
|
|
178
|
-
async function exchangeCodeForToken(code, codeVerifier) {
|
|
590
|
+
async function exchangeCodeForToken(code, codeVerifier, validation) {
|
|
179
591
|
if (!configWithDefaults)
|
|
180
592
|
throw new Error("SSO Manager not initialized");
|
|
181
593
|
const tokenUrl = configWithDefaults.token_url;
|
|
@@ -199,10 +611,16 @@ function sso(config) {
|
|
|
199
611
|
console.error("Token exchange error:", data);
|
|
200
612
|
throw new Error(`Token exchange failed: ${data.error || response.statusText} - ${data.error_description || ""}`.trim());
|
|
201
613
|
}
|
|
202
|
-
|
|
614
|
+
const tokenResponseValidator = validation?.tokenResponse ?? tokenResponseSchema("builtin");
|
|
615
|
+
const tokenResult = await tokenResponseValidator["~standard"].validate(data);
|
|
616
|
+
if ("issues" in tokenResult) {
|
|
617
|
+
console.error("Token response validation failed:", tokenResult.issues);
|
|
618
|
+
throw new Error(`Token response validation failed: ${tokenResult.issues?.map((i) => i.message).join("; ")}`);
|
|
619
|
+
}
|
|
620
|
+
return tokenResult.value;
|
|
203
621
|
} catch (error) {
|
|
204
622
|
console.error("Error during token exchange:", error);
|
|
205
|
-
throw
|
|
623
|
+
throw error;
|
|
206
624
|
}
|
|
207
625
|
}
|
|
208
626
|
async function refreshToken(refreshToken2) {
|
|
@@ -230,6 +648,33 @@ function sso(config) {
|
|
|
230
648
|
return data;
|
|
231
649
|
});
|
|
232
650
|
}
|
|
651
|
+
async function revokeToken(token) {
|
|
652
|
+
try {
|
|
653
|
+
if (!configWithDefaults)
|
|
654
|
+
throw new Error("SSO Manager not initialized");
|
|
655
|
+
if (!configWithDefaults.revocation_endpoint) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const body = new URLSearchParams;
|
|
659
|
+
body.append("token", token);
|
|
660
|
+
body.append("token_type_hint", "refresh_token");
|
|
661
|
+
body.append("client_id", configWithDefaults.client_id);
|
|
662
|
+
const response = await fetch(configWithDefaults.revocation_endpoint, {
|
|
663
|
+
method: "POST",
|
|
664
|
+
headers: {
|
|
665
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
666
|
+
},
|
|
667
|
+
body: body.toString()
|
|
668
|
+
});
|
|
669
|
+
if (!response.ok) {
|
|
670
|
+
console.warn("Token revocation failed:", response.status, response.statusText);
|
|
671
|
+
} else {
|
|
672
|
+
console.log("Token revoked successfully");
|
|
673
|
+
}
|
|
674
|
+
} catch (error) {
|
|
675
|
+
console.warn("Error revoking token:", error);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
233
678
|
async function fetchJwks() {
|
|
234
679
|
const url = configWithDefaults.jwks_uri || `${configWithDefaults.authority}/protocol/openid-connect/certs`;
|
|
235
680
|
const cached = jwksCache.get(url);
|
|
@@ -267,7 +712,7 @@ function sso(config) {
|
|
|
267
712
|
}
|
|
268
713
|
throw lastError;
|
|
269
714
|
}
|
|
270
|
-
async function parseJwt(token) {
|
|
715
|
+
async function parseJwt(token, validation) {
|
|
271
716
|
try {
|
|
272
717
|
const parts = token.split(".");
|
|
273
718
|
if (parts.length !== 3)
|
|
@@ -281,7 +726,13 @@ function sso(config) {
|
|
|
281
726
|
const isValid = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)), data);
|
|
282
727
|
if (!isValid)
|
|
283
728
|
throw new Error("Invalid JWT signature");
|
|
284
|
-
|
|
729
|
+
const idTokenClaimsValidator = validation?.idTokenClaims ?? idTokenClaimsSchema("builtin");
|
|
730
|
+
const claimsResult = await idTokenClaimsValidator["~standard"].validate(payload);
|
|
731
|
+
if ("issues" in claimsResult) {
|
|
732
|
+
console.error("ID token claims validation failed:", claimsResult.issues);
|
|
733
|
+
throw new Error(`ID token claims validation failed: ${claimsResult.issues?.map((i) => i.message).join("; ")}`);
|
|
734
|
+
}
|
|
735
|
+
return claimsResult.value;
|
|
285
736
|
} catch (e) {
|
|
286
737
|
console.error("Error verifying JWT:", e);
|
|
287
738
|
throw e;
|
|
@@ -357,7 +808,7 @@ function sso(config) {
|
|
|
357
808
|
return tokens.access_token;
|
|
358
809
|
}
|
|
359
810
|
function createCookie(name, value, expires) {
|
|
360
|
-
name = `${configWithDefaults.
|
|
811
|
+
name = `${configWithDefaults.cookies_prefix}.${name}`;
|
|
361
812
|
if (typeof value !== "string") {
|
|
362
813
|
value = btoa(JSON.stringify(value));
|
|
363
814
|
}
|
|
@@ -372,16 +823,16 @@ function sso(config) {
|
|
|
372
823
|
if (value.length > 4000) {
|
|
373
824
|
throw new Error(`Error setting cookie: ${name}. Cookie length is: ${value.length}`);
|
|
374
825
|
}
|
|
375
|
-
return `${name}=${value}; ${exp}; Path=${configWithDefaults.
|
|
826
|
+
return `${name}=${value}; ${exp}; Path=${configWithDefaults.cookies_path}; HttpOnly;${configWithDefaults.cookies_secure ? " Secure;" : ""} SameSite=${configWithDefaults.cookies_same_site};`;
|
|
376
827
|
}
|
|
377
828
|
function clearCookie(name) {
|
|
378
|
-
return `${configWithDefaults.
|
|
829
|
+
return `${configWithDefaults.cookies_prefix}.${name}=; Max-Age=0; Path=${configWithDefaults.cookies_path}; HttpOnly;${configWithDefaults.cookies_secure ? " Secure;" : ""} SameSite=${configWithDefaults.cookies_same_site};`;
|
|
379
830
|
}
|
|
380
831
|
function getCookie(name, req, parse = false) {
|
|
381
832
|
const header = req.headers.get("cookie");
|
|
382
833
|
if (!header)
|
|
383
834
|
return null;
|
|
384
|
-
const cookie = header.split(";").find((row) => row.trim().startsWith(`${configWithDefaults.
|
|
835
|
+
const cookie = header.split(";").find((row) => row.trim().startsWith(`${configWithDefaults.cookies_prefix}.${name}=`));
|
|
385
836
|
if (!cookie)
|
|
386
837
|
return null;
|
|
387
838
|
const val = cookie.split("=")[1].trim();
|
|
@@ -391,12 +842,19 @@ function sso(config) {
|
|
|
391
842
|
return JSON.parse(str);
|
|
392
843
|
}
|
|
393
844
|
async function handler(request, handlerConfig) {
|
|
394
|
-
|
|
395
|
-
if (!loginUrl)
|
|
396
|
-
loginUrl
|
|
845
|
+
const { loginUrl, userUrl, errorUrl, landingUrl, tokenUrl, refreshUrl, logoutUrl, logoutBackChannelUrl, jwksUrl, validation } = handlerConfig ?? {};
|
|
846
|
+
if (!loginUrl) {
|
|
847
|
+
console.error("loginUrl is required");
|
|
848
|
+
}
|
|
397
849
|
const path = new URL(request.url).pathname;
|
|
398
|
-
if (new URL(
|
|
399
|
-
return callbackHandler(request);
|
|
850
|
+
if (new URL(configWithDefaults.redirect_uri).pathname === path) {
|
|
851
|
+
return callbackHandler(request, validation);
|
|
852
|
+
}
|
|
853
|
+
if (loginUrl === path) {
|
|
854
|
+
return initiateLogin({
|
|
855
|
+
landingUrl: landingUrl || "/",
|
|
856
|
+
errorUrl
|
|
857
|
+
});
|
|
400
858
|
}
|
|
401
859
|
if (userUrl === path) {
|
|
402
860
|
const { tokens, refreshHeaders } = await getTokenFromCookies(request);
|
|
@@ -433,10 +891,16 @@ function sso(config) {
|
|
|
433
891
|
headers: refreshHeaders
|
|
434
892
|
});
|
|
435
893
|
}
|
|
436
|
-
if (
|
|
437
|
-
return
|
|
438
|
-
|
|
439
|
-
|
|
894
|
+
if (logoutUrl === path) {
|
|
895
|
+
return logout(request, { landingUrl: landingUrl || "/" });
|
|
896
|
+
}
|
|
897
|
+
if (logoutBackChannelUrl === path) {
|
|
898
|
+
return logoutBackChannel(request);
|
|
899
|
+
}
|
|
900
|
+
if (jwksUrl === path) {
|
|
901
|
+
const jwks = await fetchJwks();
|
|
902
|
+
return new Response(JSON.stringify(jwks), {
|
|
903
|
+
headers: [["Content-Type", "application/json"]]
|
|
440
904
|
});
|
|
441
905
|
}
|
|
442
906
|
return new Response("Not Found", { status: 404 });
|
|
@@ -446,14 +910,15 @@ function sso(config) {
|
|
|
446
910
|
getRequiredUser,
|
|
447
911
|
getJwt,
|
|
448
912
|
initiateLogin,
|
|
913
|
+
logout,
|
|
449
914
|
callbackHandler,
|
|
450
915
|
handler
|
|
451
916
|
};
|
|
452
917
|
}
|
|
453
918
|
|
|
454
919
|
// src/vault.ts
|
|
455
|
-
function vault(url
|
|
456
|
-
async function getFullSecret(path) {
|
|
920
|
+
function vault(url) {
|
|
921
|
+
async function getFullSecret(path, token) {
|
|
457
922
|
const resp = await fetch(`${url}/${path}`, { headers: { "X-Vault-Token": token } });
|
|
458
923
|
if (resp.status !== 200) {
|
|
459
924
|
throw new Error(`Vault returned invalid status, ${resp.status}: '${resp.statusText}' from URL: ${url}`);
|
|
@@ -468,111 +933,8 @@ function vault(url, token) {
|
|
|
468
933
|
return {
|
|
469
934
|
url,
|
|
470
935
|
getFullSecret,
|
|
471
|
-
getSecret: async (path) => {
|
|
472
|
-
return (await getFullSecret(path)).data;
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// src/oidc-schema.ts
|
|
478
|
-
function oidcCallbackSchema(vendor) {
|
|
479
|
-
return {
|
|
480
|
-
"~standard": {
|
|
481
|
-
version: 1,
|
|
482
|
-
vendor,
|
|
483
|
-
validate: (value) => {
|
|
484
|
-
if (typeof value !== "object" || value === null) {
|
|
485
|
-
return {
|
|
486
|
-
issues: [
|
|
487
|
-
{
|
|
488
|
-
message: "Expected an object"
|
|
489
|
-
}
|
|
490
|
-
]
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
const params = value;
|
|
494
|
-
const issues = [];
|
|
495
|
-
const result = {};
|
|
496
|
-
if ("code" in params) {
|
|
497
|
-
if (typeof params.code === "string") {
|
|
498
|
-
result.code = params.code;
|
|
499
|
-
} else {
|
|
500
|
-
issues.push({
|
|
501
|
-
message: "code must be a string",
|
|
502
|
-
path: ["code"]
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
} else if (!("error" in params)) {
|
|
506
|
-
issues.push({
|
|
507
|
-
message: "code is required",
|
|
508
|
-
path: ["code"]
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
if ("state" in params) {
|
|
512
|
-
if (typeof params.state === "string" || params.state === undefined) {
|
|
513
|
-
result.state = params.state;
|
|
514
|
-
} else {
|
|
515
|
-
issues.push({
|
|
516
|
-
message: "state must be a string",
|
|
517
|
-
path: ["state"]
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
if ("session_state" in params) {
|
|
522
|
-
if (typeof params.session_state === "string" || params.session_state === undefined) {
|
|
523
|
-
result.session_state = params.session_state;
|
|
524
|
-
} else {
|
|
525
|
-
issues.push({
|
|
526
|
-
message: "session_state must be a string",
|
|
527
|
-
path: ["session_state"]
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
if ("error" in params) {
|
|
532
|
-
if (typeof params.error === "string") {
|
|
533
|
-
result.error = params.error;
|
|
534
|
-
} else {
|
|
535
|
-
issues.push({
|
|
536
|
-
message: "error must be a string",
|
|
537
|
-
path: ["error"]
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
if ("error_description" in params) {
|
|
541
|
-
if (typeof params.error_description === "string" || params.error_description === undefined) {
|
|
542
|
-
result.error_description = params.error_description;
|
|
543
|
-
} else {
|
|
544
|
-
issues.push({
|
|
545
|
-
message: "error_description must be a string",
|
|
546
|
-
path: ["error_description"]
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
if ("error_uri" in params) {
|
|
551
|
-
if (typeof params.error_uri === "string" || params.error_uri === undefined) {
|
|
552
|
-
result.error_uri = params.error_uri;
|
|
553
|
-
} else {
|
|
554
|
-
issues.push({
|
|
555
|
-
message: "error_uri must be a string",
|
|
556
|
-
path: ["error_uri"]
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
if ("iss" in params) {
|
|
562
|
-
if (typeof params.iss === "string" || params.iss === undefined) {
|
|
563
|
-
result.iss = params.iss;
|
|
564
|
-
} else {
|
|
565
|
-
issues.push({
|
|
566
|
-
message: "iss must be a string",
|
|
567
|
-
path: ["iss"]
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
if (issues.length > 0) {
|
|
572
|
-
return { issues };
|
|
573
|
-
}
|
|
574
|
-
return { value: result };
|
|
575
|
-
}
|
|
936
|
+
getSecret: async (path, token) => {
|
|
937
|
+
return (await getFullSecret(path, token)).data;
|
|
576
938
|
}
|
|
577
939
|
};
|
|
578
940
|
}
|
|
@@ -619,6 +981,30 @@ async function handler(request, config) {
|
|
|
619
981
|
throw unavailable();
|
|
620
982
|
return sso2.handler(request, config);
|
|
621
983
|
}
|
|
984
|
+
// src/session-store.ts
|
|
985
|
+
class InMemorySessionStore {
|
|
986
|
+
sessions = new Map;
|
|
987
|
+
async create(session) {
|
|
988
|
+
if (this.sessions.has(session.sid)) {
|
|
989
|
+
throw new Error(`Session with sid ${session.sid} already exists`);
|
|
990
|
+
}
|
|
991
|
+
this.sessions.set(session.sid, session);
|
|
992
|
+
}
|
|
993
|
+
async get(sid) {
|
|
994
|
+
return this.sessions.get(sid) ?? null;
|
|
995
|
+
}
|
|
996
|
+
async update(sid, data) {
|
|
997
|
+
const session = this.sessions.get(sid);
|
|
998
|
+
if (!session) {
|
|
999
|
+
throw new Error(`Session with sid ${sid} not found`);
|
|
1000
|
+
}
|
|
1001
|
+
const updated = { ...session, ...data };
|
|
1002
|
+
this.sessions.set(sid, updated);
|
|
1003
|
+
}
|
|
1004
|
+
async delete(sid) {
|
|
1005
|
+
this.sessions.delete(sid);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
622
1008
|
// src/ui/sign-in-loading.tsx
|
|
623
1009
|
import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
|
|
624
1010
|
function SignInLoading({ complete = false, children }) {
|
|
@@ -774,9 +1160,14 @@ function SSOProvider({
|
|
|
774
1160
|
}
|
|
775
1161
|
}
|
|
776
1162
|
};
|
|
1163
|
+
const handleLogout = () => {
|
|
1164
|
+
setUserState(null);
|
|
1165
|
+
};
|
|
777
1166
|
window.addEventListener("storage", handleStorageChange);
|
|
1167
|
+
window.addEventListener("es-sso-logout", handleLogout);
|
|
778
1168
|
return () => {
|
|
779
1169
|
window.removeEventListener("storage", handleStorageChange);
|
|
1170
|
+
window.removeEventListener("es-sso-logout", handleLogout);
|
|
780
1171
|
};
|
|
781
1172
|
}, [disableListener, storage, actualStorageKey, isValidUser]);
|
|
782
1173
|
const contextValue = {
|
|
@@ -875,34 +1266,76 @@ function useToken() {
|
|
|
875
1266
|
refresh
|
|
876
1267
|
};
|
|
877
1268
|
}
|
|
1269
|
+
async function logout(logoutUrl) {
|
|
1270
|
+
try {
|
|
1271
|
+
const response = await fetch(logoutUrl, {
|
|
1272
|
+
headers: { Accept: "application/json" }
|
|
1273
|
+
});
|
|
1274
|
+
if (!response.ok) {
|
|
1275
|
+
return { success: false, error: `HTTP ${response.status}` };
|
|
1276
|
+
}
|
|
1277
|
+
const data = await response.json();
|
|
1278
|
+
if (!data.success) {
|
|
1279
|
+
return { success: false, error: data.message || "Logout failed" };
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof window !== "undefined") {
|
|
1282
|
+
for (let i = localStorage.length - 1;i >= 0; i--) {
|
|
1283
|
+
const key = localStorage.key(i);
|
|
1284
|
+
if (key?.startsWith("es-sso-user")) {
|
|
1285
|
+
localStorage.removeItem(key);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
for (let i = sessionStorage.length - 1;i >= 0; i--) {
|
|
1289
|
+
const key = sessionStorage.key(i);
|
|
1290
|
+
if (key?.startsWith("es-sso-user")) {
|
|
1291
|
+
sessionStorage.removeItem(key);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
window.dispatchEvent(new CustomEvent("es-sso-logout"));
|
|
1295
|
+
}
|
|
1296
|
+
return { success: true };
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
return {
|
|
1299
|
+
success: false,
|
|
1300
|
+
error: error instanceof Error ? error.message : "Network error"
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
878
1304
|
|
|
879
1305
|
// src/index.ts
|
|
880
|
-
async function enterpriseStandard(
|
|
1306
|
+
async function enterpriseStandard(appKey, initConfig) {
|
|
881
1307
|
let vaultUrl;
|
|
882
1308
|
let vaultToken;
|
|
883
|
-
let
|
|
1309
|
+
let secrets;
|
|
884
1310
|
const ioniteUrl = initConfig?.ioniteUrl ?? "https://ionite.com";
|
|
885
|
-
if (
|
|
1311
|
+
if (appKey?.startsWith("IONITE_PUBLIC_DEMO_")) {
|
|
886
1312
|
vaultUrl = "https://vault-ionite.ionite.dev/v1/secret/data";
|
|
887
|
-
|
|
888
|
-
|
|
1313
|
+
const port = appKey.slice("IONITE_PUBLIC_DEMO_".length);
|
|
1314
|
+
secrets = {
|
|
1315
|
+
sso: {
|
|
1316
|
+
path: `public/IONITE_PUBLIC_DEMO_SSO_${port}`,
|
|
1317
|
+
token: "hvs.VGhD2hmXDH9PmZjTacZx0G5K"
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
889
1320
|
} else if (appKey) {
|
|
890
1321
|
if (!vaultUrl || !vaultToken) {
|
|
891
1322
|
throw new Error("TODO something is wrong with the ionite config, handle this error");
|
|
892
1323
|
}
|
|
893
|
-
|
|
1324
|
+
secrets = {};
|
|
894
1325
|
} else {
|
|
895
1326
|
throw new Error("TODO tell them how to connect to ionite");
|
|
896
1327
|
}
|
|
897
1328
|
const defaultInstance2 = getDefaultInstance();
|
|
898
|
-
const vaultClient =
|
|
1329
|
+
const vaultClient = vault(vaultUrl);
|
|
899
1330
|
const result = {
|
|
900
|
-
appId,
|
|
901
1331
|
ioniteUrl,
|
|
902
1332
|
defaultInstance: initConfig?.defaultInstance || initConfig?.defaultInstance !== false && !defaultInstance2,
|
|
903
1333
|
vault: vaultClient,
|
|
904
|
-
sso:
|
|
905
|
-
|
|
1334
|
+
sso: secrets.sso ? sso({
|
|
1335
|
+
...await vaultClient.getSecret(secrets.sso.path, secrets.sso.token),
|
|
1336
|
+
...initConfig
|
|
1337
|
+
}) : undefined,
|
|
1338
|
+
iam: secrets.iam ? await iam(await vaultClient.getSecret(secrets.iam.path, secrets.iam.token)) : undefined
|
|
906
1339
|
};
|
|
907
1340
|
if (result.defaultInstance) {
|
|
908
1341
|
if (defaultInstance2) {
|
|
@@ -915,8 +1348,11 @@ async function enterpriseStandard(appId, appKey, initConfig) {
|
|
|
915
1348
|
export {
|
|
916
1349
|
useUser,
|
|
917
1350
|
useToken,
|
|
1351
|
+
tokenResponseSchema,
|
|
918
1352
|
oidcCallbackSchema,
|
|
1353
|
+
logout,
|
|
919
1354
|
initiateLogin,
|
|
1355
|
+
idTokenClaimsSchema,
|
|
920
1356
|
handler,
|
|
921
1357
|
getUser,
|
|
922
1358
|
getRequiredUser,
|
|
@@ -925,5 +1361,6 @@ export {
|
|
|
925
1361
|
SignedOut,
|
|
926
1362
|
SignedIn,
|
|
927
1363
|
SignInLoading,
|
|
928
|
-
SSOProvider
|
|
1364
|
+
SSOProvider,
|
|
1365
|
+
InMemorySessionStore
|
|
929
1366
|
};
|