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 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-CZDixCh4.js");
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
- const LocalizedDate = ({ date }) => {
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 onSaveEmail = react.useCallback(async () => {
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
- await onSave(emailText, selectedRoles);
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 getRoleNames = (roleIds) => roleIds.map((roleId) => {
224
- const r = roles.find((ro) => String(ro.id) === String(roleId));
225
- return r ? r.name : roleId;
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(JSON.parse(JSON.stringify(response.data)));
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(JSON.parse(JSON.stringify(response.data.whitelistUsers)));
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 = async (email, selectedRoles) => {
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 = async (email) => {
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(JSON.parse(JSON.stringify(refreshed.data.whitelistUsers)));
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 = "whitelist.json";
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(JSON.parse(JSON.stringify(oidcRoles)));
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(JSON.parse(JSON.stringify(getResponse.data.whitelistUsers)));
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-8hB6LKml.mjs";
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
- const LocalizedDate = ({ date }) => {
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 onSaveEmail = useCallback(async () => {
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
- await onSave(emailText, selectedRoles);
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 getRoleNames = (roleIds) => roleIds.map((roleId) => {
220
- const r = roles.find((ro) => String(ro.id) === String(roleId));
221
- return r ? r.name : roleId;
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(JSON.parse(JSON.stringify(response.data)));
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(JSON.parse(JSON.stringify(response.data.whitelistUsers)));
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 = async (email, selectedRoles) => {
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 = async (email) => {
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(JSON.parse(JSON.stringify(refreshed.data.whitelistUsers)));
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 = "whitelist.json";
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(JSON.parse(JSON.stringify(oidcRoles)));
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(JSON.parse(JSON.stringify(getResponse.data.whitelistUsers)));
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-QZkv75Xp.js"));
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-BTTGSnuQ.mjs");
125
+ return await import("./index-CY4s-vtv.mjs");
126
126
  },
127
127
  permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
128
128
  }
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const index = require("./index-CZDixCh4.js");
3
+ const index = require("./index-RMgj1w0B.js");
4
4
  exports.default = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "./index-8hB6LKml.mjs";
1
+ import { i } from "./index-ZRaWWFUL.mjs";
2
2
  export {
3
3
  i as default
4
4
  };
@@ -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
- const requiredKeys = [
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(`The following configuration keys are required: ${requiredKeys.join(", ")}`);
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
- if (!state) {
250
- state = node_crypto.randomBytes(32).toString("base64url");
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
- ctx.cookies.set("oidc_code_verifier", codeVerifier, {
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("oidc_state", state, {
260
- httpOnly: true,
261
- maxAge: 6e5,
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
- const errText = await response.text();
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
- let userInfoEndpointHeaders = {};
293
- let userInfoEndpointParameters = `?access_token=${tokenData.access_token}`;
294
- if (config2.OIDC_USER_INFO_ENDPOINT_WITH_AUTH_HEADER) {
295
- userInfoEndpointHeaders = {
296
- Authorization: `Bearer ${tokenData.access_token}`
297
- };
298
- userInfoEndpointParameters = "";
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 userInfoEndpoint = `${config2.OIDC_USER_INFO_ENDPOINT}${userInfoEndpointParameters}`;
301
- const userResponse = await fetch(userInfoEndpoint, {
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
- const errText = await userResponse.text();
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 await userResponse.json();
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 dbUser = await userService.findOneByEmail(email);
335
- let activateUser;
336
- if (dbUser) {
337
- activateUser = dbUser;
338
- } else {
339
- activateUser = await registerNewUser(
340
- userService,
341
- oauthService2,
342
- roleService2,
343
- email,
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(e.message));
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 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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
- let { useWhitelist, enforceOIDC } = ctx.request.body;
454
- const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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 whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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 = existingUsers.find((u) => u.email === singleEmail);
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 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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.db.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
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 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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
- let { users } = ctx.request.body;
553
- users = users.map((u) => ({ ...u, email: String(u.email).toLowerCase() }));
554
- const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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 (!users.find((u) => u.email === currUser.email)) {
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 existingStrapiUser = existingStrapiUsers.find((u) => u.email === user.email);
568
+ const currUser = currentUsersByEmail.get(user.email);
569
569
  let finalRoles = user.roles;
570
- const currUser = currentUsers.find((u) => u.email === user.email);
571
- if (!currUser && existingStrapiUser?.roles) {
572
- finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
573
- matchedExistingUsersCount++;
574
- }
575
- if (currUser) {
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 await userService.register({
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("/+/g", "");
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 REMEMBER_ME = config2["REMEMBER_ME"];
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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 REMEMBER_ME = config2["REMEMBER_ME"];
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, {
@@ -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
- const requiredKeys = [
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(`The following configuration keys are required: ${requiredKeys.join(", ")}`);
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
- if (!state) {
244
- state = randomBytes(32).toString("base64url");
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
- ctx.cookies.set("oidc_code_verifier", codeVerifier, {
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("oidc_state", state, {
254
- httpOnly: true,
255
- maxAge: 6e5,
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
- const errText = await response.text();
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
- let userInfoEndpointHeaders = {};
287
- let userInfoEndpointParameters = `?access_token=${tokenData.access_token}`;
288
- if (config2.OIDC_USER_INFO_ENDPOINT_WITH_AUTH_HEADER) {
289
- userInfoEndpointHeaders = {
290
- Authorization: `Bearer ${tokenData.access_token}`
291
- };
292
- userInfoEndpointParameters = "";
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 userInfoEndpoint = `${config2.OIDC_USER_INFO_ENDPOINT}${userInfoEndpointParameters}`;
295
- const userResponse = await fetch(userInfoEndpoint, {
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
- const errText = await userResponse.text();
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 await userResponse.json();
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 dbUser = await userService.findOneByEmail(email);
329
- let activateUser;
330
- if (dbUser) {
331
- activateUser = dbUser;
332
- } else {
333
- activateUser = await registerNewUser(
334
- userService,
335
- oauthService2,
336
- roleService2,
337
- email,
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(e.message));
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 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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
- let { useWhitelist, enforceOIDC } = ctx.request.body;
448
- const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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 whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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 = existingUsers.find((u) => u.email === singleEmail);
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 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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.db.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
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 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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
- let { users } = ctx.request.body;
547
- users = users.map((u) => ({ ...u, email: String(u.email).toLowerCase() }));
548
- const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
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 (!users.find((u) => u.email === currUser.email)) {
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 existingStrapiUser = existingStrapiUsers.find((u) => u.email === user.email);
562
+ const currUser = currentUsersByEmail.get(user.email);
563
563
  let finalRoles = user.roles;
564
- const currUser = currentUsers.find((u) => u.email === user.email);
565
- if (!currUser && existingStrapiUser?.roles) {
566
- finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
567
- matchedExistingUsersCount++;
568
- }
569
- if (currUser) {
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 await userService.register({
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("/+/g", "");
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 REMEMBER_ME = config2["REMEMBER_ME"];
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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 REMEMBER_ME = config2["REMEMBER_ME"];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-oidc",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "A Strapi plugin that provides OpenID Connect (OIDC) authentication functionality for the Strapi Admin Panel.",
5
5
  "strapi": {
6
6
  "displayName": "OIDC Plugin",