strapi-plugin-oidc 1.8.0 → 1.8.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 +6 -12
- package/dist/admin/{index-B-K4X_N9.mjs → index-Bb9-aYb4.mjs} +53 -11
- package/dist/admin/{index-CgG_mHzZ.js → index-Bmg4eTYb.js} +113 -88
- package/dist/admin/{index-8YTLPV3h.mjs → index-BqWd-Iiq.mjs} +72 -47
- package/dist/admin/{index-BSgVStns.js → index-Dk6TYtio.js} +57 -13
- package/dist/admin/index.js +3 -1
- package/dist/admin/index.mjs +3 -1
- package/dist/server/index.js +86 -51
- package/dist/server/index.mjs +86 -51
- package/package.json +1 -1
|
@@ -2,10 +2,10 @@ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useBlocker, Routes, Route } from "react-router-dom";
|
|
3
3
|
import { useNotification, useFetchClient, Page, Layouts } from "@strapi/strapi/admin";
|
|
4
4
|
import { useState, useRef, useId, useEffect, useCallback, useReducer, useMemo, memo } from "react";
|
|
5
|
-
import { Typography, Flex, Box, MultiSelect, MultiSelectOption, Button, Dialog, Table, Pagination, PreviousLink, NextLink, PageLink, Field, Divider, Thead, Tr, Th, Tbody, Td, IconButton, Tooltip, Alert } from "@strapi/design-system";
|
|
5
|
+
import { Typography, Flex, Box, MultiSelect, MultiSelectOption, Button, Dialog, Table, Pagination, PreviousLink, NextLink, PageLink, Field, Divider, Thead, Tr, Th, Tbody, Td, IconButton, Loader, Tooltip, Alert } from "@strapi/design-system";
|
|
6
6
|
import { Cross, WarningCircle, Plus, Download, Upload, Trash, Calendar, Mail, Information } from "@strapi/icons";
|
|
7
7
|
import { useIntl } from "react-intl";
|
|
8
|
-
import { g as getTrad } from "./index-
|
|
8
|
+
import { g as getTrad } from "./index-Bb9-aYb4.mjs";
|
|
9
9
|
import styled from "styled-components";
|
|
10
10
|
import { Filter, ClipboardList, Server } from "lucide-react";
|
|
11
11
|
function Role({ oidcRoles, roles, onChangeRole }) {
|
|
@@ -1014,7 +1014,7 @@ function Whitelist({
|
|
|
1014
1014
|
/* @__PURE__ */ jsx(Th, { children: formatMessage(getTrad("whitelist.table.created")) }),
|
|
1015
1015
|
/* @__PURE__ */ jsx(Th, { style: { paddingRight: 0 }, children: " " })
|
|
1016
1016
|
] }) }),
|
|
1017
|
-
/* @__PURE__ */ jsx(Tbody, { children: users.length === 0 ? /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: 4, children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center",
|
|
1017
|
+
/* @__PURE__ */ jsx(Tbody, { children: users.length === 0 ? /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: 4, children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", style: { minHeight: "80px" }, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: formatMessage(getTrad("whitelist.table.empty")) }) }) }) }) : paginatedUsers.map((user, index) => /* @__PURE__ */ jsxs(Tr, { children: [
|
|
1018
1018
|
/* @__PURE__ */ jsx(Td, { children: index + 1 + (page - 1) * PAGE_SIZE$1 }),
|
|
1019
1019
|
/* @__PURE__ */ jsx(Td, { children: user.email }),
|
|
1020
1020
|
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(LocalizedDate, { date: user.createdAt, options: { second: "2-digit" } }) }),
|
|
@@ -3618,6 +3618,7 @@ const AUDIT_ACTIONS = [
|
|
|
3618
3618
|
"user_created"
|
|
3619
3619
|
];
|
|
3620
3620
|
const PAGE_SIZE = 10;
|
|
3621
|
+
const MIN_SPINNER_MS = 400;
|
|
3621
3622
|
const DETAILS_TEXT_STYLE = {
|
|
3622
3623
|
display: "block",
|
|
3623
3624
|
overflow: "hidden",
|
|
@@ -3667,28 +3668,35 @@ function AuditLog({ title } = {}) {
|
|
|
3667
3668
|
pageCount: 1
|
|
3668
3669
|
});
|
|
3669
3670
|
const [page, setPage] = useState(1);
|
|
3670
|
-
const [loading, setLoading] = useState(
|
|
3671
|
+
const [loading, setLoading] = useState(true);
|
|
3671
3672
|
const [filters, setFilters] = useState({});
|
|
3673
|
+
const fetchGenRef = useRef(0);
|
|
3672
3674
|
const debouncedFilters = useDebounced(filters);
|
|
3673
3675
|
const fetchLogs = useCallback(
|
|
3674
3676
|
async (p, f) => {
|
|
3677
|
+
const gen = ++fetchGenRef.current;
|
|
3675
3678
|
setLoading(true);
|
|
3679
|
+
const startTime = Date.now();
|
|
3680
|
+
let newRecords = [];
|
|
3681
|
+
let newPagination = { page: p, pageSize: PAGE_SIZE, total: 0, pageCount: 1 };
|
|
3676
3682
|
try {
|
|
3677
|
-
const queryString = buildQueryString({
|
|
3678
|
-
filters: f,
|
|
3679
|
-
page: p,
|
|
3680
|
-
pageSize: PAGE_SIZE
|
|
3681
|
-
});
|
|
3683
|
+
const queryString = buildQueryString({ filters: f, page: p, pageSize: PAGE_SIZE });
|
|
3682
3684
|
const response = await get2(`/strapi-plugin-oidc/audit-logs?${queryString}`);
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3685
|
+
newRecords = response.data.results ?? [];
|
|
3686
|
+
newPagination = response.data.pagination ?? {
|
|
3687
|
+
page: p,
|
|
3688
|
+
pageSize: PAGE_SIZE,
|
|
3689
|
+
total: 0,
|
|
3690
|
+
pageCount: 1
|
|
3691
|
+
};
|
|
3687
3692
|
} catch {
|
|
3688
|
-
setRecords([]);
|
|
3689
|
-
} finally {
|
|
3690
|
-
setLoading(false);
|
|
3691
3693
|
}
|
|
3694
|
+
const remaining = MIN_SPINNER_MS - (Date.now() - startTime);
|
|
3695
|
+
if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));
|
|
3696
|
+
if (gen !== fetchGenRef.current) return;
|
|
3697
|
+
setRecords(newRecords);
|
|
3698
|
+
setPagination(newPagination);
|
|
3699
|
+
setLoading(false);
|
|
3692
3700
|
},
|
|
3693
3701
|
[get2]
|
|
3694
3702
|
);
|
|
@@ -3865,37 +3873,54 @@ function AuditLog({ title } = {}) {
|
|
|
3865
3873
|
]
|
|
3866
3874
|
}
|
|
3867
3875
|
),
|
|
3868
|
-
/* @__PURE__ */ jsxs(
|
|
3869
|
-
/* @__PURE__ */
|
|
3870
|
-
/* @__PURE__ */ jsx(
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3876
|
+
/* @__PURE__ */ jsxs("div", { style: { position: "relative", width: "100%" }, children: [
|
|
3877
|
+
/* @__PURE__ */ jsxs(CustomTable, { colCount: 5, rowCount: records.length, children: [
|
|
3878
|
+
/* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
|
|
3879
|
+
/* @__PURE__ */ jsx(Th, { children: formatMessage(getTrad("auditlog.table.timestamp")) }),
|
|
3880
|
+
/* @__PURE__ */ jsx(Th, { children: formatMessage(getTrad("auditlog.table.action")) }),
|
|
3881
|
+
/* @__PURE__ */ jsx(Th, { children: formatMessage(getTrad("auditlog.table.email")) }),
|
|
3882
|
+
/* @__PURE__ */ jsx(Th, { children: formatMessage(getTrad("auditlog.table.ip")) }),
|
|
3883
|
+
/* @__PURE__ */ jsx(Th, { children: formatMessage(getTrad("auditlog.table.details")) })
|
|
3884
|
+
] }) }),
|
|
3885
|
+
/* @__PURE__ */ jsxs(Tbody, { children: [
|
|
3886
|
+
records.length === 0 && /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: 5, children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", style: { minHeight: "80px" }, children: loading ? /* @__PURE__ */ jsx(Loader, { small: true }) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: hasActiveFilters ? formatMessage(getTrad("auditlog.filters.empty")) : formatMessage(getTrad("auditlog.table.empty")) }) }) }) }),
|
|
3887
|
+
records.map((record) => /* @__PURE__ */ jsxs(
|
|
3888
|
+
Tr,
|
|
3889
|
+
{
|
|
3890
|
+
style: { opacity: loading ? 0.4 : 1, transition: "opacity 0.15s" },
|
|
3891
|
+
children: [
|
|
3892
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "omega", children: /* @__PURE__ */ jsx(LocalizedDate, { date: record.createdAt, options: { second: "2-digit" } }) }) }),
|
|
3893
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
3894
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", children: record.action }),
|
|
3895
|
+
/* @__PURE__ */ jsx(Tooltip, { label: formatMessage(getTrad(`auditlog.action.${record.action}`)), children: /* @__PURE__ */ jsx(
|
|
3896
|
+
Information,
|
|
3897
|
+
{
|
|
3898
|
+
"aria-hidden": true,
|
|
3899
|
+
style: { cursor: "help" },
|
|
3900
|
+
width: "1.4rem",
|
|
3901
|
+
height: "1.4rem",
|
|
3902
|
+
fill: "primary600"
|
|
3903
|
+
}
|
|
3904
|
+
) })
|
|
3905
|
+
] }) }),
|
|
3906
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "omega", children: record.email ?? "—" }) }),
|
|
3907
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "omega", children: record.ip ?? "—" }) }),
|
|
3908
|
+
/* @__PURE__ */ jsx(Td, { style: { maxWidth: "200px" }, children: record.details ? /* @__PURE__ */ jsx(Tooltip, { label: record.details, side: "top", children: /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", style: DETAILS_TEXT_STYLE, children: record.details }) }) : /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: "—" }) })
|
|
3909
|
+
]
|
|
3910
|
+
},
|
|
3911
|
+
record.id
|
|
3912
|
+
))
|
|
3913
|
+
] })
|
|
3914
|
+
] }),
|
|
3915
|
+
loading && records.length > 0 && /* @__PURE__ */ jsx(
|
|
3916
|
+
Flex,
|
|
3917
|
+
{
|
|
3918
|
+
justifyContent: "center",
|
|
3919
|
+
alignItems: "center",
|
|
3920
|
+
style: { position: "absolute", inset: 0, pointerEvents: "none" },
|
|
3921
|
+
children: /* @__PURE__ */ jsx(Loader, { small: true })
|
|
3922
|
+
}
|
|
3923
|
+
)
|
|
3899
3924
|
] }),
|
|
3900
3925
|
/* @__PURE__ */ jsx(
|
|
3901
3926
|
TablePagination,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const
|
|
2
|
+
const React = require("react");
|
|
3
|
+
const client = require("react-dom/client");
|
|
4
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
5
|
+
const designSystem = require("@strapi/design-system");
|
|
6
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
7
|
+
const React__default = /* @__PURE__ */ _interopDefault(React);
|
|
3
8
|
const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
|
|
4
9
|
const v = glob[path];
|
|
5
10
|
if (v) {
|
|
@@ -24,15 +29,51 @@ const pluginPkg = {
|
|
|
24
29
|
};
|
|
25
30
|
const pluginId = pluginPkg.name.replace(/^@strapi\/plugin-/i, "");
|
|
26
31
|
function Initializer({ setPlugin }) {
|
|
27
|
-
const ref =
|
|
32
|
+
const ref = React.useRef();
|
|
28
33
|
ref.current = setPlugin;
|
|
29
|
-
|
|
34
|
+
React.useEffect(() => {
|
|
30
35
|
if (ref.current) {
|
|
31
36
|
ref.current(pluginId);
|
|
32
37
|
}
|
|
33
38
|
}, []);
|
|
34
39
|
return null;
|
|
35
40
|
}
|
|
41
|
+
const LOGOUT_EVENT = "strapi-oidc:logout";
|
|
42
|
+
function Overlay({ bg }) {
|
|
43
|
+
const [active, setActive] = React.useState(false);
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
const handler = () => setActive(true);
|
|
46
|
+
window.addEventListener(LOGOUT_EVENT, handler);
|
|
47
|
+
return () => window.removeEventListener(LOGOUT_EVENT, handler);
|
|
48
|
+
}, []);
|
|
49
|
+
if (!active) return null;
|
|
50
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
51
|
+
"div",
|
|
52
|
+
{
|
|
53
|
+
style: {
|
|
54
|
+
position: "fixed",
|
|
55
|
+
inset: 0,
|
|
56
|
+
zIndex: 1e4,
|
|
57
|
+
display: "flex",
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
justifyContent: "center",
|
|
60
|
+
background: bg,
|
|
61
|
+
backdropFilter: "blur(2px)"
|
|
62
|
+
},
|
|
63
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, {})
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function resolveTheme() {
|
|
68
|
+
const stored = window.localStorage.getItem("STRAPI_THEME") ?? "system";
|
|
69
|
+
const isDark = stored === "dark" || stored === "system" && (window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false);
|
|
70
|
+
return isDark ? designSystem.darkTheme : designSystem.lightTheme;
|
|
71
|
+
}
|
|
72
|
+
function LogoutOverlay() {
|
|
73
|
+
const theme = resolveTheme();
|
|
74
|
+
const bg = theme === designSystem.darkTheme ? "rgba(24, 24, 38, 0.85)" : "rgba(255, 255, 255, 0.85)";
|
|
75
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.DesignSystemProvider, { theme, children: /* @__PURE__ */ jsxRuntime.jsx(Overlay, { bg }) });
|
|
76
|
+
}
|
|
36
77
|
const en = {
|
|
37
78
|
"global.plugins.strapi-plugin-oidc": "OIDC Plugin",
|
|
38
79
|
"page.title": "Configure OIDC default role(s) and access controls.",
|
|
@@ -95,7 +136,6 @@ const en = {
|
|
|
95
136
|
"auditlog.table.ip": "IP",
|
|
96
137
|
"auditlog.table.details": "Details",
|
|
97
138
|
"auditlog.table.empty": "No audit log entries",
|
|
98
|
-
"auditlog.loading": "Loading…",
|
|
99
139
|
"auditlog.clear": "Clear Logs",
|
|
100
140
|
"auditlog.clear.title": "Clear All Logs",
|
|
101
141
|
"auditlog.clear.description": "This will permanently delete all {count, plural, one {# audit log entry} other {# audit log entries}}. This action cannot be undone.",
|
|
@@ -168,7 +208,7 @@ const index = {
|
|
|
168
208
|
defaultMessage: "Configuration"
|
|
169
209
|
},
|
|
170
210
|
Component: async () => {
|
|
171
|
-
return await Promise.resolve().then(() => require("./index-
|
|
211
|
+
return await Promise.resolve().then(() => require("./index-Bmg4eTYb.js"));
|
|
172
212
|
},
|
|
173
213
|
permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
|
|
174
214
|
}
|
|
@@ -180,6 +220,9 @@ const index = {
|
|
|
180
220
|
});
|
|
181
221
|
},
|
|
182
222
|
bootstrap() {
|
|
223
|
+
const overlayContainer = document.createElement("div");
|
|
224
|
+
document.body.appendChild(overlayContainer);
|
|
225
|
+
client.createRoot(overlayContainer).render(React__default.default.createElement(LogoutOverlay));
|
|
183
226
|
const defaultButtonText = t("login.sso");
|
|
184
227
|
const isAuthRoute = (path) => /\/auth\/(login|register|forgot-password|reset-password)/.test(path);
|
|
185
228
|
let ssoButtonInjected = false;
|
|
@@ -239,6 +282,7 @@ const index = {
|
|
|
239
282
|
if (!isAuthRoute(window.location.pathname)) return;
|
|
240
283
|
injectSSOButton(buttonText);
|
|
241
284
|
if (enforced) removeEnforcedElements();
|
|
285
|
+
if (ssoButtonInjected && !enforced) loginObserver?.disconnect();
|
|
242
286
|
};
|
|
243
287
|
tick();
|
|
244
288
|
loginObserver = new MutationObserver(tick);
|
|
@@ -259,27 +303,27 @@ const index = {
|
|
|
259
303
|
}
|
|
260
304
|
};
|
|
261
305
|
applySettings();
|
|
306
|
+
if (window.__strapiOidcFetchPatched) return;
|
|
307
|
+
window.__strapiOidcFetchPatched = true;
|
|
262
308
|
const originalFetch = window.fetch;
|
|
263
309
|
window.fetch = async (...args) => {
|
|
264
310
|
const url = typeof args[0] === "string" ? args[0] : args[0].url;
|
|
265
311
|
const isLogout = url?.endsWith("/admin/logout") && args[1]?.method?.toUpperCase() === "POST";
|
|
266
|
-
|
|
267
|
-
|
|
312
|
+
if (isLogout) {
|
|
313
|
+
window.dispatchEvent(new CustomEvent(LOGOUT_EVENT));
|
|
268
314
|
window.localStorage.removeItem("jwtToken");
|
|
269
315
|
window.localStorage.removeItem("isLoggedIn");
|
|
270
316
|
window.sessionStorage.removeItem("jwtToken");
|
|
271
317
|
window.sessionStorage.removeItem("isLoggedIn");
|
|
272
318
|
document.cookie = "jwtToken=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
|
|
273
319
|
document.cookie = "jwtToken=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/admin";
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
document.body.appendChild(form);
|
|
278
|
-
form.submit();
|
|
320
|
+
originalFetch(...args).catch(() => {
|
|
321
|
+
});
|
|
322
|
+
window.location.href = "/strapi-plugin-oidc/logout";
|
|
279
323
|
return new Promise(() => {
|
|
280
324
|
});
|
|
281
325
|
}
|
|
282
|
-
return
|
|
326
|
+
return originalFetch(...args);
|
|
283
327
|
};
|
|
284
328
|
},
|
|
285
329
|
async registerTrads({ locales }) {
|
package/dist/admin/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
|
-
const index = require("./index-
|
|
3
|
+
const index = require("./index-Dk6TYtio.js");
|
|
4
|
+
require("react");
|
|
5
|
+
require("react-dom/client");
|
|
4
6
|
exports.default = index.index;
|
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -44,8 +44,44 @@ const getRoleService = () => strapi.plugin(PLUGIN_NAME).service("role");
|
|
|
44
44
|
const getWhitelistService = () => strapi.plugin(PLUGIN_NAME).service("whitelist");
|
|
45
45
|
const getAuditLogService = () => strapi.plugin(PLUGIN_NAME).service("auditLog");
|
|
46
46
|
const getAdminUserService = () => strapi.service("admin::user");
|
|
47
|
+
const DISCOVERY_TIMEOUT_MS = 5e3;
|
|
48
|
+
const FIELD_MAP = [
|
|
49
|
+
["issuer", "OIDC_ISSUER"],
|
|
50
|
+
["authorization_endpoint", "OIDC_AUTHORIZATION_ENDPOINT"],
|
|
51
|
+
["token_endpoint", "OIDC_TOKEN_ENDPOINT"],
|
|
52
|
+
["userinfo_endpoint", "OIDC_USERINFO_ENDPOINT"],
|
|
53
|
+
["end_session_endpoint", "OIDC_END_SESSION_ENDPOINT"],
|
|
54
|
+
["jwks_uri", "OIDC_JWKS_URI"]
|
|
55
|
+
];
|
|
56
|
+
async function applyDiscovery(strapi2) {
|
|
57
|
+
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
58
|
+
const discoveryUrl = config2.OIDC_DISCOVERY_URL;
|
|
59
|
+
if (!discoveryUrl) return;
|
|
60
|
+
let doc;
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(discoveryUrl, { signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS) });
|
|
63
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
64
|
+
doc = await res.json();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
strapi2.log.error(
|
|
67
|
+
`[strapi-plugin-oidc] Failed to fetch OIDC discovery document from ${discoveryUrl}: ${e instanceof Error ? e.message : String(e)}`
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const updates = {};
|
|
72
|
+
for (const [docField, configKey] of FIELD_MAP) {
|
|
73
|
+
if (doc[docField]) {
|
|
74
|
+
updates[configKey] = doc[docField];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (Object.keys(updates).length > 0) {
|
|
78
|
+
strapi2.config.set("plugin::strapi-plugin-oidc", { ...config2, ...updates });
|
|
79
|
+
strapi2.log.info(`[strapi-plugin-oidc] Discovery applied: ${Object.keys(updates).join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
47
82
|
const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
|
|
48
83
|
async function bootstrap({ strapi: strapi2 }) {
|
|
84
|
+
await applyDiscovery(strapi2);
|
|
49
85
|
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
50
86
|
const tokenRefreshPath = `${adminUrl}/token/refresh`;
|
|
51
87
|
const enforceOidcMiddleware = async (ctx, next) => {
|
|
@@ -157,17 +193,13 @@ function destroy() {
|
|
|
157
193
|
const config = {
|
|
158
194
|
default: {
|
|
159
195
|
REMEMBER_ME: false,
|
|
196
|
+
OIDC_DISCOVERY_URL: "",
|
|
160
197
|
OIDC_REDIRECT_URI: "http://localhost:1337/strapi-plugin-oidc/oidc/callback",
|
|
161
198
|
OIDC_CLIENT_ID: "",
|
|
162
199
|
OIDC_CLIENT_SECRET: "",
|
|
163
200
|
OIDC_SCOPE: "openid profile email",
|
|
164
|
-
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
165
|
-
OIDC_TOKEN_ENDPOINT: "",
|
|
166
|
-
OIDC_USERINFO_ENDPOINT: "",
|
|
167
|
-
OIDC_GRANT_TYPE: "authorization_code",
|
|
168
201
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
169
202
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
170
|
-
OIDC_END_SESSION_ENDPOINT: "",
|
|
171
203
|
OIDC_SSO_BUTTON_TEXT: "Login via SSO",
|
|
172
204
|
OIDC_ENFORCE: null,
|
|
173
205
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
@@ -176,9 +208,14 @@ const config = {
|
|
|
176
208
|
OIDC_GROUP_ROLE_MAP: "{}",
|
|
177
209
|
OIDC_REQUIRE_EMAIL_VERIFIED: true,
|
|
178
210
|
OIDC_TRUSTED_IP_HEADER: "",
|
|
211
|
+
OIDC_FORCE_SECURE_COOKIES: false,
|
|
212
|
+
// Populated at bootstrap from OIDC_DISCOVERY_URL — not user-configurable directly
|
|
213
|
+
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
214
|
+
OIDC_TOKEN_ENDPOINT: "",
|
|
215
|
+
OIDC_USERINFO_ENDPOINT: "",
|
|
216
|
+
OIDC_END_SESSION_ENDPOINT: "",
|
|
179
217
|
OIDC_JWKS_URI: "",
|
|
180
|
-
OIDC_ISSUER: ""
|
|
181
|
-
OIDC_FORCE_SECURE_COOKIES: false
|
|
218
|
+
OIDC_ISSUER: ""
|
|
182
219
|
},
|
|
183
220
|
validator() {
|
|
184
221
|
}
|
|
@@ -234,8 +271,7 @@ function shouldMarkSecure(strapi2, ctx) {
|
|
|
234
271
|
if (config2.OIDC_FORCE_SECURE_COOKIES === true) return true;
|
|
235
272
|
if (ctx.request.secure) return true;
|
|
236
273
|
const proxyTrusted = ctx.app?.proxy === true;
|
|
237
|
-
if (proxyTrusted &&
|
|
238
|
-
return true;
|
|
274
|
+
if (proxyTrusted && ctx.get("x-forwarded-proto") === "https") return true;
|
|
239
275
|
return false;
|
|
240
276
|
}
|
|
241
277
|
function getExpiredCookieOptions(strapi2, ctx) {
|
|
@@ -369,7 +405,6 @@ const en = {
|
|
|
369
405
|
"auditlog.table.ip": "IP",
|
|
370
406
|
"auditlog.table.details": "Details",
|
|
371
407
|
"auditlog.table.empty": "No audit log entries",
|
|
372
|
-
"auditlog.loading": "Loading…",
|
|
373
408
|
"auditlog.clear": "Clear Logs",
|
|
374
409
|
"auditlog.clear.title": "Clear All Logs",
|
|
375
410
|
"auditlog.clear.description": "This will permanently delete all {count, plural, one {# audit log entry} other {# audit log entries}}. This action cannot be undone.",
|
|
@@ -424,7 +459,6 @@ const locales = Object.fromEntries(
|
|
|
424
459
|
return [code ?? "", mod.default];
|
|
425
460
|
})
|
|
426
461
|
);
|
|
427
|
-
Object.keys(locales).filter(Boolean);
|
|
428
462
|
const DEFAULT_LOCALE = "en";
|
|
429
463
|
function parseAcceptLanguage(header) {
|
|
430
464
|
return header.split(",").map((part) => {
|
|
@@ -527,13 +561,13 @@ const OIDC_ERROR_DISPATCH = {
|
|
|
527
561
|
key: "sign_in_unknown"
|
|
528
562
|
}
|
|
529
563
|
};
|
|
530
|
-
const
|
|
564
|
+
const TRUSTED_IP_HEADER = "cf-connecting-ip";
|
|
531
565
|
function getTrustedHeaderName() {
|
|
532
566
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc") ?? {};
|
|
533
567
|
const raw = config2.OIDC_TRUSTED_IP_HEADER;
|
|
534
568
|
if (typeof raw !== "string" || !raw) return void 0;
|
|
535
569
|
const normalized = raw.trim().toLowerCase();
|
|
536
|
-
return
|
|
570
|
+
return normalized === TRUSTED_IP_HEADER ? normalized : void 0;
|
|
537
571
|
}
|
|
538
572
|
function getClientIp(ctx) {
|
|
539
573
|
const proxyTrusted = ctx.app?.proxy === true;
|
|
@@ -550,19 +584,23 @@ function getClientIp(ctx) {
|
|
|
550
584
|
}
|
|
551
585
|
return ctx.ip;
|
|
552
586
|
}
|
|
587
|
+
function toMessage(e) {
|
|
588
|
+
return e instanceof Error ? e.message : String(e);
|
|
589
|
+
}
|
|
553
590
|
const REQUIRED_CONFIG_KEYS = [
|
|
591
|
+
"OIDC_DISCOVERY_URL",
|
|
554
592
|
"OIDC_CLIENT_ID",
|
|
555
593
|
"OIDC_CLIENT_SECRET",
|
|
556
594
|
"OIDC_REDIRECT_URI",
|
|
557
595
|
"OIDC_SCOPE",
|
|
558
|
-
"OIDC_TOKEN_ENDPOINT",
|
|
559
|
-
"OIDC_USERINFO_ENDPOINT",
|
|
560
|
-
"OIDC_GRANT_TYPE",
|
|
561
596
|
"OIDC_FAMILY_NAME_FIELD",
|
|
562
597
|
"OIDC_GIVEN_NAME_FIELD",
|
|
598
|
+
// Populated at bootstrap from OIDC_DISCOVERY_URL — checked here as a runtime safety net
|
|
599
|
+
"OIDC_TOKEN_ENDPOINT",
|
|
600
|
+
"OIDC_USERINFO_ENDPOINT",
|
|
563
601
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
564
602
|
];
|
|
565
|
-
const LOGOUT_USERINFO_TIMEOUT_MS =
|
|
603
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
|
|
566
604
|
const jwksCache = /* @__PURE__ */ new Map();
|
|
567
605
|
let jwksDisabledWarned = false;
|
|
568
606
|
function getJwks(uri) {
|
|
@@ -594,7 +632,7 @@ async function verifyIdToken(idToken, config2) {
|
|
|
594
632
|
return payload;
|
|
595
633
|
} catch (e) {
|
|
596
634
|
if (e instanceof jose.errors.JWTClaimValidationFailed || e instanceof jose.errors.JWSSignatureVerificationFailed || e instanceof jose.errors.JWTExpired || e instanceof jose.errors.JWTInvalid || e instanceof jose.errors.JWSInvalid) {
|
|
597
|
-
const msg =
|
|
635
|
+
const msg = toMessage(e);
|
|
598
636
|
throw new OidcError("id_token_invalid", msg, e);
|
|
599
637
|
}
|
|
600
638
|
throw e;
|
|
@@ -785,7 +823,7 @@ async function ensureUser(userService, oauthService2, email, userResponseData, c
|
|
|
785
823
|
);
|
|
786
824
|
return { user, userCreated: true, rolesUpdated: true };
|
|
787
825
|
} catch (e) {
|
|
788
|
-
const msg =
|
|
826
|
+
const msg = toMessage(e);
|
|
789
827
|
throw new OidcError("user_creation_failed", msg, e);
|
|
790
828
|
}
|
|
791
829
|
}
|
|
@@ -835,7 +873,7 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
|
|
|
835
873
|
function classifyOidcError(e, userInfo) {
|
|
836
874
|
const kind = e instanceof OidcError ? e.kind : "unknown";
|
|
837
875
|
const dispatch = OIDC_ERROR_DISPATCH[kind];
|
|
838
|
-
const msg =
|
|
876
|
+
const msg = toMessage(e);
|
|
839
877
|
let params;
|
|
840
878
|
if (kind === "id_token_parse_failed" || kind === "id_token_invalid" || kind === "unknown") {
|
|
841
879
|
params = { error: msg };
|
|
@@ -884,7 +922,7 @@ async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated
|
|
|
884
922
|
}
|
|
885
923
|
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
886
924
|
const errorInfo = classifyOidcError(e, userInfo);
|
|
887
|
-
const message =
|
|
925
|
+
const message = toMessage(e);
|
|
888
926
|
await auditLog2.log({
|
|
889
927
|
action: errorInfo.action,
|
|
890
928
|
email: userInfo?.email,
|
|
@@ -925,7 +963,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
925
963
|
client_id: config2.OIDC_CLIENT_ID,
|
|
926
964
|
client_secret: config2.OIDC_CLIENT_SECRET,
|
|
927
965
|
redirect_uri: config2.OIDC_REDIRECT_URI,
|
|
928
|
-
grant_type:
|
|
966
|
+
grant_type: "authorization_code",
|
|
929
967
|
code_verifier: codeVerifier ?? ""
|
|
930
968
|
});
|
|
931
969
|
let userInfo;
|
|
@@ -969,13 +1007,13 @@ async function oidcSignInCallback(ctx) {
|
|
|
969
1007
|
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
970
1008
|
}
|
|
971
1009
|
}
|
|
972
|
-
async function
|
|
1010
|
+
async function isProviderSessionExpired(userinfoEndpoint, accessToken) {
|
|
973
1011
|
try {
|
|
974
1012
|
const response = await fetch(userinfoEndpoint, {
|
|
975
1013
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
976
1014
|
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
977
1015
|
});
|
|
978
|
-
return response.ok;
|
|
1016
|
+
return !response.ok;
|
|
979
1017
|
} catch {
|
|
980
1018
|
return false;
|
|
981
1019
|
}
|
|
@@ -995,14 +1033,14 @@ async function logout(ctx) {
|
|
|
995
1033
|
}
|
|
996
1034
|
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: getClientIp(ctx) }) : Promise.resolve();
|
|
997
1035
|
if (logoutUrl && accessToken) {
|
|
998
|
-
const
|
|
999
|
-
if (
|
|
1000
|
-
logAudit("
|
|
1001
|
-
|
|
1002
|
-
return ctx.redirect(logoutUrl);
|
|
1036
|
+
const expired = await isProviderSessionExpired(config2.OIDC_USERINFO_ENDPOINT, accessToken);
|
|
1037
|
+
if (expired) {
|
|
1038
|
+
await logAudit("session_expired");
|
|
1039
|
+
return ctx.redirect(loginUrl);
|
|
1003
1040
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1041
|
+
logAudit("logout").catch(() => {
|
|
1042
|
+
});
|
|
1043
|
+
return ctx.redirect(logoutUrl);
|
|
1006
1044
|
}
|
|
1007
1045
|
await logAudit("logout");
|
|
1008
1046
|
ctx.redirect(logoutUrl || loginUrl);
|
|
@@ -1160,13 +1198,9 @@ async function importUsers(ctx) {
|
|
|
1160
1198
|
const whitelistService2 = getWhitelistService();
|
|
1161
1199
|
const existing = await whitelistService2.getUsers();
|
|
1162
1200
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
await whitelistService2.registerUser(email);
|
|
1167
|
-
importedCount++;
|
|
1168
|
-
}
|
|
1169
|
-
ctx.body = { importedCount };
|
|
1201
|
+
const toImport = deduped.filter((email) => !existingEmails.has(email));
|
|
1202
|
+
await Promise.all(toImport.map((email) => whitelistService2.registerUser(email)));
|
|
1203
|
+
ctx.body = { importedCount: toImport.length };
|
|
1170
1204
|
}
|
|
1171
1205
|
async function syncUsers(ctx) {
|
|
1172
1206
|
const { users: rawUsers } = ctx.request.body;
|
|
@@ -1175,16 +1209,10 @@ async function syncUsers(ctx) {
|
|
|
1175
1209
|
const currentUsers = await whitelistService2.getUsers();
|
|
1176
1210
|
const syncEmailSet = new Set(emails);
|
|
1177
1211
|
const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1183
|
-
for (const email of emails) {
|
|
1184
|
-
if (!currentUsersByEmail.has(email)) {
|
|
1185
|
-
await whitelistService2.registerUser(email);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1212
|
+
await Promise.all([
|
|
1213
|
+
...currentUsers.filter((u) => !syncEmailSet.has(u.email)).map((u) => whitelistService2.removeUser(u.email)),
|
|
1214
|
+
...emails.filter((email) => !currentUsersByEmail.has(email)).map((email) => whitelistService2.registerUser(email))
|
|
1215
|
+
]);
|
|
1188
1216
|
ctx.body = {};
|
|
1189
1217
|
}
|
|
1190
1218
|
const whitelist = {
|
|
@@ -1488,6 +1516,12 @@ const routes = {
|
|
|
1488
1516
|
handler: "oidc.oidcSignInCallback",
|
|
1489
1517
|
config: { auth: false, middlewares: [rateLimitMiddleware] }
|
|
1490
1518
|
},
|
|
1519
|
+
{
|
|
1520
|
+
method: "GET",
|
|
1521
|
+
path: "/logout",
|
|
1522
|
+
handler: "oidc.logout",
|
|
1523
|
+
config: { auth: false }
|
|
1524
|
+
},
|
|
1491
1525
|
{
|
|
1492
1526
|
method: "POST",
|
|
1493
1527
|
path: "/logout",
|
|
@@ -1796,9 +1830,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1796
1830
|
}
|
|
1797
1831
|
const modelDef = strapi2.getModel("admin::user");
|
|
1798
1832
|
const sanitizedEntity = await strapiUtils__default.default.sanitize.sanitizers.defaultSanitizeOutput(
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1833
|
+
{
|
|
1834
|
+
schema: modelDef,
|
|
1835
|
+
getModel: (uid2) => strapi2.getModel(uid2)
|
|
1836
|
+
},
|
|
1802
1837
|
user
|
|
1803
1838
|
);
|
|
1804
1839
|
eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
|