strapi-plugin-oidc 1.4.0 → 1.4.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/README.md +0 -1
- package/dist/admin/{index-QZkv75Xp.js → index-BnFRueNv.js} +30 -33
- package/dist/admin/{index-BTTGSnuQ.mjs → index-CY4s-vtv.mjs} +30 -33
- package/dist/admin/{index-CZDixCh4.js → index-RMgj1w0B.js} +1 -1
- package/dist/admin/{index-8hB6LKml.mjs → index-ZRaWWFUL.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +90 -95
- package/dist/server/index.mjs +90 -95
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,6 @@ module.exports = ({ env }) => ({
|
|
|
41
41
|
OIDC_GRANT_TYPE: 'authorization_code',
|
|
42
42
|
OIDC_FAMILY_NAME_FIELD: 'family_name',
|
|
43
43
|
OIDC_GIVEN_NAME_FIELD: 'given_name',
|
|
44
|
-
OIDC_USER_INFO_ENDPOINT_WITH_AUTH_HEADER: false, // true = Bearer header, false = query param
|
|
45
44
|
OIDC_LOGOUT_URL: '', // Provider logout URL; omit to redirect to Strapi login
|
|
46
45
|
OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
|
|
47
46
|
OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
|
|
@@ -7,7 +7,7 @@ const react = require("react");
|
|
|
7
7
|
const designSystem = require("@strapi/design-system");
|
|
8
8
|
const icons = require("@strapi/icons");
|
|
9
9
|
const reactIntl = require("react-intl");
|
|
10
|
-
const index = require("./index-
|
|
10
|
+
const index = require("./index-RMgj1w0B.js");
|
|
11
11
|
const styled = require("styled-components");
|
|
12
12
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
13
13
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
@@ -39,7 +39,7 @@ const CustomTable = styled__default.default(designSystem.Table)`
|
|
|
39
39
|
font-size: 1.3rem !important;
|
|
40
40
|
}
|
|
41
41
|
`;
|
|
42
|
-
|
|
42
|
+
function LocalizedDate({ date }) {
|
|
43
43
|
const userLocale = navigator.language || "en-US";
|
|
44
44
|
return new Intl.DateTimeFormat(userLocale, {
|
|
45
45
|
year: "numeric",
|
|
@@ -48,7 +48,7 @@ const LocalizedDate = ({ date }) => {
|
|
|
48
48
|
hour: "2-digit",
|
|
49
49
|
minute: "2-digit"
|
|
50
50
|
}).format(new Date(date));
|
|
51
|
-
}
|
|
51
|
+
}
|
|
52
52
|
function Whitelist({
|
|
53
53
|
users,
|
|
54
54
|
roles,
|
|
@@ -70,7 +70,14 @@ function Whitelist({
|
|
|
70
70
|
const PAGE_SIZE = 10;
|
|
71
71
|
const pageCount = Math.ceil(users.length / PAGE_SIZE) || 1;
|
|
72
72
|
const paginatedUsers = users.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
|
73
|
-
const
|
|
73
|
+
const getRoleNames = (roleIds) => roleIds.map((roleId) => {
|
|
74
|
+
const r = roles.find((ro) => String(ro.id) === String(roleId));
|
|
75
|
+
return r ? r.name : roleId;
|
|
76
|
+
}).join(", ");
|
|
77
|
+
const defaultRoleNames = getRoleNames(oidcRoles.flatMap((oidc) => oidc.role ?? []));
|
|
78
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
79
|
+
const isValidEmail = emailRegex.test(email);
|
|
80
|
+
const onSaveEmail = react.useCallback(() => {
|
|
74
81
|
const emailText = email.trim();
|
|
75
82
|
if (users.some((user) => user.email === emailText)) {
|
|
76
83
|
toggleNotification({
|
|
@@ -78,15 +85,11 @@ function Whitelist({
|
|
|
78
85
|
message: formatMessage(index.getTrad("whitelist.error.unique"))
|
|
79
86
|
});
|
|
80
87
|
} else {
|
|
81
|
-
|
|
88
|
+
onSave(emailText, selectedRoles);
|
|
82
89
|
setEmail("");
|
|
83
90
|
setSelectedRoles([]);
|
|
84
91
|
}
|
|
85
92
|
}, [email, selectedRoles, users, onSave, formatMessage, toggleNotification]);
|
|
86
|
-
const isValidEmail = react.useCallback(() => {
|
|
87
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
88
|
-
return emailRegex.test(email);
|
|
89
|
-
}, [email]);
|
|
90
93
|
const handleImport = react.useCallback(
|
|
91
94
|
async (e) => {
|
|
92
95
|
const file = e.target.files?.[0];
|
|
@@ -181,7 +184,7 @@ function Whitelist({
|
|
|
181
184
|
type: "text",
|
|
182
185
|
disabled: loading,
|
|
183
186
|
value: email,
|
|
184
|
-
hasError: Boolean(email && !isValidEmail
|
|
187
|
+
hasError: Boolean(email && !isValidEmail),
|
|
185
188
|
onChange: (e) => setEmail(e.currentTarget.value),
|
|
186
189
|
placeholder: formatMessage(index.getTrad("whitelist.email.placeholder"))
|
|
187
190
|
}
|
|
@@ -203,7 +206,7 @@ function Whitelist({
|
|
|
203
206
|
{
|
|
204
207
|
size: "L",
|
|
205
208
|
startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
|
|
206
|
-
disabled: loading || email.trim() === "" || !isValidEmail
|
|
209
|
+
disabled: loading || email.trim() === "" || !isValidEmail,
|
|
207
210
|
loading,
|
|
208
211
|
onClick: onSaveEmail,
|
|
209
212
|
children: formatMessage(index.getTrad("page.add"))
|
|
@@ -220,20 +223,9 @@ function Whitelist({
|
|
|
220
223
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { paddingRight: 0 }, children: " " })
|
|
221
224
|
] }) }),
|
|
222
225
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Tbody, { children: users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tr, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { colSpan: 5, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: formatMessage(index.getTrad("whitelist.table.empty")) }) }) }) }) : paginatedUsers.map((user, index$1) => {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}).join(", ");
|
|
227
|
-
let userRolesNames = getRoleNames(user.roles || []);
|
|
228
|
-
let isDefault = false;
|
|
229
|
-
if (!userRolesNames) {
|
|
230
|
-
const defaultRolesIds = oidcRoles.reduce((acc, oidc) => {
|
|
231
|
-
if (oidc.role) acc.push(...oidc.role);
|
|
232
|
-
return acc;
|
|
233
|
-
}, []);
|
|
234
|
-
userRolesNames = getRoleNames(defaultRolesIds);
|
|
235
|
-
isDefault = Boolean(userRolesNames);
|
|
236
|
-
}
|
|
226
|
+
const explicitRoleNames = getRoleNames(user.roles || []);
|
|
227
|
+
const isDefault = !explicitRoleNames && Boolean(defaultRoleNames);
|
|
228
|
+
const userRolesNames = explicitRoleNames || defaultRoleNames;
|
|
237
229
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
|
|
238
230
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: index$1 + 1 + (page - 1) * PAGE_SIZE }),
|
|
239
231
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: user.email }),
|
|
@@ -443,6 +435,9 @@ function CustomSwitch({ checked, onChange, label, disabled }) {
|
|
|
443
435
|
label && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: disabled ? "neutral500" : "neutral800", children: label })
|
|
444
436
|
] });
|
|
445
437
|
}
|
|
438
|
+
function deepClone(value) {
|
|
439
|
+
return JSON.parse(JSON.stringify(value));
|
|
440
|
+
}
|
|
446
441
|
function useOidcSettings() {
|
|
447
442
|
const { get, put, post } = admin.useFetchClient();
|
|
448
443
|
const [loading, setLoading] = react.useState(false);
|
|
@@ -462,14 +457,14 @@ function useOidcSettings() {
|
|
|
462
457
|
react.useEffect(() => {
|
|
463
458
|
get(`/strapi-plugin-oidc/oidc-roles`).then((response) => {
|
|
464
459
|
setOIDCRoles(response.data);
|
|
465
|
-
setInitialOIDCRoles(
|
|
460
|
+
setInitialOIDCRoles(deepClone(response.data));
|
|
466
461
|
});
|
|
467
462
|
get(`/admin/roles`).then((response) => {
|
|
468
463
|
setRoles(response.data.data);
|
|
469
464
|
});
|
|
470
465
|
get("/strapi-plugin-oidc/whitelist").then((response) => {
|
|
471
466
|
setUsers(response.data.whitelistUsers);
|
|
472
|
-
setInitialUsers(
|
|
467
|
+
setInitialUsers(deepClone(response.data.whitelistUsers));
|
|
473
468
|
setUseWhitelist(response.data.useWhitelist);
|
|
474
469
|
setInitialUseWhitelist(response.data.useWhitelist);
|
|
475
470
|
setEnforceOIDC(response.data.enforceOIDC);
|
|
@@ -483,11 +478,11 @@ function useOidcSettings() {
|
|
|
483
478
|
);
|
|
484
479
|
setOIDCRoles(updatedRoles);
|
|
485
480
|
};
|
|
486
|
-
const onRegisterWhitelist =
|
|
481
|
+
const onRegisterWhitelist = (email, selectedRoles) => {
|
|
487
482
|
const newUser = { email, roles: selectedRoles, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
488
483
|
setUsers([...users, newUser]);
|
|
489
484
|
};
|
|
490
|
-
const onDeleteWhitelist =
|
|
485
|
+
const onDeleteWhitelist = (email) => {
|
|
491
486
|
const updatedUsers = users.filter((u) => u.email !== email);
|
|
492
487
|
setUsers(updatedUsers);
|
|
493
488
|
if (useWhitelist && updatedUsers.length === 0) {
|
|
@@ -502,7 +497,7 @@ function useOidcSettings() {
|
|
|
502
497
|
const response = await post("/strapi-plugin-oidc/whitelist/import", { users: entries });
|
|
503
498
|
const refreshed = await get("/strapi-plugin-oidc/whitelist");
|
|
504
499
|
setUsers(refreshed.data.whitelistUsers);
|
|
505
|
-
setInitialUsers(
|
|
500
|
+
setInitialUsers(deepClone(refreshed.data.whitelistUsers));
|
|
506
501
|
return response.data.importedCount;
|
|
507
502
|
};
|
|
508
503
|
const onExport = () => {
|
|
@@ -511,11 +506,13 @@ function useOidcSettings() {
|
|
|
511
506
|
email,
|
|
512
507
|
roles: (userRoles || []).map((id) => roleMap.get(String(id)) ?? id)
|
|
513
508
|
}));
|
|
509
|
+
const now = /* @__PURE__ */ new Date();
|
|
510
|
+
const datetime = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
|
|
514
511
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
515
512
|
const url = URL.createObjectURL(blob);
|
|
516
513
|
const a = document.createElement("a");
|
|
517
514
|
a.href = url;
|
|
518
|
-
a.download =
|
|
515
|
+
a.download = `strapi-oidc-whitelist-${datetime}.json`;
|
|
519
516
|
a.click();
|
|
520
517
|
URL.revokeObjectURL(url);
|
|
521
518
|
};
|
|
@@ -546,12 +543,12 @@ function useOidcSettings() {
|
|
|
546
543
|
useWhitelist,
|
|
547
544
|
enforceOIDC
|
|
548
545
|
});
|
|
549
|
-
setInitialOIDCRoles(
|
|
546
|
+
setInitialOIDCRoles(deepClone(oidcRoles));
|
|
550
547
|
setInitialUseWhitelist(useWhitelist);
|
|
551
548
|
setInitialEnforceOIDC(enforceOIDC);
|
|
552
549
|
get("/strapi-plugin-oidc/whitelist").then((getResponse) => {
|
|
553
550
|
setUsers(getResponse.data.whitelistUsers);
|
|
554
|
-
setInitialUsers(
|
|
551
|
+
setInitialUsers(deepClone(getResponse.data.whitelistUsers));
|
|
555
552
|
});
|
|
556
553
|
if (syncResponse.data?.matchedExistingUsersCount > 0) {
|
|
557
554
|
setMatched(syncResponse.data.matchedExistingUsersCount);
|
|
@@ -5,7 +5,7 @@ import { useState, useRef, useCallback, useEffect, memo } from "react";
|
|
|
5
5
|
import { Typography, Flex, Box, MultiSelect, MultiSelectOption, Button, Dialog, Field, Divider, Thead, Tr, Th, Tbody, Td, IconButton, Pagination, PreviousLink, PageLink, NextLink, Table, Alert } from "@strapi/design-system";
|
|
6
6
|
import { Download, Upload, Trash, WarningCircle, Plus, Information } from "@strapi/icons";
|
|
7
7
|
import { useIntl } from "react-intl";
|
|
8
|
-
import { g as getTrad } from "./index-
|
|
8
|
+
import { g as getTrad } from "./index-ZRaWWFUL.mjs";
|
|
9
9
|
import styled from "styled-components";
|
|
10
10
|
function Role({ oidcRoles, roles, onChangeRole }) {
|
|
11
11
|
const { formatMessage } = useIntl();
|
|
@@ -35,7 +35,7 @@ const CustomTable = styled(Table)`
|
|
|
35
35
|
font-size: 1.3rem !important;
|
|
36
36
|
}
|
|
37
37
|
`;
|
|
38
|
-
|
|
38
|
+
function LocalizedDate({ date }) {
|
|
39
39
|
const userLocale = navigator.language || "en-US";
|
|
40
40
|
return new Intl.DateTimeFormat(userLocale, {
|
|
41
41
|
year: "numeric",
|
|
@@ -44,7 +44,7 @@ const LocalizedDate = ({ date }) => {
|
|
|
44
44
|
hour: "2-digit",
|
|
45
45
|
minute: "2-digit"
|
|
46
46
|
}).format(new Date(date));
|
|
47
|
-
}
|
|
47
|
+
}
|
|
48
48
|
function Whitelist({
|
|
49
49
|
users,
|
|
50
50
|
roles,
|
|
@@ -66,7 +66,14 @@ function Whitelist({
|
|
|
66
66
|
const PAGE_SIZE = 10;
|
|
67
67
|
const pageCount = Math.ceil(users.length / PAGE_SIZE) || 1;
|
|
68
68
|
const paginatedUsers = users.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
|
69
|
-
const
|
|
69
|
+
const getRoleNames = (roleIds) => roleIds.map((roleId) => {
|
|
70
|
+
const r = roles.find((ro) => String(ro.id) === String(roleId));
|
|
71
|
+
return r ? r.name : roleId;
|
|
72
|
+
}).join(", ");
|
|
73
|
+
const defaultRoleNames = getRoleNames(oidcRoles.flatMap((oidc) => oidc.role ?? []));
|
|
74
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
75
|
+
const isValidEmail = emailRegex.test(email);
|
|
76
|
+
const onSaveEmail = useCallback(() => {
|
|
70
77
|
const emailText = email.trim();
|
|
71
78
|
if (users.some((user) => user.email === emailText)) {
|
|
72
79
|
toggleNotification({
|
|
@@ -74,15 +81,11 @@ function Whitelist({
|
|
|
74
81
|
message: formatMessage(getTrad("whitelist.error.unique"))
|
|
75
82
|
});
|
|
76
83
|
} else {
|
|
77
|
-
|
|
84
|
+
onSave(emailText, selectedRoles);
|
|
78
85
|
setEmail("");
|
|
79
86
|
setSelectedRoles([]);
|
|
80
87
|
}
|
|
81
88
|
}, [email, selectedRoles, users, onSave, formatMessage, toggleNotification]);
|
|
82
|
-
const isValidEmail = useCallback(() => {
|
|
83
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
84
|
-
return emailRegex.test(email);
|
|
85
|
-
}, [email]);
|
|
86
89
|
const handleImport = useCallback(
|
|
87
90
|
async (e) => {
|
|
88
91
|
const file = e.target.files?.[0];
|
|
@@ -177,7 +180,7 @@ function Whitelist({
|
|
|
177
180
|
type: "text",
|
|
178
181
|
disabled: loading,
|
|
179
182
|
value: email,
|
|
180
|
-
hasError: Boolean(email && !isValidEmail
|
|
183
|
+
hasError: Boolean(email && !isValidEmail),
|
|
181
184
|
onChange: (e) => setEmail(e.currentTarget.value),
|
|
182
185
|
placeholder: formatMessage(getTrad("whitelist.email.placeholder"))
|
|
183
186
|
}
|
|
@@ -199,7 +202,7 @@ function Whitelist({
|
|
|
199
202
|
{
|
|
200
203
|
size: "L",
|
|
201
204
|
startIcon: /* @__PURE__ */ jsx(Plus, {}),
|
|
202
|
-
disabled: loading || email.trim() === "" || !isValidEmail
|
|
205
|
+
disabled: loading || email.trim() === "" || !isValidEmail,
|
|
203
206
|
loading,
|
|
204
207
|
onClick: onSaveEmail,
|
|
205
208
|
children: formatMessage(getTrad("page.add"))
|
|
@@ -216,20 +219,9 @@ function Whitelist({
|
|
|
216
219
|
/* @__PURE__ */ jsx(Th, { style: { paddingRight: 0 }, children: " " })
|
|
217
220
|
] }) }),
|
|
218
221
|
/* @__PURE__ */ jsx(Tbody, { children: users.length === 0 ? /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: 5, children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: formatMessage(getTrad("whitelist.table.empty")) }) }) }) }) : paginatedUsers.map((user, index) => {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}).join(", ");
|
|
223
|
-
let userRolesNames = getRoleNames(user.roles || []);
|
|
224
|
-
let isDefault = false;
|
|
225
|
-
if (!userRolesNames) {
|
|
226
|
-
const defaultRolesIds = oidcRoles.reduce((acc, oidc) => {
|
|
227
|
-
if (oidc.role) acc.push(...oidc.role);
|
|
228
|
-
return acc;
|
|
229
|
-
}, []);
|
|
230
|
-
userRolesNames = getRoleNames(defaultRolesIds);
|
|
231
|
-
isDefault = Boolean(userRolesNames);
|
|
232
|
-
}
|
|
222
|
+
const explicitRoleNames = getRoleNames(user.roles || []);
|
|
223
|
+
const isDefault = !explicitRoleNames && Boolean(defaultRoleNames);
|
|
224
|
+
const userRolesNames = explicitRoleNames || defaultRoleNames;
|
|
233
225
|
return /* @__PURE__ */ jsxs(Tr, { children: [
|
|
234
226
|
/* @__PURE__ */ jsx(Td, { children: index + 1 + (page - 1) * PAGE_SIZE }),
|
|
235
227
|
/* @__PURE__ */ jsx(Td, { children: user.email }),
|
|
@@ -439,6 +431,9 @@ function CustomSwitch({ checked, onChange, label, disabled }) {
|
|
|
439
431
|
label && /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: disabled ? "neutral500" : "neutral800", children: label })
|
|
440
432
|
] });
|
|
441
433
|
}
|
|
434
|
+
function deepClone(value) {
|
|
435
|
+
return JSON.parse(JSON.stringify(value));
|
|
436
|
+
}
|
|
442
437
|
function useOidcSettings() {
|
|
443
438
|
const { get, put, post } = useFetchClient();
|
|
444
439
|
const [loading, setLoading] = useState(false);
|
|
@@ -458,14 +453,14 @@ function useOidcSettings() {
|
|
|
458
453
|
useEffect(() => {
|
|
459
454
|
get(`/strapi-plugin-oidc/oidc-roles`).then((response) => {
|
|
460
455
|
setOIDCRoles(response.data);
|
|
461
|
-
setInitialOIDCRoles(
|
|
456
|
+
setInitialOIDCRoles(deepClone(response.data));
|
|
462
457
|
});
|
|
463
458
|
get(`/admin/roles`).then((response) => {
|
|
464
459
|
setRoles(response.data.data);
|
|
465
460
|
});
|
|
466
461
|
get("/strapi-plugin-oidc/whitelist").then((response) => {
|
|
467
462
|
setUsers(response.data.whitelistUsers);
|
|
468
|
-
setInitialUsers(
|
|
463
|
+
setInitialUsers(deepClone(response.data.whitelistUsers));
|
|
469
464
|
setUseWhitelist(response.data.useWhitelist);
|
|
470
465
|
setInitialUseWhitelist(response.data.useWhitelist);
|
|
471
466
|
setEnforceOIDC(response.data.enforceOIDC);
|
|
@@ -479,11 +474,11 @@ function useOidcSettings() {
|
|
|
479
474
|
);
|
|
480
475
|
setOIDCRoles(updatedRoles);
|
|
481
476
|
};
|
|
482
|
-
const onRegisterWhitelist =
|
|
477
|
+
const onRegisterWhitelist = (email, selectedRoles) => {
|
|
483
478
|
const newUser = { email, roles: selectedRoles, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
484
479
|
setUsers([...users, newUser]);
|
|
485
480
|
};
|
|
486
|
-
const onDeleteWhitelist =
|
|
481
|
+
const onDeleteWhitelist = (email) => {
|
|
487
482
|
const updatedUsers = users.filter((u) => u.email !== email);
|
|
488
483
|
setUsers(updatedUsers);
|
|
489
484
|
if (useWhitelist && updatedUsers.length === 0) {
|
|
@@ -498,7 +493,7 @@ function useOidcSettings() {
|
|
|
498
493
|
const response = await post("/strapi-plugin-oidc/whitelist/import", { users: entries });
|
|
499
494
|
const refreshed = await get("/strapi-plugin-oidc/whitelist");
|
|
500
495
|
setUsers(refreshed.data.whitelistUsers);
|
|
501
|
-
setInitialUsers(
|
|
496
|
+
setInitialUsers(deepClone(refreshed.data.whitelistUsers));
|
|
502
497
|
return response.data.importedCount;
|
|
503
498
|
};
|
|
504
499
|
const onExport = () => {
|
|
@@ -507,11 +502,13 @@ function useOidcSettings() {
|
|
|
507
502
|
email,
|
|
508
503
|
roles: (userRoles || []).map((id) => roleMap.get(String(id)) ?? id)
|
|
509
504
|
}));
|
|
505
|
+
const now = /* @__PURE__ */ new Date();
|
|
506
|
+
const datetime = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
|
|
510
507
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
511
508
|
const url = URL.createObjectURL(blob);
|
|
512
509
|
const a = document.createElement("a");
|
|
513
510
|
a.href = url;
|
|
514
|
-
a.download =
|
|
511
|
+
a.download = `strapi-oidc-whitelist-${datetime}.json`;
|
|
515
512
|
a.click();
|
|
516
513
|
URL.revokeObjectURL(url);
|
|
517
514
|
};
|
|
@@ -542,12 +539,12 @@ function useOidcSettings() {
|
|
|
542
539
|
useWhitelist,
|
|
543
540
|
enforceOIDC
|
|
544
541
|
});
|
|
545
|
-
setInitialOIDCRoles(
|
|
542
|
+
setInitialOIDCRoles(deepClone(oidcRoles));
|
|
546
543
|
setInitialUseWhitelist(useWhitelist);
|
|
547
544
|
setInitialEnforceOIDC(enforceOIDC);
|
|
548
545
|
get("/strapi-plugin-oidc/whitelist").then((getResponse) => {
|
|
549
546
|
setUsers(getResponse.data.whitelistUsers);
|
|
550
|
-
setInitialUsers(
|
|
547
|
+
setInitialUsers(deepClone(getResponse.data.whitelistUsers));
|
|
551
548
|
});
|
|
552
549
|
if (syncResponse.data?.matchedExistingUsersCount > 0) {
|
|
553
550
|
setMatched(syncResponse.data.matchedExistingUsersCount);
|
|
@@ -123,7 +123,7 @@ const index = {
|
|
|
123
123
|
defaultMessage: "Configuration"
|
|
124
124
|
},
|
|
125
125
|
Component: async () => {
|
|
126
|
-
return await Promise.resolve().then(() => require("./index-
|
|
126
|
+
return await Promise.resolve().then(() => require("./index-BnFRueNv.js"));
|
|
127
127
|
},
|
|
128
128
|
permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
|
|
129
129
|
}
|
|
@@ -122,7 +122,7 @@ const index = {
|
|
|
122
122
|
defaultMessage: "Configuration"
|
|
123
123
|
},
|
|
124
124
|
Component: async () => {
|
|
125
|
-
return await import("./index-
|
|
125
|
+
return await import("./index-CY4s-vtv.mjs");
|
|
126
126
|
},
|
|
127
127
|
permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
|
|
128
128
|
}
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -164,7 +164,6 @@ const config = {
|
|
|
164
164
|
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
165
165
|
OIDC_TOKEN_ENDPOINT: "",
|
|
166
166
|
OIDC_USER_INFO_ENDPOINT: "",
|
|
167
|
-
OIDC_USER_INFO_ENDPOINT_WITH_AUTH_HEADER: false,
|
|
168
167
|
OIDC_GRANT_TYPE: "authorization_code",
|
|
169
168
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
170
169
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
@@ -223,45 +222,43 @@ function clearAuthCookies(strapi2, ctx) {
|
|
|
223
222
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
224
223
|
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
225
224
|
}
|
|
225
|
+
const REQUIRED_CONFIG_KEYS = [
|
|
226
|
+
"OIDC_CLIENT_ID",
|
|
227
|
+
"OIDC_CLIENT_SECRET",
|
|
228
|
+
"OIDC_REDIRECT_URI",
|
|
229
|
+
"OIDC_SCOPES",
|
|
230
|
+
"OIDC_TOKEN_ENDPOINT",
|
|
231
|
+
"OIDC_USER_INFO_ENDPOINT",
|
|
232
|
+
"OIDC_GRANT_TYPE",
|
|
233
|
+
"OIDC_FAMILY_NAME_FIELD",
|
|
234
|
+
"OIDC_GIVEN_NAME_FIELD",
|
|
235
|
+
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
236
|
+
];
|
|
226
237
|
function configValidation() {
|
|
227
238
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
228
|
-
|
|
229
|
-
"OIDC_CLIENT_ID",
|
|
230
|
-
"OIDC_CLIENT_SECRET",
|
|
231
|
-
"OIDC_REDIRECT_URI",
|
|
232
|
-
"OIDC_SCOPES",
|
|
233
|
-
"OIDC_TOKEN_ENDPOINT",
|
|
234
|
-
"OIDC_USER_INFO_ENDPOINT",
|
|
235
|
-
"OIDC_GRANT_TYPE",
|
|
236
|
-
"OIDC_FAMILY_NAME_FIELD",
|
|
237
|
-
"OIDC_GIVEN_NAME_FIELD",
|
|
238
|
-
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
239
|
-
];
|
|
240
|
-
if (requiredKeys.every((key) => config2[key])) {
|
|
239
|
+
if (REQUIRED_CONFIG_KEYS.every((key) => config2[key])) {
|
|
241
240
|
return config2;
|
|
242
241
|
}
|
|
243
|
-
throw new Error(
|
|
242
|
+
throw new Error(
|
|
243
|
+
`The following configuration keys are required: ${REQUIRED_CONFIG_KEYS.join(", ")}`
|
|
244
|
+
);
|
|
244
245
|
}
|
|
245
246
|
async function oidcSignIn(ctx) {
|
|
246
|
-
let { state } = ctx.query;
|
|
247
247
|
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPES, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
|
|
248
248
|
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge__default.default();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
249
|
+
const state = node_crypto.randomBytes(32).toString("base64url");
|
|
250
|
+
const nonce = node_crypto.randomBytes(32).toString("base64url");
|
|
252
251
|
const isProduction = strapi.config.get("environment") === "production";
|
|
253
|
-
|
|
252
|
+
const cookieOptions = {
|
|
254
253
|
httpOnly: true,
|
|
255
254
|
maxAge: 6e5,
|
|
255
|
+
// 10 minutes
|
|
256
256
|
secure: isProduction && ctx.request.secure,
|
|
257
257
|
sameSite: "lax"
|
|
258
|
-
}
|
|
259
|
-
ctx.cookies.set("
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
secure: isProduction && ctx.request.secure,
|
|
263
|
-
sameSite: "lax"
|
|
264
|
-
});
|
|
258
|
+
};
|
|
259
|
+
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
260
|
+
ctx.cookies.set("oidc_state", state, cookieOptions);
|
|
261
|
+
ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
|
|
265
262
|
const params = new URLSearchParams();
|
|
266
263
|
params.append("response_type", "code");
|
|
267
264
|
params.append("client_id", OIDC_CLIENT_ID);
|
|
@@ -270,11 +267,12 @@ async function oidcSignIn(ctx) {
|
|
|
270
267
|
params.append("code_challenge", codeChallenge);
|
|
271
268
|
params.append("code_challenge_method", "S256");
|
|
272
269
|
params.append("state", state);
|
|
270
|
+
params.append("nonce", nonce);
|
|
273
271
|
const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
|
|
274
272
|
ctx.set("Location", authorizationUrl);
|
|
275
273
|
return ctx.send({}, 302);
|
|
276
274
|
}
|
|
277
|
-
async function exchangeTokenAndFetchUserInfo(config2, params) {
|
|
275
|
+
async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
278
276
|
const response = await fetch(config2.OIDC_TOKEN_ENDPOINT, {
|
|
279
277
|
method: "POST",
|
|
280
278
|
body: params,
|
|
@@ -283,31 +281,28 @@ async function exchangeTokenAndFetchUserInfo(config2, params) {
|
|
|
283
281
|
}
|
|
284
282
|
});
|
|
285
283
|
if (!response.ok) {
|
|
286
|
-
|
|
287
|
-
throw new Error(
|
|
288
|
-
`Failed to exchange token: ${response.status} ${response.statusText} - ${errText}`
|
|
289
|
-
);
|
|
284
|
+
throw new Error("Token exchange failed");
|
|
290
285
|
}
|
|
291
286
|
const tokenData = await response.json();
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
287
|
+
if (tokenData.id_token) {
|
|
288
|
+
try {
|
|
289
|
+
const payloadB64 = tokenData.id_token.split(".")[1];
|
|
290
|
+
const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
291
|
+
if (idTokenPayload.nonce !== expectedNonce) {
|
|
292
|
+
throw new Error("Nonce mismatch");
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
if (e.message === "Nonce mismatch") throw e;
|
|
296
|
+
throw new Error("Failed to parse ID token");
|
|
297
|
+
}
|
|
299
298
|
}
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
headers: userInfoEndpointHeaders
|
|
299
|
+
const userResponse = await fetch(config2.OIDC_USER_INFO_ENDPOINT, {
|
|
300
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
303
301
|
});
|
|
304
302
|
if (!userResponse.ok) {
|
|
305
|
-
|
|
306
|
-
throw new Error(
|
|
307
|
-
`Failed to fetch user info: ${userResponse.status} ${userResponse.statusText} - ${errText}`
|
|
308
|
-
);
|
|
303
|
+
throw new Error("Failed to fetch user info");
|
|
309
304
|
}
|
|
310
|
-
return
|
|
305
|
+
return userResponse.json();
|
|
311
306
|
}
|
|
312
307
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
313
308
|
let roles2 = [];
|
|
@@ -331,22 +326,16 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
|
|
|
331
326
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
332
327
|
const email = String(userResponseData.email).toLowerCase();
|
|
333
328
|
const whitelistUser = await whitelistService2.checkWhitelistForEmail(email);
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
userResponseData,
|
|
345
|
-
whitelistUser,
|
|
346
|
-
config2,
|
|
347
|
-
ctx
|
|
348
|
-
);
|
|
349
|
-
}
|
|
329
|
+
const activateUser = await userService.findOneByEmail(email) ?? await registerNewUser(
|
|
330
|
+
userService,
|
|
331
|
+
oauthService2,
|
|
332
|
+
roleService2,
|
|
333
|
+
email,
|
|
334
|
+
userResponseData,
|
|
335
|
+
whitelistUser,
|
|
336
|
+
config2,
|
|
337
|
+
ctx
|
|
338
|
+
);
|
|
350
339
|
const jwtToken = await oauthService2.generateToken(activateUser, ctx);
|
|
351
340
|
oauthService2.triggerSignInSuccess(activateUser);
|
|
352
341
|
return { activateUser, jwtToken };
|
|
@@ -362,8 +351,10 @@ async function oidcSignInCallback(ctx) {
|
|
|
362
351
|
}
|
|
363
352
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
364
353
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
354
|
+
const oidcNonce = ctx.cookies.get("oidc_nonce");
|
|
365
355
|
ctx.cookies.set("oidc_state", null);
|
|
366
356
|
ctx.cookies.set("oidc_code_verifier", null);
|
|
357
|
+
ctx.cookies.set("oidc_nonce", null);
|
|
367
358
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
368
359
|
return ctx.send(oauthService2.renderSignUpError("Invalid state"));
|
|
369
360
|
}
|
|
@@ -375,7 +366,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
375
366
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
376
367
|
params.append("code_verifier", codeVerifier);
|
|
377
368
|
try {
|
|
378
|
-
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params);
|
|
369
|
+
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce);
|
|
379
370
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
380
371
|
userService,
|
|
381
372
|
oauthService2,
|
|
@@ -391,7 +382,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
391
382
|
ctx.send(html);
|
|
392
383
|
} catch (e) {
|
|
393
384
|
console.error("ERROR CAUGHT IN OIDC SIGNIN:", e);
|
|
394
|
-
ctx.send(oauthService2.renderSignUpError(
|
|
385
|
+
ctx.send(oauthService2.renderSignUpError("Authentication failed. Please try again."));
|
|
395
386
|
}
|
|
396
387
|
}
|
|
397
388
|
async function logout(ctx) {
|
|
@@ -438,8 +429,11 @@ const role = {
|
|
|
438
429
|
find,
|
|
439
430
|
update
|
|
440
431
|
};
|
|
432
|
+
function getWhitelistService() {
|
|
433
|
+
return strapi.plugin("strapi-plugin-oidc").service("whitelist");
|
|
434
|
+
}
|
|
441
435
|
async function info(ctx) {
|
|
442
|
-
const whitelistService2 =
|
|
436
|
+
const whitelistService2 = getWhitelistService();
|
|
443
437
|
const settings = await whitelistService2.getSettings();
|
|
444
438
|
const whitelistUsers = await whitelistService2.getUsers();
|
|
445
439
|
ctx.body = {
|
|
@@ -450,8 +444,9 @@ async function info(ctx) {
|
|
|
450
444
|
};
|
|
451
445
|
}
|
|
452
446
|
async function updateSettings(ctx) {
|
|
453
|
-
|
|
454
|
-
|
|
447
|
+
const { useWhitelist } = ctx.request.body;
|
|
448
|
+
let { enforceOIDC } = ctx.request.body;
|
|
449
|
+
const whitelistService2 = getWhitelistService();
|
|
455
450
|
if (useWhitelist && enforceOIDC) {
|
|
456
451
|
const users = await whitelistService2.getUsers();
|
|
457
452
|
if (users.length === 0) {
|
|
@@ -462,7 +457,7 @@ async function updateSettings(ctx) {
|
|
|
462
457
|
ctx.body = { useWhitelist, enforceOIDC };
|
|
463
458
|
}
|
|
464
459
|
async function publicSettings(ctx) {
|
|
465
|
-
const whitelistService2 =
|
|
460
|
+
const whitelistService2 = getWhitelistService();
|
|
466
461
|
const settings = await whitelistService2.getSettings();
|
|
467
462
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
468
463
|
ctx.body = {
|
|
@@ -482,10 +477,11 @@ async function register(ctx) {
|
|
|
482
477
|
where: { email: { $in: emailList } },
|
|
483
478
|
populate: ["roles"]
|
|
484
479
|
});
|
|
485
|
-
const
|
|
480
|
+
const existingUsersByEmail = new Map(existingUsers.map((u) => [u.email, u]));
|
|
481
|
+
const whitelistService2 = getWhitelistService();
|
|
486
482
|
let matchedExistingUsersCount = 0;
|
|
487
483
|
for (const singleEmail of emailList) {
|
|
488
|
-
const existingUser =
|
|
484
|
+
const existingUser = existingUsersByEmail.get(singleEmail);
|
|
489
485
|
let finalRoles = roles2;
|
|
490
486
|
if (existingUser?.roles) {
|
|
491
487
|
finalRoles = existingUser.roles.map((r) => String(r.id));
|
|
@@ -502,12 +498,12 @@ async function register(ctx) {
|
|
|
502
498
|
}
|
|
503
499
|
async function removeEmail(ctx) {
|
|
504
500
|
const { id } = ctx.params;
|
|
505
|
-
const whitelistService2 =
|
|
501
|
+
const whitelistService2 = getWhitelistService();
|
|
506
502
|
await whitelistService2.removeUser(id);
|
|
507
503
|
ctx.body = {};
|
|
508
504
|
}
|
|
509
505
|
async function deleteAll(ctx) {
|
|
510
|
-
await strapi.
|
|
506
|
+
await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
|
|
511
507
|
ctx.body = {};
|
|
512
508
|
}
|
|
513
509
|
async function importUsers(ctx) {
|
|
@@ -535,7 +531,7 @@ async function importUsers(ctx) {
|
|
|
535
531
|
populate: ["roles"]
|
|
536
532
|
});
|
|
537
533
|
const strapiUserMap = new Map(strapiUsers.map((u) => [u.email, u]));
|
|
538
|
-
const whitelistService2 =
|
|
534
|
+
const whitelistService2 = getWhitelistService();
|
|
539
535
|
const existing = await whitelistService2.getUsers();
|
|
540
536
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
541
537
|
let importedCount = 0;
|
|
@@ -549,9 +545,9 @@ async function importUsers(ctx) {
|
|
|
549
545
|
ctx.body = { importedCount };
|
|
550
546
|
}
|
|
551
547
|
async function syncUsers(ctx) {
|
|
552
|
-
|
|
553
|
-
users =
|
|
554
|
-
const whitelistService2 =
|
|
548
|
+
const { users: rawUsers } = ctx.request.body;
|
|
549
|
+
const users = rawUsers.map((u) => ({ ...u, email: String(u.email).toLowerCase() }));
|
|
550
|
+
const whitelistService2 = getWhitelistService();
|
|
555
551
|
const currentUsers = await whitelistService2.getUsers();
|
|
556
552
|
let matchedExistingUsersCount = 0;
|
|
557
553
|
const emailsToSync = users.map((u) => u.email);
|
|
@@ -559,26 +555,29 @@ async function syncUsers(ctx) {
|
|
|
559
555
|
where: { email: { $in: emailsToSync } },
|
|
560
556
|
populate: ["roles"]
|
|
561
557
|
});
|
|
558
|
+
const syncEmailSet = new Set(emailsToSync);
|
|
559
|
+
const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
|
|
560
|
+
const strapiUsersByEmail = new Map(existingStrapiUsers.map((u) => [u.email, u]));
|
|
562
561
|
for (const currUser of currentUsers) {
|
|
563
|
-
if (!
|
|
562
|
+
if (!syncEmailSet.has(currUser.email)) {
|
|
564
563
|
await whitelistService2.removeUser(currUser.id);
|
|
565
564
|
}
|
|
566
565
|
}
|
|
567
566
|
for (const user of users) {
|
|
568
|
-
const
|
|
567
|
+
const currUser = currentUsersByEmail.get(user.email);
|
|
569
568
|
let finalRoles = user.roles;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
569
|
+
if (!currUser) {
|
|
570
|
+
const existingStrapiUser = strapiUsersByEmail.get(user.email);
|
|
571
|
+
if (existingStrapiUser?.roles) {
|
|
572
|
+
finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
|
|
573
|
+
matchedExistingUsersCount++;
|
|
574
|
+
}
|
|
575
|
+
await whitelistService2.registerUser(user.email, finalRoles);
|
|
576
|
+
} else {
|
|
576
577
|
await strapi.query("plugin::strapi-plugin-oidc.whitelists").update({
|
|
577
578
|
where: { id: currUser.id },
|
|
578
579
|
data: { roles: finalRoles }
|
|
579
580
|
});
|
|
580
|
-
} else {
|
|
581
|
-
await whitelistService2.registerUser(user.email, finalRoles);
|
|
582
581
|
}
|
|
583
582
|
}
|
|
584
583
|
ctx.body = { matchedExistingUsersCount };
|
|
@@ -877,7 +876,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
877
876
|
roles: roles2,
|
|
878
877
|
preferedLanguage: locale
|
|
879
878
|
});
|
|
880
|
-
return
|
|
879
|
+
return userService.register({
|
|
881
880
|
registrationToken: createdUser.registrationToken,
|
|
882
881
|
userInfo: {
|
|
883
882
|
firstname: firstname || "unset",
|
|
@@ -898,7 +897,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
898
897
|
if (!baseAlias) {
|
|
899
898
|
return baseEmail;
|
|
900
899
|
}
|
|
901
|
-
const alias = baseAlias.replace(
|
|
900
|
+
const alias = baseAlias.replace(/\+/g, "");
|
|
902
901
|
const beforePosition = baseEmail.indexOf("@");
|
|
903
902
|
const origin = baseEmail.substring(0, beforePosition);
|
|
904
903
|
const domain = baseEmail.substring(beforePosition);
|
|
@@ -935,11 +934,9 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
935
934
|
provider: "strapi-plugin-oidc"
|
|
936
935
|
});
|
|
937
936
|
},
|
|
938
|
-
// Sign In Success
|
|
939
937
|
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
940
938
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
941
|
-
const
|
|
942
|
-
const isRememberMe = !!REMEMBER_ME;
|
|
939
|
+
const isRememberMe = !!config2["REMEMBER_ME"];
|
|
943
940
|
const content = `
|
|
944
941
|
<noscript>
|
|
945
942
|
<div class="card">
|
|
@@ -965,7 +962,6 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
965
962
|
<\/script>`;
|
|
966
963
|
return renderHtmlTemplate("Authenticating...", content);
|
|
967
964
|
},
|
|
968
|
-
// Sign In Error
|
|
969
965
|
renderSignUpError(message) {
|
|
970
966
|
const safeMessage = String(message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
971
967
|
const content = `
|
|
@@ -993,8 +989,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
993
989
|
const userId = String(user.id);
|
|
994
990
|
const deviceId = node_crypto.randomUUID();
|
|
995
991
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
996
|
-
const
|
|
997
|
-
const rememberMe = !!REMEMBER_ME;
|
|
992
|
+
const rememberMe = !!config2["REMEMBER_ME"];
|
|
998
993
|
const { token: refreshToken, absoluteExpiresAt } = await sessionManager(
|
|
999
994
|
"admin"
|
|
1000
995
|
).generateRefreshToken(userId, deviceId, {
|
package/dist/server/index.mjs
CHANGED
|
@@ -158,7 +158,6 @@ const config = {
|
|
|
158
158
|
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
159
159
|
OIDC_TOKEN_ENDPOINT: "",
|
|
160
160
|
OIDC_USER_INFO_ENDPOINT: "",
|
|
161
|
-
OIDC_USER_INFO_ENDPOINT_WITH_AUTH_HEADER: false,
|
|
162
161
|
OIDC_GRANT_TYPE: "authorization_code",
|
|
163
162
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
164
163
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
@@ -217,45 +216,43 @@ function clearAuthCookies(strapi2, ctx) {
|
|
|
217
216
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
218
217
|
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
219
218
|
}
|
|
219
|
+
const REQUIRED_CONFIG_KEYS = [
|
|
220
|
+
"OIDC_CLIENT_ID",
|
|
221
|
+
"OIDC_CLIENT_SECRET",
|
|
222
|
+
"OIDC_REDIRECT_URI",
|
|
223
|
+
"OIDC_SCOPES",
|
|
224
|
+
"OIDC_TOKEN_ENDPOINT",
|
|
225
|
+
"OIDC_USER_INFO_ENDPOINT",
|
|
226
|
+
"OIDC_GRANT_TYPE",
|
|
227
|
+
"OIDC_FAMILY_NAME_FIELD",
|
|
228
|
+
"OIDC_GIVEN_NAME_FIELD",
|
|
229
|
+
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
230
|
+
];
|
|
220
231
|
function configValidation() {
|
|
221
232
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
222
|
-
|
|
223
|
-
"OIDC_CLIENT_ID",
|
|
224
|
-
"OIDC_CLIENT_SECRET",
|
|
225
|
-
"OIDC_REDIRECT_URI",
|
|
226
|
-
"OIDC_SCOPES",
|
|
227
|
-
"OIDC_TOKEN_ENDPOINT",
|
|
228
|
-
"OIDC_USER_INFO_ENDPOINT",
|
|
229
|
-
"OIDC_GRANT_TYPE",
|
|
230
|
-
"OIDC_FAMILY_NAME_FIELD",
|
|
231
|
-
"OIDC_GIVEN_NAME_FIELD",
|
|
232
|
-
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
233
|
-
];
|
|
234
|
-
if (requiredKeys.every((key) => config2[key])) {
|
|
233
|
+
if (REQUIRED_CONFIG_KEYS.every((key) => config2[key])) {
|
|
235
234
|
return config2;
|
|
236
235
|
}
|
|
237
|
-
throw new Error(
|
|
236
|
+
throw new Error(
|
|
237
|
+
`The following configuration keys are required: ${REQUIRED_CONFIG_KEYS.join(", ")}`
|
|
238
|
+
);
|
|
238
239
|
}
|
|
239
240
|
async function oidcSignIn(ctx) {
|
|
240
|
-
let { state } = ctx.query;
|
|
241
241
|
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPES, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
|
|
242
242
|
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
243
|
+
const state = randomBytes(32).toString("base64url");
|
|
244
|
+
const nonce = randomBytes(32).toString("base64url");
|
|
246
245
|
const isProduction = strapi.config.get("environment") === "production";
|
|
247
|
-
|
|
246
|
+
const cookieOptions = {
|
|
248
247
|
httpOnly: true,
|
|
249
248
|
maxAge: 6e5,
|
|
249
|
+
// 10 minutes
|
|
250
250
|
secure: isProduction && ctx.request.secure,
|
|
251
251
|
sameSite: "lax"
|
|
252
|
-
}
|
|
253
|
-
ctx.cookies.set("
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
secure: isProduction && ctx.request.secure,
|
|
257
|
-
sameSite: "lax"
|
|
258
|
-
});
|
|
252
|
+
};
|
|
253
|
+
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
254
|
+
ctx.cookies.set("oidc_state", state, cookieOptions);
|
|
255
|
+
ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
|
|
259
256
|
const params = new URLSearchParams();
|
|
260
257
|
params.append("response_type", "code");
|
|
261
258
|
params.append("client_id", OIDC_CLIENT_ID);
|
|
@@ -264,11 +261,12 @@ async function oidcSignIn(ctx) {
|
|
|
264
261
|
params.append("code_challenge", codeChallenge);
|
|
265
262
|
params.append("code_challenge_method", "S256");
|
|
266
263
|
params.append("state", state);
|
|
264
|
+
params.append("nonce", nonce);
|
|
267
265
|
const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
|
|
268
266
|
ctx.set("Location", authorizationUrl);
|
|
269
267
|
return ctx.send({}, 302);
|
|
270
268
|
}
|
|
271
|
-
async function exchangeTokenAndFetchUserInfo(config2, params) {
|
|
269
|
+
async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
272
270
|
const response = await fetch(config2.OIDC_TOKEN_ENDPOINT, {
|
|
273
271
|
method: "POST",
|
|
274
272
|
body: params,
|
|
@@ -277,31 +275,28 @@ async function exchangeTokenAndFetchUserInfo(config2, params) {
|
|
|
277
275
|
}
|
|
278
276
|
});
|
|
279
277
|
if (!response.ok) {
|
|
280
|
-
|
|
281
|
-
throw new Error(
|
|
282
|
-
`Failed to exchange token: ${response.status} ${response.statusText} - ${errText}`
|
|
283
|
-
);
|
|
278
|
+
throw new Error("Token exchange failed");
|
|
284
279
|
}
|
|
285
280
|
const tokenData = await response.json();
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
281
|
+
if (tokenData.id_token) {
|
|
282
|
+
try {
|
|
283
|
+
const payloadB64 = tokenData.id_token.split(".")[1];
|
|
284
|
+
const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
285
|
+
if (idTokenPayload.nonce !== expectedNonce) {
|
|
286
|
+
throw new Error("Nonce mismatch");
|
|
287
|
+
}
|
|
288
|
+
} catch (e) {
|
|
289
|
+
if (e.message === "Nonce mismatch") throw e;
|
|
290
|
+
throw new Error("Failed to parse ID token");
|
|
291
|
+
}
|
|
293
292
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
headers: userInfoEndpointHeaders
|
|
293
|
+
const userResponse = await fetch(config2.OIDC_USER_INFO_ENDPOINT, {
|
|
294
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
297
295
|
});
|
|
298
296
|
if (!userResponse.ok) {
|
|
299
|
-
|
|
300
|
-
throw new Error(
|
|
301
|
-
`Failed to fetch user info: ${userResponse.status} ${userResponse.statusText} - ${errText}`
|
|
302
|
-
);
|
|
297
|
+
throw new Error("Failed to fetch user info");
|
|
303
298
|
}
|
|
304
|
-
return
|
|
299
|
+
return userResponse.json();
|
|
305
300
|
}
|
|
306
301
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
307
302
|
let roles2 = [];
|
|
@@ -325,22 +320,16 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
|
|
|
325
320
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
326
321
|
const email = String(userResponseData.email).toLowerCase();
|
|
327
322
|
const whitelistUser = await whitelistService2.checkWhitelistForEmail(email);
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
userResponseData,
|
|
339
|
-
whitelistUser,
|
|
340
|
-
config2,
|
|
341
|
-
ctx
|
|
342
|
-
);
|
|
343
|
-
}
|
|
323
|
+
const activateUser = await userService.findOneByEmail(email) ?? await registerNewUser(
|
|
324
|
+
userService,
|
|
325
|
+
oauthService2,
|
|
326
|
+
roleService2,
|
|
327
|
+
email,
|
|
328
|
+
userResponseData,
|
|
329
|
+
whitelistUser,
|
|
330
|
+
config2,
|
|
331
|
+
ctx
|
|
332
|
+
);
|
|
344
333
|
const jwtToken = await oauthService2.generateToken(activateUser, ctx);
|
|
345
334
|
oauthService2.triggerSignInSuccess(activateUser);
|
|
346
335
|
return { activateUser, jwtToken };
|
|
@@ -356,8 +345,10 @@ async function oidcSignInCallback(ctx) {
|
|
|
356
345
|
}
|
|
357
346
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
358
347
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
348
|
+
const oidcNonce = ctx.cookies.get("oidc_nonce");
|
|
359
349
|
ctx.cookies.set("oidc_state", null);
|
|
360
350
|
ctx.cookies.set("oidc_code_verifier", null);
|
|
351
|
+
ctx.cookies.set("oidc_nonce", null);
|
|
361
352
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
362
353
|
return ctx.send(oauthService2.renderSignUpError("Invalid state"));
|
|
363
354
|
}
|
|
@@ -369,7 +360,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
369
360
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
370
361
|
params.append("code_verifier", codeVerifier);
|
|
371
362
|
try {
|
|
372
|
-
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params);
|
|
363
|
+
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce);
|
|
373
364
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
374
365
|
userService,
|
|
375
366
|
oauthService2,
|
|
@@ -385,7 +376,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
385
376
|
ctx.send(html);
|
|
386
377
|
} catch (e) {
|
|
387
378
|
console.error("ERROR CAUGHT IN OIDC SIGNIN:", e);
|
|
388
|
-
ctx.send(oauthService2.renderSignUpError(
|
|
379
|
+
ctx.send(oauthService2.renderSignUpError("Authentication failed. Please try again."));
|
|
389
380
|
}
|
|
390
381
|
}
|
|
391
382
|
async function logout(ctx) {
|
|
@@ -432,8 +423,11 @@ const role = {
|
|
|
432
423
|
find,
|
|
433
424
|
update
|
|
434
425
|
};
|
|
426
|
+
function getWhitelistService() {
|
|
427
|
+
return strapi.plugin("strapi-plugin-oidc").service("whitelist");
|
|
428
|
+
}
|
|
435
429
|
async function info(ctx) {
|
|
436
|
-
const whitelistService2 =
|
|
430
|
+
const whitelistService2 = getWhitelistService();
|
|
437
431
|
const settings = await whitelistService2.getSettings();
|
|
438
432
|
const whitelistUsers = await whitelistService2.getUsers();
|
|
439
433
|
ctx.body = {
|
|
@@ -444,8 +438,9 @@ async function info(ctx) {
|
|
|
444
438
|
};
|
|
445
439
|
}
|
|
446
440
|
async function updateSettings(ctx) {
|
|
447
|
-
|
|
448
|
-
|
|
441
|
+
const { useWhitelist } = ctx.request.body;
|
|
442
|
+
let { enforceOIDC } = ctx.request.body;
|
|
443
|
+
const whitelistService2 = getWhitelistService();
|
|
449
444
|
if (useWhitelist && enforceOIDC) {
|
|
450
445
|
const users = await whitelistService2.getUsers();
|
|
451
446
|
if (users.length === 0) {
|
|
@@ -456,7 +451,7 @@ async function updateSettings(ctx) {
|
|
|
456
451
|
ctx.body = { useWhitelist, enforceOIDC };
|
|
457
452
|
}
|
|
458
453
|
async function publicSettings(ctx) {
|
|
459
|
-
const whitelistService2 =
|
|
454
|
+
const whitelistService2 = getWhitelistService();
|
|
460
455
|
const settings = await whitelistService2.getSettings();
|
|
461
456
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
462
457
|
ctx.body = {
|
|
@@ -476,10 +471,11 @@ async function register(ctx) {
|
|
|
476
471
|
where: { email: { $in: emailList } },
|
|
477
472
|
populate: ["roles"]
|
|
478
473
|
});
|
|
479
|
-
const
|
|
474
|
+
const existingUsersByEmail = new Map(existingUsers.map((u) => [u.email, u]));
|
|
475
|
+
const whitelistService2 = getWhitelistService();
|
|
480
476
|
let matchedExistingUsersCount = 0;
|
|
481
477
|
for (const singleEmail of emailList) {
|
|
482
|
-
const existingUser =
|
|
478
|
+
const existingUser = existingUsersByEmail.get(singleEmail);
|
|
483
479
|
let finalRoles = roles2;
|
|
484
480
|
if (existingUser?.roles) {
|
|
485
481
|
finalRoles = existingUser.roles.map((r) => String(r.id));
|
|
@@ -496,12 +492,12 @@ async function register(ctx) {
|
|
|
496
492
|
}
|
|
497
493
|
async function removeEmail(ctx) {
|
|
498
494
|
const { id } = ctx.params;
|
|
499
|
-
const whitelistService2 =
|
|
495
|
+
const whitelistService2 = getWhitelistService();
|
|
500
496
|
await whitelistService2.removeUser(id);
|
|
501
497
|
ctx.body = {};
|
|
502
498
|
}
|
|
503
499
|
async function deleteAll(ctx) {
|
|
504
|
-
await strapi.
|
|
500
|
+
await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
|
|
505
501
|
ctx.body = {};
|
|
506
502
|
}
|
|
507
503
|
async function importUsers(ctx) {
|
|
@@ -529,7 +525,7 @@ async function importUsers(ctx) {
|
|
|
529
525
|
populate: ["roles"]
|
|
530
526
|
});
|
|
531
527
|
const strapiUserMap = new Map(strapiUsers.map((u) => [u.email, u]));
|
|
532
|
-
const whitelistService2 =
|
|
528
|
+
const whitelistService2 = getWhitelistService();
|
|
533
529
|
const existing = await whitelistService2.getUsers();
|
|
534
530
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
535
531
|
let importedCount = 0;
|
|
@@ -543,9 +539,9 @@ async function importUsers(ctx) {
|
|
|
543
539
|
ctx.body = { importedCount };
|
|
544
540
|
}
|
|
545
541
|
async function syncUsers(ctx) {
|
|
546
|
-
|
|
547
|
-
users =
|
|
548
|
-
const whitelistService2 =
|
|
542
|
+
const { users: rawUsers } = ctx.request.body;
|
|
543
|
+
const users = rawUsers.map((u) => ({ ...u, email: String(u.email).toLowerCase() }));
|
|
544
|
+
const whitelistService2 = getWhitelistService();
|
|
549
545
|
const currentUsers = await whitelistService2.getUsers();
|
|
550
546
|
let matchedExistingUsersCount = 0;
|
|
551
547
|
const emailsToSync = users.map((u) => u.email);
|
|
@@ -553,26 +549,29 @@ async function syncUsers(ctx) {
|
|
|
553
549
|
where: { email: { $in: emailsToSync } },
|
|
554
550
|
populate: ["roles"]
|
|
555
551
|
});
|
|
552
|
+
const syncEmailSet = new Set(emailsToSync);
|
|
553
|
+
const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
|
|
554
|
+
const strapiUsersByEmail = new Map(existingStrapiUsers.map((u) => [u.email, u]));
|
|
556
555
|
for (const currUser of currentUsers) {
|
|
557
|
-
if (!
|
|
556
|
+
if (!syncEmailSet.has(currUser.email)) {
|
|
558
557
|
await whitelistService2.removeUser(currUser.id);
|
|
559
558
|
}
|
|
560
559
|
}
|
|
561
560
|
for (const user of users) {
|
|
562
|
-
const
|
|
561
|
+
const currUser = currentUsersByEmail.get(user.email);
|
|
563
562
|
let finalRoles = user.roles;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
563
|
+
if (!currUser) {
|
|
564
|
+
const existingStrapiUser = strapiUsersByEmail.get(user.email);
|
|
565
|
+
if (existingStrapiUser?.roles) {
|
|
566
|
+
finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
|
|
567
|
+
matchedExistingUsersCount++;
|
|
568
|
+
}
|
|
569
|
+
await whitelistService2.registerUser(user.email, finalRoles);
|
|
570
|
+
} else {
|
|
570
571
|
await strapi.query("plugin::strapi-plugin-oidc.whitelists").update({
|
|
571
572
|
where: { id: currUser.id },
|
|
572
573
|
data: { roles: finalRoles }
|
|
573
574
|
});
|
|
574
|
-
} else {
|
|
575
|
-
await whitelistService2.registerUser(user.email, finalRoles);
|
|
576
575
|
}
|
|
577
576
|
}
|
|
578
577
|
ctx.body = { matchedExistingUsersCount };
|
|
@@ -871,7 +870,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
871
870
|
roles: roles2,
|
|
872
871
|
preferedLanguage: locale
|
|
873
872
|
});
|
|
874
|
-
return
|
|
873
|
+
return userService.register({
|
|
875
874
|
registrationToken: createdUser.registrationToken,
|
|
876
875
|
userInfo: {
|
|
877
876
|
firstname: firstname || "unset",
|
|
@@ -892,7 +891,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
892
891
|
if (!baseAlias) {
|
|
893
892
|
return baseEmail;
|
|
894
893
|
}
|
|
895
|
-
const alias = baseAlias.replace(
|
|
894
|
+
const alias = baseAlias.replace(/\+/g, "");
|
|
896
895
|
const beforePosition = baseEmail.indexOf("@");
|
|
897
896
|
const origin = baseEmail.substring(0, beforePosition);
|
|
898
897
|
const domain = baseEmail.substring(beforePosition);
|
|
@@ -929,11 +928,9 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
929
928
|
provider: "strapi-plugin-oidc"
|
|
930
929
|
});
|
|
931
930
|
},
|
|
932
|
-
// Sign In Success
|
|
933
931
|
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
934
932
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
935
|
-
const
|
|
936
|
-
const isRememberMe = !!REMEMBER_ME;
|
|
933
|
+
const isRememberMe = !!config2["REMEMBER_ME"];
|
|
937
934
|
const content = `
|
|
938
935
|
<noscript>
|
|
939
936
|
<div class="card">
|
|
@@ -959,7 +956,6 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
959
956
|
<\/script>`;
|
|
960
957
|
return renderHtmlTemplate("Authenticating...", content);
|
|
961
958
|
},
|
|
962
|
-
// Sign In Error
|
|
963
959
|
renderSignUpError(message) {
|
|
964
960
|
const safeMessage = String(message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
965
961
|
const content = `
|
|
@@ -987,8 +983,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
987
983
|
const userId = String(user.id);
|
|
988
984
|
const deviceId = randomUUID();
|
|
989
985
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
990
|
-
const
|
|
991
|
-
const rememberMe = !!REMEMBER_ME;
|
|
986
|
+
const rememberMe = !!config2["REMEMBER_ME"];
|
|
992
987
|
const { token: refreshToken, absoluteExpiresAt } = await sessionManager(
|
|
993
988
|
"admin"
|
|
994
989
|
).generateRefreshToken(userId, deviceId, {
|
package/package.json
CHANGED