strapi-plugin-oidc 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +91 -95
- package/dist/server/index.mjs +91 -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
|
@@ -30,6 +30,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
30
30
|
const authRoutes = [
|
|
31
31
|
`${adminUrl}/login`,
|
|
32
32
|
`${adminUrl}/register`,
|
|
33
|
+
`${adminUrl}/register-admin`,
|
|
33
34
|
`${adminUrl}/forgot-password`,
|
|
34
35
|
`${adminUrl}/reset-password`
|
|
35
36
|
];
|
|
@@ -164,7 +165,6 @@ const config = {
|
|
|
164
165
|
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
165
166
|
OIDC_TOKEN_ENDPOINT: "",
|
|
166
167
|
OIDC_USER_INFO_ENDPOINT: "",
|
|
167
|
-
OIDC_USER_INFO_ENDPOINT_WITH_AUTH_HEADER: false,
|
|
168
168
|
OIDC_GRANT_TYPE: "authorization_code",
|
|
169
169
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
170
170
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
@@ -223,45 +223,43 @@ function clearAuthCookies(strapi2, ctx) {
|
|
|
223
223
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
224
224
|
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
225
225
|
}
|
|
226
|
+
const REQUIRED_CONFIG_KEYS = [
|
|
227
|
+
"OIDC_CLIENT_ID",
|
|
228
|
+
"OIDC_CLIENT_SECRET",
|
|
229
|
+
"OIDC_REDIRECT_URI",
|
|
230
|
+
"OIDC_SCOPES",
|
|
231
|
+
"OIDC_TOKEN_ENDPOINT",
|
|
232
|
+
"OIDC_USER_INFO_ENDPOINT",
|
|
233
|
+
"OIDC_GRANT_TYPE",
|
|
234
|
+
"OIDC_FAMILY_NAME_FIELD",
|
|
235
|
+
"OIDC_GIVEN_NAME_FIELD",
|
|
236
|
+
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
237
|
+
];
|
|
226
238
|
function configValidation() {
|
|
227
239
|
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])) {
|
|
240
|
+
if (REQUIRED_CONFIG_KEYS.every((key) => config2[key])) {
|
|
241
241
|
return config2;
|
|
242
242
|
}
|
|
243
|
-
throw new Error(
|
|
243
|
+
throw new Error(
|
|
244
|
+
`The following configuration keys are required: ${REQUIRED_CONFIG_KEYS.join(", ")}`
|
|
245
|
+
);
|
|
244
246
|
}
|
|
245
247
|
async function oidcSignIn(ctx) {
|
|
246
|
-
let { state } = ctx.query;
|
|
247
248
|
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPES, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
|
|
248
249
|
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge__default.default();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
250
|
+
const state = node_crypto.randomBytes(32).toString("base64url");
|
|
251
|
+
const nonce = node_crypto.randomBytes(32).toString("base64url");
|
|
252
252
|
const isProduction = strapi.config.get("environment") === "production";
|
|
253
|
-
|
|
253
|
+
const cookieOptions = {
|
|
254
254
|
httpOnly: true,
|
|
255
255
|
maxAge: 6e5,
|
|
256
|
+
// 10 minutes
|
|
256
257
|
secure: isProduction && ctx.request.secure,
|
|
257
258
|
sameSite: "lax"
|
|
258
|
-
}
|
|
259
|
-
ctx.cookies.set("
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
secure: isProduction && ctx.request.secure,
|
|
263
|
-
sameSite: "lax"
|
|
264
|
-
});
|
|
259
|
+
};
|
|
260
|
+
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
261
|
+
ctx.cookies.set("oidc_state", state, cookieOptions);
|
|
262
|
+
ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
|
|
265
263
|
const params = new URLSearchParams();
|
|
266
264
|
params.append("response_type", "code");
|
|
267
265
|
params.append("client_id", OIDC_CLIENT_ID);
|
|
@@ -270,11 +268,12 @@ async function oidcSignIn(ctx) {
|
|
|
270
268
|
params.append("code_challenge", codeChallenge);
|
|
271
269
|
params.append("code_challenge_method", "S256");
|
|
272
270
|
params.append("state", state);
|
|
271
|
+
params.append("nonce", nonce);
|
|
273
272
|
const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
|
|
274
273
|
ctx.set("Location", authorizationUrl);
|
|
275
274
|
return ctx.send({}, 302);
|
|
276
275
|
}
|
|
277
|
-
async function exchangeTokenAndFetchUserInfo(config2, params) {
|
|
276
|
+
async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
278
277
|
const response = await fetch(config2.OIDC_TOKEN_ENDPOINT, {
|
|
279
278
|
method: "POST",
|
|
280
279
|
body: params,
|
|
@@ -283,31 +282,28 @@ async function exchangeTokenAndFetchUserInfo(config2, params) {
|
|
|
283
282
|
}
|
|
284
283
|
});
|
|
285
284
|
if (!response.ok) {
|
|
286
|
-
|
|
287
|
-
throw new Error(
|
|
288
|
-
`Failed to exchange token: ${response.status} ${response.statusText} - ${errText}`
|
|
289
|
-
);
|
|
285
|
+
throw new Error("Token exchange failed");
|
|
290
286
|
}
|
|
291
287
|
const tokenData = await response.json();
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
288
|
+
if (tokenData.id_token) {
|
|
289
|
+
try {
|
|
290
|
+
const payloadB64 = tokenData.id_token.split(".")[1];
|
|
291
|
+
const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
292
|
+
if (idTokenPayload.nonce !== expectedNonce) {
|
|
293
|
+
throw new Error("Nonce mismatch");
|
|
294
|
+
}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
if (e.message === "Nonce mismatch") throw e;
|
|
297
|
+
throw new Error("Failed to parse ID token");
|
|
298
|
+
}
|
|
299
299
|
}
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
headers: userInfoEndpointHeaders
|
|
300
|
+
const userResponse = await fetch(config2.OIDC_USER_INFO_ENDPOINT, {
|
|
301
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
303
302
|
});
|
|
304
303
|
if (!userResponse.ok) {
|
|
305
|
-
|
|
306
|
-
throw new Error(
|
|
307
|
-
`Failed to fetch user info: ${userResponse.status} ${userResponse.statusText} - ${errText}`
|
|
308
|
-
);
|
|
304
|
+
throw new Error("Failed to fetch user info");
|
|
309
305
|
}
|
|
310
|
-
return
|
|
306
|
+
return userResponse.json();
|
|
311
307
|
}
|
|
312
308
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
313
309
|
let roles2 = [];
|
|
@@ -331,22 +327,16 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
|
|
|
331
327
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
332
328
|
const email = String(userResponseData.email).toLowerCase();
|
|
333
329
|
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
|
-
}
|
|
330
|
+
const activateUser = await userService.findOneByEmail(email) ?? await registerNewUser(
|
|
331
|
+
userService,
|
|
332
|
+
oauthService2,
|
|
333
|
+
roleService2,
|
|
334
|
+
email,
|
|
335
|
+
userResponseData,
|
|
336
|
+
whitelistUser,
|
|
337
|
+
config2,
|
|
338
|
+
ctx
|
|
339
|
+
);
|
|
350
340
|
const jwtToken = await oauthService2.generateToken(activateUser, ctx);
|
|
351
341
|
oauthService2.triggerSignInSuccess(activateUser);
|
|
352
342
|
return { activateUser, jwtToken };
|
|
@@ -362,8 +352,10 @@ async function oidcSignInCallback(ctx) {
|
|
|
362
352
|
}
|
|
363
353
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
364
354
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
355
|
+
const oidcNonce = ctx.cookies.get("oidc_nonce");
|
|
365
356
|
ctx.cookies.set("oidc_state", null);
|
|
366
357
|
ctx.cookies.set("oidc_code_verifier", null);
|
|
358
|
+
ctx.cookies.set("oidc_nonce", null);
|
|
367
359
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
368
360
|
return ctx.send(oauthService2.renderSignUpError("Invalid state"));
|
|
369
361
|
}
|
|
@@ -375,7 +367,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
375
367
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
376
368
|
params.append("code_verifier", codeVerifier);
|
|
377
369
|
try {
|
|
378
|
-
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params);
|
|
370
|
+
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce);
|
|
379
371
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
380
372
|
userService,
|
|
381
373
|
oauthService2,
|
|
@@ -391,7 +383,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
391
383
|
ctx.send(html);
|
|
392
384
|
} catch (e) {
|
|
393
385
|
console.error("ERROR CAUGHT IN OIDC SIGNIN:", e);
|
|
394
|
-
ctx.send(oauthService2.renderSignUpError(
|
|
386
|
+
ctx.send(oauthService2.renderSignUpError("Authentication failed. Please try again."));
|
|
395
387
|
}
|
|
396
388
|
}
|
|
397
389
|
async function logout(ctx) {
|
|
@@ -438,8 +430,11 @@ const role = {
|
|
|
438
430
|
find,
|
|
439
431
|
update
|
|
440
432
|
};
|
|
433
|
+
function getWhitelistService() {
|
|
434
|
+
return strapi.plugin("strapi-plugin-oidc").service("whitelist");
|
|
435
|
+
}
|
|
441
436
|
async function info(ctx) {
|
|
442
|
-
const whitelistService2 =
|
|
437
|
+
const whitelistService2 = getWhitelistService();
|
|
443
438
|
const settings = await whitelistService2.getSettings();
|
|
444
439
|
const whitelistUsers = await whitelistService2.getUsers();
|
|
445
440
|
ctx.body = {
|
|
@@ -450,8 +445,9 @@ async function info(ctx) {
|
|
|
450
445
|
};
|
|
451
446
|
}
|
|
452
447
|
async function updateSettings(ctx) {
|
|
453
|
-
|
|
454
|
-
|
|
448
|
+
const { useWhitelist } = ctx.request.body;
|
|
449
|
+
let { enforceOIDC } = ctx.request.body;
|
|
450
|
+
const whitelistService2 = getWhitelistService();
|
|
455
451
|
if (useWhitelist && enforceOIDC) {
|
|
456
452
|
const users = await whitelistService2.getUsers();
|
|
457
453
|
if (users.length === 0) {
|
|
@@ -462,7 +458,7 @@ async function updateSettings(ctx) {
|
|
|
462
458
|
ctx.body = { useWhitelist, enforceOIDC };
|
|
463
459
|
}
|
|
464
460
|
async function publicSettings(ctx) {
|
|
465
|
-
const whitelistService2 =
|
|
461
|
+
const whitelistService2 = getWhitelistService();
|
|
466
462
|
const settings = await whitelistService2.getSettings();
|
|
467
463
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
468
464
|
ctx.body = {
|
|
@@ -482,10 +478,11 @@ async function register(ctx) {
|
|
|
482
478
|
where: { email: { $in: emailList } },
|
|
483
479
|
populate: ["roles"]
|
|
484
480
|
});
|
|
485
|
-
const
|
|
481
|
+
const existingUsersByEmail = new Map(existingUsers.map((u) => [u.email, u]));
|
|
482
|
+
const whitelistService2 = getWhitelistService();
|
|
486
483
|
let matchedExistingUsersCount = 0;
|
|
487
484
|
for (const singleEmail of emailList) {
|
|
488
|
-
const existingUser =
|
|
485
|
+
const existingUser = existingUsersByEmail.get(singleEmail);
|
|
489
486
|
let finalRoles = roles2;
|
|
490
487
|
if (existingUser?.roles) {
|
|
491
488
|
finalRoles = existingUser.roles.map((r) => String(r.id));
|
|
@@ -502,12 +499,12 @@ async function register(ctx) {
|
|
|
502
499
|
}
|
|
503
500
|
async function removeEmail(ctx) {
|
|
504
501
|
const { id } = ctx.params;
|
|
505
|
-
const whitelistService2 =
|
|
502
|
+
const whitelistService2 = getWhitelistService();
|
|
506
503
|
await whitelistService2.removeUser(id);
|
|
507
504
|
ctx.body = {};
|
|
508
505
|
}
|
|
509
506
|
async function deleteAll(ctx) {
|
|
510
|
-
await strapi.
|
|
507
|
+
await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
|
|
511
508
|
ctx.body = {};
|
|
512
509
|
}
|
|
513
510
|
async function importUsers(ctx) {
|
|
@@ -535,7 +532,7 @@ async function importUsers(ctx) {
|
|
|
535
532
|
populate: ["roles"]
|
|
536
533
|
});
|
|
537
534
|
const strapiUserMap = new Map(strapiUsers.map((u) => [u.email, u]));
|
|
538
|
-
const whitelistService2 =
|
|
535
|
+
const whitelistService2 = getWhitelistService();
|
|
539
536
|
const existing = await whitelistService2.getUsers();
|
|
540
537
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
541
538
|
let importedCount = 0;
|
|
@@ -549,9 +546,9 @@ async function importUsers(ctx) {
|
|
|
549
546
|
ctx.body = { importedCount };
|
|
550
547
|
}
|
|
551
548
|
async function syncUsers(ctx) {
|
|
552
|
-
|
|
553
|
-
users =
|
|
554
|
-
const whitelistService2 =
|
|
549
|
+
const { users: rawUsers } = ctx.request.body;
|
|
550
|
+
const users = rawUsers.map((u) => ({ ...u, email: String(u.email).toLowerCase() }));
|
|
551
|
+
const whitelistService2 = getWhitelistService();
|
|
555
552
|
const currentUsers = await whitelistService2.getUsers();
|
|
556
553
|
let matchedExistingUsersCount = 0;
|
|
557
554
|
const emailsToSync = users.map((u) => u.email);
|
|
@@ -559,26 +556,29 @@ async function syncUsers(ctx) {
|
|
|
559
556
|
where: { email: { $in: emailsToSync } },
|
|
560
557
|
populate: ["roles"]
|
|
561
558
|
});
|
|
559
|
+
const syncEmailSet = new Set(emailsToSync);
|
|
560
|
+
const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
|
|
561
|
+
const strapiUsersByEmail = new Map(existingStrapiUsers.map((u) => [u.email, u]));
|
|
562
562
|
for (const currUser of currentUsers) {
|
|
563
|
-
if (!
|
|
563
|
+
if (!syncEmailSet.has(currUser.email)) {
|
|
564
564
|
await whitelistService2.removeUser(currUser.id);
|
|
565
565
|
}
|
|
566
566
|
}
|
|
567
567
|
for (const user of users) {
|
|
568
|
-
const
|
|
568
|
+
const currUser = currentUsersByEmail.get(user.email);
|
|
569
569
|
let finalRoles = user.roles;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
570
|
+
if (!currUser) {
|
|
571
|
+
const existingStrapiUser = strapiUsersByEmail.get(user.email);
|
|
572
|
+
if (existingStrapiUser?.roles) {
|
|
573
|
+
finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
|
|
574
|
+
matchedExistingUsersCount++;
|
|
575
|
+
}
|
|
576
|
+
await whitelistService2.registerUser(user.email, finalRoles);
|
|
577
|
+
} else {
|
|
576
578
|
await strapi.query("plugin::strapi-plugin-oidc.whitelists").update({
|
|
577
579
|
where: { id: currUser.id },
|
|
578
580
|
data: { roles: finalRoles }
|
|
579
581
|
});
|
|
580
|
-
} else {
|
|
581
|
-
await whitelistService2.registerUser(user.email, finalRoles);
|
|
582
582
|
}
|
|
583
583
|
}
|
|
584
584
|
ctx.body = { matchedExistingUsersCount };
|
|
@@ -877,7 +877,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
877
877
|
roles: roles2,
|
|
878
878
|
preferedLanguage: locale
|
|
879
879
|
});
|
|
880
|
-
return
|
|
880
|
+
return userService.register({
|
|
881
881
|
registrationToken: createdUser.registrationToken,
|
|
882
882
|
userInfo: {
|
|
883
883
|
firstname: firstname || "unset",
|
|
@@ -898,7 +898,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
898
898
|
if (!baseAlias) {
|
|
899
899
|
return baseEmail;
|
|
900
900
|
}
|
|
901
|
-
const alias = baseAlias.replace(
|
|
901
|
+
const alias = baseAlias.replace(/\+/g, "");
|
|
902
902
|
const beforePosition = baseEmail.indexOf("@");
|
|
903
903
|
const origin = baseEmail.substring(0, beforePosition);
|
|
904
904
|
const domain = baseEmail.substring(beforePosition);
|
|
@@ -935,11 +935,9 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
935
935
|
provider: "strapi-plugin-oidc"
|
|
936
936
|
});
|
|
937
937
|
},
|
|
938
|
-
// Sign In Success
|
|
939
938
|
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
940
939
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
941
|
-
const
|
|
942
|
-
const isRememberMe = !!REMEMBER_ME;
|
|
940
|
+
const isRememberMe = !!config2["REMEMBER_ME"];
|
|
943
941
|
const content = `
|
|
944
942
|
<noscript>
|
|
945
943
|
<div class="card">
|
|
@@ -965,7 +963,6 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
965
963
|
<\/script>`;
|
|
966
964
|
return renderHtmlTemplate("Authenticating...", content);
|
|
967
965
|
},
|
|
968
|
-
// Sign In Error
|
|
969
966
|
renderSignUpError(message) {
|
|
970
967
|
const safeMessage = String(message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
971
968
|
const content = `
|
|
@@ -993,8 +990,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
993
990
|
const userId = String(user.id);
|
|
994
991
|
const deviceId = node_crypto.randomUUID();
|
|
995
992
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
996
|
-
const
|
|
997
|
-
const rememberMe = !!REMEMBER_ME;
|
|
993
|
+
const rememberMe = !!config2["REMEMBER_ME"];
|
|
998
994
|
const { token: refreshToken, absoluteExpiresAt } = await sessionManager(
|
|
999
995
|
"admin"
|
|
1000
996
|
).generateRefreshToken(userId, deviceId, {
|
package/dist/server/index.mjs
CHANGED
|
@@ -24,6 +24,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
24
24
|
const authRoutes = [
|
|
25
25
|
`${adminUrl}/login`,
|
|
26
26
|
`${adminUrl}/register`,
|
|
27
|
+
`${adminUrl}/register-admin`,
|
|
27
28
|
`${adminUrl}/forgot-password`,
|
|
28
29
|
`${adminUrl}/reset-password`
|
|
29
30
|
];
|
|
@@ -158,7 +159,6 @@ const config = {
|
|
|
158
159
|
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
159
160
|
OIDC_TOKEN_ENDPOINT: "",
|
|
160
161
|
OIDC_USER_INFO_ENDPOINT: "",
|
|
161
|
-
OIDC_USER_INFO_ENDPOINT_WITH_AUTH_HEADER: false,
|
|
162
162
|
OIDC_GRANT_TYPE: "authorization_code",
|
|
163
163
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
164
164
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
@@ -217,45 +217,43 @@ function clearAuthCookies(strapi2, ctx) {
|
|
|
217
217
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
218
218
|
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
219
219
|
}
|
|
220
|
+
const REQUIRED_CONFIG_KEYS = [
|
|
221
|
+
"OIDC_CLIENT_ID",
|
|
222
|
+
"OIDC_CLIENT_SECRET",
|
|
223
|
+
"OIDC_REDIRECT_URI",
|
|
224
|
+
"OIDC_SCOPES",
|
|
225
|
+
"OIDC_TOKEN_ENDPOINT",
|
|
226
|
+
"OIDC_USER_INFO_ENDPOINT",
|
|
227
|
+
"OIDC_GRANT_TYPE",
|
|
228
|
+
"OIDC_FAMILY_NAME_FIELD",
|
|
229
|
+
"OIDC_GIVEN_NAME_FIELD",
|
|
230
|
+
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
231
|
+
];
|
|
220
232
|
function configValidation() {
|
|
221
233
|
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])) {
|
|
234
|
+
if (REQUIRED_CONFIG_KEYS.every((key) => config2[key])) {
|
|
235
235
|
return config2;
|
|
236
236
|
}
|
|
237
|
-
throw new Error(
|
|
237
|
+
throw new Error(
|
|
238
|
+
`The following configuration keys are required: ${REQUIRED_CONFIG_KEYS.join(", ")}`
|
|
239
|
+
);
|
|
238
240
|
}
|
|
239
241
|
async function oidcSignIn(ctx) {
|
|
240
|
-
let { state } = ctx.query;
|
|
241
242
|
const { OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPES, OIDC_AUTHORIZATION_ENDPOINT } = configValidation();
|
|
242
243
|
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
244
|
+
const state = randomBytes(32).toString("base64url");
|
|
245
|
+
const nonce = randomBytes(32).toString("base64url");
|
|
246
246
|
const isProduction = strapi.config.get("environment") === "production";
|
|
247
|
-
|
|
247
|
+
const cookieOptions = {
|
|
248
248
|
httpOnly: true,
|
|
249
249
|
maxAge: 6e5,
|
|
250
|
+
// 10 minutes
|
|
250
251
|
secure: isProduction && ctx.request.secure,
|
|
251
252
|
sameSite: "lax"
|
|
252
|
-
}
|
|
253
|
-
ctx.cookies.set("
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
secure: isProduction && ctx.request.secure,
|
|
257
|
-
sameSite: "lax"
|
|
258
|
-
});
|
|
253
|
+
};
|
|
254
|
+
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
255
|
+
ctx.cookies.set("oidc_state", state, cookieOptions);
|
|
256
|
+
ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
|
|
259
257
|
const params = new URLSearchParams();
|
|
260
258
|
params.append("response_type", "code");
|
|
261
259
|
params.append("client_id", OIDC_CLIENT_ID);
|
|
@@ -264,11 +262,12 @@ async function oidcSignIn(ctx) {
|
|
|
264
262
|
params.append("code_challenge", codeChallenge);
|
|
265
263
|
params.append("code_challenge_method", "S256");
|
|
266
264
|
params.append("state", state);
|
|
265
|
+
params.append("nonce", nonce);
|
|
267
266
|
const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
|
|
268
267
|
ctx.set("Location", authorizationUrl);
|
|
269
268
|
return ctx.send({}, 302);
|
|
270
269
|
}
|
|
271
|
-
async function exchangeTokenAndFetchUserInfo(config2, params) {
|
|
270
|
+
async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
272
271
|
const response = await fetch(config2.OIDC_TOKEN_ENDPOINT, {
|
|
273
272
|
method: "POST",
|
|
274
273
|
body: params,
|
|
@@ -277,31 +276,28 @@ async function exchangeTokenAndFetchUserInfo(config2, params) {
|
|
|
277
276
|
}
|
|
278
277
|
});
|
|
279
278
|
if (!response.ok) {
|
|
280
|
-
|
|
281
|
-
throw new Error(
|
|
282
|
-
`Failed to exchange token: ${response.status} ${response.statusText} - ${errText}`
|
|
283
|
-
);
|
|
279
|
+
throw new Error("Token exchange failed");
|
|
284
280
|
}
|
|
285
281
|
const tokenData = await response.json();
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
282
|
+
if (tokenData.id_token) {
|
|
283
|
+
try {
|
|
284
|
+
const payloadB64 = tokenData.id_token.split(".")[1];
|
|
285
|
+
const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
286
|
+
if (idTokenPayload.nonce !== expectedNonce) {
|
|
287
|
+
throw new Error("Nonce mismatch");
|
|
288
|
+
}
|
|
289
|
+
} catch (e) {
|
|
290
|
+
if (e.message === "Nonce mismatch") throw e;
|
|
291
|
+
throw new Error("Failed to parse ID token");
|
|
292
|
+
}
|
|
293
293
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
headers: userInfoEndpointHeaders
|
|
294
|
+
const userResponse = await fetch(config2.OIDC_USER_INFO_ENDPOINT, {
|
|
295
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
297
296
|
});
|
|
298
297
|
if (!userResponse.ok) {
|
|
299
|
-
|
|
300
|
-
throw new Error(
|
|
301
|
-
`Failed to fetch user info: ${userResponse.status} ${userResponse.statusText} - ${errText}`
|
|
302
|
-
);
|
|
298
|
+
throw new Error("Failed to fetch user info");
|
|
303
299
|
}
|
|
304
|
-
return
|
|
300
|
+
return userResponse.json();
|
|
305
301
|
}
|
|
306
302
|
async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
|
|
307
303
|
let roles2 = [];
|
|
@@ -325,22 +321,16 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
|
|
|
325
321
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
326
322
|
const email = String(userResponseData.email).toLowerCase();
|
|
327
323
|
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
|
-
}
|
|
324
|
+
const activateUser = await userService.findOneByEmail(email) ?? await registerNewUser(
|
|
325
|
+
userService,
|
|
326
|
+
oauthService2,
|
|
327
|
+
roleService2,
|
|
328
|
+
email,
|
|
329
|
+
userResponseData,
|
|
330
|
+
whitelistUser,
|
|
331
|
+
config2,
|
|
332
|
+
ctx
|
|
333
|
+
);
|
|
344
334
|
const jwtToken = await oauthService2.generateToken(activateUser, ctx);
|
|
345
335
|
oauthService2.triggerSignInSuccess(activateUser);
|
|
346
336
|
return { activateUser, jwtToken };
|
|
@@ -356,8 +346,10 @@ async function oidcSignInCallback(ctx) {
|
|
|
356
346
|
}
|
|
357
347
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
358
348
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
349
|
+
const oidcNonce = ctx.cookies.get("oidc_nonce");
|
|
359
350
|
ctx.cookies.set("oidc_state", null);
|
|
360
351
|
ctx.cookies.set("oidc_code_verifier", null);
|
|
352
|
+
ctx.cookies.set("oidc_nonce", null);
|
|
361
353
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
362
354
|
return ctx.send(oauthService2.renderSignUpError("Invalid state"));
|
|
363
355
|
}
|
|
@@ -369,7 +361,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
369
361
|
params.append("grant_type", config2.OIDC_GRANT_TYPE);
|
|
370
362
|
params.append("code_verifier", codeVerifier);
|
|
371
363
|
try {
|
|
372
|
-
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params);
|
|
364
|
+
const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce);
|
|
373
365
|
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
374
366
|
userService,
|
|
375
367
|
oauthService2,
|
|
@@ -385,7 +377,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
385
377
|
ctx.send(html);
|
|
386
378
|
} catch (e) {
|
|
387
379
|
console.error("ERROR CAUGHT IN OIDC SIGNIN:", e);
|
|
388
|
-
ctx.send(oauthService2.renderSignUpError(
|
|
380
|
+
ctx.send(oauthService2.renderSignUpError("Authentication failed. Please try again."));
|
|
389
381
|
}
|
|
390
382
|
}
|
|
391
383
|
async function logout(ctx) {
|
|
@@ -432,8 +424,11 @@ const role = {
|
|
|
432
424
|
find,
|
|
433
425
|
update
|
|
434
426
|
};
|
|
427
|
+
function getWhitelistService() {
|
|
428
|
+
return strapi.plugin("strapi-plugin-oidc").service("whitelist");
|
|
429
|
+
}
|
|
435
430
|
async function info(ctx) {
|
|
436
|
-
const whitelistService2 =
|
|
431
|
+
const whitelistService2 = getWhitelistService();
|
|
437
432
|
const settings = await whitelistService2.getSettings();
|
|
438
433
|
const whitelistUsers = await whitelistService2.getUsers();
|
|
439
434
|
ctx.body = {
|
|
@@ -444,8 +439,9 @@ async function info(ctx) {
|
|
|
444
439
|
};
|
|
445
440
|
}
|
|
446
441
|
async function updateSettings(ctx) {
|
|
447
|
-
|
|
448
|
-
|
|
442
|
+
const { useWhitelist } = ctx.request.body;
|
|
443
|
+
let { enforceOIDC } = ctx.request.body;
|
|
444
|
+
const whitelistService2 = getWhitelistService();
|
|
449
445
|
if (useWhitelist && enforceOIDC) {
|
|
450
446
|
const users = await whitelistService2.getUsers();
|
|
451
447
|
if (users.length === 0) {
|
|
@@ -456,7 +452,7 @@ async function updateSettings(ctx) {
|
|
|
456
452
|
ctx.body = { useWhitelist, enforceOIDC };
|
|
457
453
|
}
|
|
458
454
|
async function publicSettings(ctx) {
|
|
459
|
-
const whitelistService2 =
|
|
455
|
+
const whitelistService2 = getWhitelistService();
|
|
460
456
|
const settings = await whitelistService2.getSettings();
|
|
461
457
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
462
458
|
ctx.body = {
|
|
@@ -476,10 +472,11 @@ async function register(ctx) {
|
|
|
476
472
|
where: { email: { $in: emailList } },
|
|
477
473
|
populate: ["roles"]
|
|
478
474
|
});
|
|
479
|
-
const
|
|
475
|
+
const existingUsersByEmail = new Map(existingUsers.map((u) => [u.email, u]));
|
|
476
|
+
const whitelistService2 = getWhitelistService();
|
|
480
477
|
let matchedExistingUsersCount = 0;
|
|
481
478
|
for (const singleEmail of emailList) {
|
|
482
|
-
const existingUser =
|
|
479
|
+
const existingUser = existingUsersByEmail.get(singleEmail);
|
|
483
480
|
let finalRoles = roles2;
|
|
484
481
|
if (existingUser?.roles) {
|
|
485
482
|
finalRoles = existingUser.roles.map((r) => String(r.id));
|
|
@@ -496,12 +493,12 @@ async function register(ctx) {
|
|
|
496
493
|
}
|
|
497
494
|
async function removeEmail(ctx) {
|
|
498
495
|
const { id } = ctx.params;
|
|
499
|
-
const whitelistService2 =
|
|
496
|
+
const whitelistService2 = getWhitelistService();
|
|
500
497
|
await whitelistService2.removeUser(id);
|
|
501
498
|
ctx.body = {};
|
|
502
499
|
}
|
|
503
500
|
async function deleteAll(ctx) {
|
|
504
|
-
await strapi.
|
|
501
|
+
await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
|
|
505
502
|
ctx.body = {};
|
|
506
503
|
}
|
|
507
504
|
async function importUsers(ctx) {
|
|
@@ -529,7 +526,7 @@ async function importUsers(ctx) {
|
|
|
529
526
|
populate: ["roles"]
|
|
530
527
|
});
|
|
531
528
|
const strapiUserMap = new Map(strapiUsers.map((u) => [u.email, u]));
|
|
532
|
-
const whitelistService2 =
|
|
529
|
+
const whitelistService2 = getWhitelistService();
|
|
533
530
|
const existing = await whitelistService2.getUsers();
|
|
534
531
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
535
532
|
let importedCount = 0;
|
|
@@ -543,9 +540,9 @@ async function importUsers(ctx) {
|
|
|
543
540
|
ctx.body = { importedCount };
|
|
544
541
|
}
|
|
545
542
|
async function syncUsers(ctx) {
|
|
546
|
-
|
|
547
|
-
users =
|
|
548
|
-
const whitelistService2 =
|
|
543
|
+
const { users: rawUsers } = ctx.request.body;
|
|
544
|
+
const users = rawUsers.map((u) => ({ ...u, email: String(u.email).toLowerCase() }));
|
|
545
|
+
const whitelistService2 = getWhitelistService();
|
|
549
546
|
const currentUsers = await whitelistService2.getUsers();
|
|
550
547
|
let matchedExistingUsersCount = 0;
|
|
551
548
|
const emailsToSync = users.map((u) => u.email);
|
|
@@ -553,26 +550,29 @@ async function syncUsers(ctx) {
|
|
|
553
550
|
where: { email: { $in: emailsToSync } },
|
|
554
551
|
populate: ["roles"]
|
|
555
552
|
});
|
|
553
|
+
const syncEmailSet = new Set(emailsToSync);
|
|
554
|
+
const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
|
|
555
|
+
const strapiUsersByEmail = new Map(existingStrapiUsers.map((u) => [u.email, u]));
|
|
556
556
|
for (const currUser of currentUsers) {
|
|
557
|
-
if (!
|
|
557
|
+
if (!syncEmailSet.has(currUser.email)) {
|
|
558
558
|
await whitelistService2.removeUser(currUser.id);
|
|
559
559
|
}
|
|
560
560
|
}
|
|
561
561
|
for (const user of users) {
|
|
562
|
-
const
|
|
562
|
+
const currUser = currentUsersByEmail.get(user.email);
|
|
563
563
|
let finalRoles = user.roles;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
564
|
+
if (!currUser) {
|
|
565
|
+
const existingStrapiUser = strapiUsersByEmail.get(user.email);
|
|
566
|
+
if (existingStrapiUser?.roles) {
|
|
567
|
+
finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
|
|
568
|
+
matchedExistingUsersCount++;
|
|
569
|
+
}
|
|
570
|
+
await whitelistService2.registerUser(user.email, finalRoles);
|
|
571
|
+
} else {
|
|
570
572
|
await strapi.query("plugin::strapi-plugin-oidc.whitelists").update({
|
|
571
573
|
where: { id: currUser.id },
|
|
572
574
|
data: { roles: finalRoles }
|
|
573
575
|
});
|
|
574
|
-
} else {
|
|
575
|
-
await whitelistService2.registerUser(user.email, finalRoles);
|
|
576
576
|
}
|
|
577
577
|
}
|
|
578
578
|
ctx.body = { matchedExistingUsersCount };
|
|
@@ -871,7 +871,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
871
871
|
roles: roles2,
|
|
872
872
|
preferedLanguage: locale
|
|
873
873
|
});
|
|
874
|
-
return
|
|
874
|
+
return userService.register({
|
|
875
875
|
registrationToken: createdUser.registrationToken,
|
|
876
876
|
userInfo: {
|
|
877
877
|
firstname: firstname || "unset",
|
|
@@ -892,7 +892,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
892
892
|
if (!baseAlias) {
|
|
893
893
|
return baseEmail;
|
|
894
894
|
}
|
|
895
|
-
const alias = baseAlias.replace(
|
|
895
|
+
const alias = baseAlias.replace(/\+/g, "");
|
|
896
896
|
const beforePosition = baseEmail.indexOf("@");
|
|
897
897
|
const origin = baseEmail.substring(0, beforePosition);
|
|
898
898
|
const domain = baseEmail.substring(beforePosition);
|
|
@@ -929,11 +929,9 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
929
929
|
provider: "strapi-plugin-oidc"
|
|
930
930
|
});
|
|
931
931
|
},
|
|
932
|
-
// Sign In Success
|
|
933
932
|
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
934
933
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
935
|
-
const
|
|
936
|
-
const isRememberMe = !!REMEMBER_ME;
|
|
934
|
+
const isRememberMe = !!config2["REMEMBER_ME"];
|
|
937
935
|
const content = `
|
|
938
936
|
<noscript>
|
|
939
937
|
<div class="card">
|
|
@@ -959,7 +957,6 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
959
957
|
<\/script>`;
|
|
960
958
|
return renderHtmlTemplate("Authenticating...", content);
|
|
961
959
|
},
|
|
962
|
-
// Sign In Error
|
|
963
960
|
renderSignUpError(message) {
|
|
964
961
|
const safeMessage = String(message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
965
962
|
const content = `
|
|
@@ -987,8 +984,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
987
984
|
const userId = String(user.id);
|
|
988
985
|
const deviceId = randomUUID();
|
|
989
986
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
990
|
-
const
|
|
991
|
-
const rememberMe = !!REMEMBER_ME;
|
|
987
|
+
const rememberMe = !!config2["REMEMBER_ME"];
|
|
992
988
|
const { token: refreshToken, absoluteExpiresAt } = await sessionManager(
|
|
993
989
|
"admin"
|
|
994
990
|
).generateRefreshToken(userId, deviceId, {
|
package/package.json
CHANGED