strapi-plugin-oidc 1.4.0 → 1.4.1

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