strapi-plugin-oidc 1.9.7 → 1.10.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 +16 -1
- package/dist/admin/{index-BUuwz71P.js → index-BF9ZcATc.js} +30 -11
- package/dist/admin/{index-CUVEK5GT.mjs → index-CpOrxYA4.mjs} +30 -11
- package/dist/admin/{index-vkwBXPXn.mjs → index-DH-_mRbC.mjs} +1 -1
- package/dist/admin/{index-F7EShzfg.js → index-iTiehRZW.js} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +38 -6
- package/dist/server/index.mjs +38 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
<img src="https://img.shields.io/npm/v/strapi-plugin-oidc.svg" alt="npm version">
|
|
7
7
|
</a>
|
|
8
8
|
<a href="https://github.com/edmogeor/strapi-plugin-oidc/actions/workflows/ci.yml">
|
|
9
|
-
<img src="https://github.com/edmogeor/strapi-plugin-oidc/actions/workflows/ci.yml/badge.svg" alt="CI"/>
|
|
9
|
+
<img src="https://github.com/edmogeor/strapi-plugin-oidc/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"/>
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://github.com/fallow-rs/fallow">
|
|
12
|
+
<img src="https://raw.githubusercontent.com/edmogeor/strapi-plugin-oidc/badges/badge.svg" alt="fallow health"/>
|
|
10
13
|
</a>
|
|
11
14
|
<a href="./LICENSE">
|
|
12
15
|
<img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"/>
|
|
@@ -50,6 +53,7 @@ module.exports = ({ env }) => ({
|
|
|
50
53
|
OIDC_REQUIRE_EMAIL_VERIFIED: true, // Reject logins when provider does not report email_verified=true (set false to disable)
|
|
51
54
|
OIDC_TRUSTED_IP_HEADER: '', // Optional: header set by your CDN/proxy containing the real client IP (see note below); only honoured when Koa proxy mode is enabled (see below)
|
|
52
55
|
OIDC_FORCE_SECURE_COOKIES: false, // Set true when behind a trusted HTTPS proxy that Strapi can't auto-detect
|
|
56
|
+
OIDC_SKIP_LOGIN_PAGE: false, // Skip the Strapi login page; redirects unauthenticated users straight to the OIDC provider
|
|
53
57
|
},
|
|
54
58
|
},
|
|
55
59
|
});
|
|
@@ -94,6 +98,17 @@ Only headers that CDN/proxy vendors guarantee to strip from inbound client reque
|
|
|
94
98
|
|
|
95
99
|
Navigate to `/strapi-plugin-oidc/oidc` to start the OIDC flow, or click the **Login via SSO** button injected into the Strapi login page.
|
|
96
100
|
|
|
101
|
+
## Skip Login Page
|
|
102
|
+
|
|
103
|
+
Set `OIDC_SKIP_LOGIN_PAGE: true` to prevent users from ever seeing the Strapi admin login page. Unauthenticated requests are redirected directly to the OIDC provider. This is typically used alongside `OIDC_ENFORCE: true`.
|
|
104
|
+
|
|
105
|
+
Two independent mechanisms cover both entry points:
|
|
106
|
+
|
|
107
|
+
- **Server-side** — A Koa middleware intercepts unauthenticated GET requests to `/admin` and redirects to `/strapi-plugin-oidc/oidc` before the SPA is served. Excluded from this redirect: API routes (`/admin/login`, `/admin/logout`, `/admin/init`, etc.), static assets (`.js`, `.css`, `.png`, etc.), and POST requests.
|
|
108
|
+
- **Client-side** — A DOM `MutationObserver` watches for React Router navigations to `/auth/login` (triggered by session expiry or 401 responses) and redirects before the login form renders.
|
|
109
|
+
|
|
110
|
+
After logout without a provider `end_session_endpoint`, the fallback URL becomes `/strapi-plugin-oidc/oidc` instead of `/admin/auth/login`, ensuring the user lands on the provider's own page.
|
|
111
|
+
|
|
97
112
|
## Logout
|
|
98
113
|
|
|
99
114
|
When the discovery document includes an `end_session_endpoint`, clicking logout redirects to the provider's end-session URL (RP-initiated logout). If the provider session has already expired, Strapi skips the redirect and goes straight to the login page.
|
|
@@ -197,6 +197,7 @@ const PERMISSIONS = {
|
|
|
197
197
|
const AUDIT_LOG_DEFAULTS = {
|
|
198
198
|
ADMIN_PAGE_SIZE: 10
|
|
199
199
|
};
|
|
200
|
+
const OIDC_SIGN_IN_PATH = "/strapi-plugin-oidc/oidc";
|
|
200
201
|
const UI_DEFAULTS = {
|
|
201
202
|
MIN_SPINNER_MS: 400
|
|
202
203
|
};
|
|
@@ -211,7 +212,7 @@ const index = {
|
|
|
211
212
|
id: "settings.configuration",
|
|
212
213
|
defaultMessage: "Configuration"
|
|
213
214
|
},
|
|
214
|
-
Component: () => Promise.resolve().then(() => require("./index-
|
|
215
|
+
Component: () => Promise.resolve().then(() => require("./index-iTiehRZW.js")),
|
|
215
216
|
permissions: [{ action: PERMISSIONS.READ, subject: null }]
|
|
216
217
|
};
|
|
217
218
|
app.addSettingsLink(
|
|
@@ -231,13 +232,18 @@ const index = {
|
|
|
231
232
|
});
|
|
232
233
|
},
|
|
233
234
|
bootstrap() {
|
|
235
|
+
const isAuthRoute = (path) => /\/auth\/(login|register|forgot-password|reset-password)/.test(path);
|
|
236
|
+
const hasSkipCookie = document.cookie.split(";").some((c) => c.trim() === "oidc_skip_login_page=1");
|
|
237
|
+
if (hasSkipCookie && isAuthRoute(window.location.pathname)) {
|
|
238
|
+
window.location.replace(OIDC_SIGN_IN_PATH);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
234
241
|
const overlayContainer = document.createElement("div");
|
|
235
242
|
document.body.appendChild(overlayContainer);
|
|
236
243
|
client.createRoot(overlayContainer).render(React__default.default.createElement(LogoutOverlay));
|
|
237
244
|
const defaultButtonText = t("login.sso");
|
|
238
|
-
const isAuthRoute = (path) => /\/auth\/(login|register|forgot-password|reset-password)/.test(path);
|
|
239
245
|
let ssoButtonInjected = false;
|
|
240
|
-
let
|
|
246
|
+
let domObserver = null;
|
|
241
247
|
const injectSSOButton = (buttonText) => {
|
|
242
248
|
if (ssoButtonInjected) return;
|
|
243
249
|
if (!isAuthRoute(window.location.pathname)) return;
|
|
@@ -249,7 +255,7 @@ const index = {
|
|
|
249
255
|
btn.type = "button";
|
|
250
256
|
btn.className = submitButton.className;
|
|
251
257
|
btn.onclick = () => {
|
|
252
|
-
window.location.href =
|
|
258
|
+
window.location.href = OIDC_SIGN_IN_PATH;
|
|
253
259
|
};
|
|
254
260
|
const innerSpan = submitButton.querySelector("span");
|
|
255
261
|
const span = document.createElement("span");
|
|
@@ -287,23 +293,36 @@ const index = {
|
|
|
287
293
|
(el.closest("div")?.parentElement ?? el).remove();
|
|
288
294
|
});
|
|
289
295
|
};
|
|
296
|
+
const startObserver = (tick) => {
|
|
297
|
+
if (domObserver) return;
|
|
298
|
+
tick();
|
|
299
|
+
domObserver = new MutationObserver(tick);
|
|
300
|
+
domObserver.observe(document.body, { childList: true, subtree: true });
|
|
301
|
+
};
|
|
290
302
|
const startLoginObserver = (buttonText, enforced) => {
|
|
291
|
-
|
|
292
|
-
const tick = () => {
|
|
303
|
+
startObserver(() => {
|
|
293
304
|
if (!isAuthRoute(window.location.pathname)) return;
|
|
294
305
|
injectSSOButton(buttonText);
|
|
295
306
|
if (enforced) removeEnforcedElements();
|
|
296
|
-
if (ssoButtonInjected && !enforced)
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
307
|
+
if (ssoButtonInjected && !enforced) domObserver?.disconnect();
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const startSkipLoginRedirect = () => {
|
|
311
|
+
startObserver(() => {
|
|
312
|
+
if (isAuthRoute(window.location.pathname)) {
|
|
313
|
+
window.location.href = OIDC_SIGN_IN_PATH;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
301
316
|
};
|
|
302
317
|
const applySettings = async () => {
|
|
303
318
|
try {
|
|
304
319
|
const response = await window.fetch("/strapi-plugin-oidc/settings/public");
|
|
305
320
|
if (response.ok) {
|
|
306
321
|
const data = await response.json();
|
|
322
|
+
if (data.skipLoginPage) {
|
|
323
|
+
startSkipLoginRedirect();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
307
326
|
startLoginObserver(data.ssoButtonText || defaultButtonText, !!data.enforceOIDC);
|
|
308
327
|
} else {
|
|
309
328
|
startLoginObserver(defaultButtonText, false);
|
|
@@ -194,6 +194,7 @@ const PERMISSIONS = {
|
|
|
194
194
|
const AUDIT_LOG_DEFAULTS = {
|
|
195
195
|
ADMIN_PAGE_SIZE: 10
|
|
196
196
|
};
|
|
197
|
+
const OIDC_SIGN_IN_PATH = "/strapi-plugin-oidc/oidc";
|
|
197
198
|
const UI_DEFAULTS = {
|
|
198
199
|
MIN_SPINNER_MS: 400
|
|
199
200
|
};
|
|
@@ -208,7 +209,7 @@ const index = {
|
|
|
208
209
|
id: "settings.configuration",
|
|
209
210
|
defaultMessage: "Configuration"
|
|
210
211
|
},
|
|
211
|
-
Component: () => import("./index-
|
|
212
|
+
Component: () => import("./index-DH-_mRbC.mjs"),
|
|
212
213
|
permissions: [{ action: PERMISSIONS.READ, subject: null }]
|
|
213
214
|
};
|
|
214
215
|
app.addSettingsLink(
|
|
@@ -228,13 +229,18 @@ const index = {
|
|
|
228
229
|
});
|
|
229
230
|
},
|
|
230
231
|
bootstrap() {
|
|
232
|
+
const isAuthRoute = (path) => /\/auth\/(login|register|forgot-password|reset-password)/.test(path);
|
|
233
|
+
const hasSkipCookie = document.cookie.split(";").some((c) => c.trim() === "oidc_skip_login_page=1");
|
|
234
|
+
if (hasSkipCookie && isAuthRoute(window.location.pathname)) {
|
|
235
|
+
window.location.replace(OIDC_SIGN_IN_PATH);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
231
238
|
const overlayContainer = document.createElement("div");
|
|
232
239
|
document.body.appendChild(overlayContainer);
|
|
233
240
|
createRoot(overlayContainer).render(React.createElement(LogoutOverlay));
|
|
234
241
|
const defaultButtonText = t("login.sso");
|
|
235
|
-
const isAuthRoute = (path) => /\/auth\/(login|register|forgot-password|reset-password)/.test(path);
|
|
236
242
|
let ssoButtonInjected = false;
|
|
237
|
-
let
|
|
243
|
+
let domObserver = null;
|
|
238
244
|
const injectSSOButton = (buttonText) => {
|
|
239
245
|
if (ssoButtonInjected) return;
|
|
240
246
|
if (!isAuthRoute(window.location.pathname)) return;
|
|
@@ -246,7 +252,7 @@ const index = {
|
|
|
246
252
|
btn.type = "button";
|
|
247
253
|
btn.className = submitButton.className;
|
|
248
254
|
btn.onclick = () => {
|
|
249
|
-
window.location.href =
|
|
255
|
+
window.location.href = OIDC_SIGN_IN_PATH;
|
|
250
256
|
};
|
|
251
257
|
const innerSpan = submitButton.querySelector("span");
|
|
252
258
|
const span = document.createElement("span");
|
|
@@ -284,23 +290,36 @@ const index = {
|
|
|
284
290
|
(el.closest("div")?.parentElement ?? el).remove();
|
|
285
291
|
});
|
|
286
292
|
};
|
|
293
|
+
const startObserver = (tick) => {
|
|
294
|
+
if (domObserver) return;
|
|
295
|
+
tick();
|
|
296
|
+
domObserver = new MutationObserver(tick);
|
|
297
|
+
domObserver.observe(document.body, { childList: true, subtree: true });
|
|
298
|
+
};
|
|
287
299
|
const startLoginObserver = (buttonText, enforced) => {
|
|
288
|
-
|
|
289
|
-
const tick = () => {
|
|
300
|
+
startObserver(() => {
|
|
290
301
|
if (!isAuthRoute(window.location.pathname)) return;
|
|
291
302
|
injectSSOButton(buttonText);
|
|
292
303
|
if (enforced) removeEnforcedElements();
|
|
293
|
-
if (ssoButtonInjected && !enforced)
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
304
|
+
if (ssoButtonInjected && !enforced) domObserver?.disconnect();
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
const startSkipLoginRedirect = () => {
|
|
308
|
+
startObserver(() => {
|
|
309
|
+
if (isAuthRoute(window.location.pathname)) {
|
|
310
|
+
window.location.href = OIDC_SIGN_IN_PATH;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
298
313
|
};
|
|
299
314
|
const applySettings = async () => {
|
|
300
315
|
try {
|
|
301
316
|
const response = await window.fetch("/strapi-plugin-oidc/settings/public");
|
|
302
317
|
if (response.ok) {
|
|
303
318
|
const data = await response.json();
|
|
319
|
+
if (data.skipLoginPage) {
|
|
320
|
+
startSkipLoginRedirect();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
304
323
|
startLoginObserver(data.ssoButtonText || defaultButtonText, !!data.enforceOIDC);
|
|
305
324
|
} else {
|
|
306
325
|
startLoginObserver(defaultButtonText, false);
|
|
@@ -5,7 +5,7 @@ import { useState, useRef, useId, useEffect, useCallback, useReducer, useMemo, m
|
|
|
5
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, E as EMAIL_REGEX, e as en, A as AUDIT_LOG_DEFAULTS, U as UI_DEFAULTS } from "./index-
|
|
8
|
+
import { g as getTrad, E as EMAIL_REGEX, e as en, A as AUDIT_LOG_DEFAULTS, U as UI_DEFAULTS } from "./index-CpOrxYA4.mjs";
|
|
9
9
|
import styled from "styled-components";
|
|
10
10
|
import { Filter, ClipboardList, Server } from "lucide-react";
|
|
11
11
|
function Role({ oidcRoles, roles, onChangeRole }) {
|
|
@@ -7,7 +7,7 @@ const React = require("react");
|
|
|
7
7
|
const designSystem = require("@strapi/design-system");
|
|
8
8
|
const icons = require("@strapi/icons");
|
|
9
9
|
const reactIntl = require("react-intl");
|
|
10
|
-
const index = require("./index-
|
|
10
|
+
const index = require("./index-BF9ZcATc.js");
|
|
11
11
|
const styled = require("styled-components");
|
|
12
12
|
const lucideReact = require("lucide-react");
|
|
13
13
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
package/dist/admin/index.js
CHANGED
|
@@ -1,6 +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-BF9ZcATc.js");
|
|
4
4
|
require("react");
|
|
5
5
|
require("react-dom/client");
|
|
6
6
|
exports.default = index.index;
|
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -122,7 +122,8 @@ const pluginConfigSchema = zod.z.object({
|
|
|
122
122
|
OIDC_TRUSTED_IP_HEADER: zod.z.string().default(""),
|
|
123
123
|
OIDC_JWKS_URI: zod.z.string().default(""),
|
|
124
124
|
OIDC_ISSUER: zod.z.string().default(""),
|
|
125
|
-
OIDC_FORCE_SECURE_COOKIES: coerceBool(false)
|
|
125
|
+
OIDC_FORCE_SECURE_COOKIES: coerceBool(false),
|
|
126
|
+
OIDC_SKIP_LOGIN_PAGE: coerceBool(false)
|
|
126
127
|
});
|
|
127
128
|
function parseGroupRoleMap(raw) {
|
|
128
129
|
if (typeof raw !== "string") {
|
|
@@ -173,6 +174,7 @@ const DEFAULT_RETENTION_DAYS = 90;
|
|
|
173
174
|
const DAY_MS = 864e5;
|
|
174
175
|
const DISCOVERY_TIMEOUT_MS = 5e3;
|
|
175
176
|
const OIDC_DISCOVERY_PATH = "/.well-known/openid-configuration";
|
|
177
|
+
const OIDC_SIGN_IN_PATH = "/strapi-plugin-oidc/oidc";
|
|
176
178
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
177
179
|
function getPluginConfig() {
|
|
178
180
|
return pluginConfigSchema.parse(strapi.config.get("plugin::strapi-plugin-oidc") ?? {});
|
|
@@ -252,7 +254,8 @@ const COOKIE_NAMES = {
|
|
|
252
254
|
accessToken: "oidc_access_token",
|
|
253
255
|
userEmail: "oidc_user_email",
|
|
254
256
|
adminRefresh: "strapi_admin_refresh",
|
|
255
|
-
authenticated: "oidc_authenticated"
|
|
257
|
+
authenticated: "oidc_authenticated",
|
|
258
|
+
skipLoginPage: "oidc_skip_login_page"
|
|
256
259
|
};
|
|
257
260
|
function shouldMarkSecure(strapi2, ctx) {
|
|
258
261
|
const isProduction = strapi2.config.get("environment") === "production";
|
|
@@ -284,15 +287,41 @@ function clearAuthCookies(strapi2, ctx) {
|
|
|
284
287
|
ctx.cookies.set(COOKIE_NAMES.userEmail, "", rootPathOptions);
|
|
285
288
|
}
|
|
286
289
|
const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
|
|
290
|
+
const STATIC_EXTENSIONS = [".js", ".css", ".png", ".svg", ".ico", ".woff2", ".json", ".map"];
|
|
287
291
|
async function bootstrap({ strapi: strapi2 }) {
|
|
288
292
|
await applyDiscovery(strapi2);
|
|
289
293
|
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
290
294
|
const tokenRefreshPath = `${adminUrl}/token/refresh`;
|
|
295
|
+
const EXCLUDED_ADMIN_PATHS = [
|
|
296
|
+
`${adminUrl}/login`,
|
|
297
|
+
`${adminUrl}/access-token`,
|
|
298
|
+
`${adminUrl}/logout`,
|
|
299
|
+
`${adminUrl}/init`,
|
|
300
|
+
`${adminUrl}/register`,
|
|
301
|
+
`${adminUrl}/register-admin`,
|
|
302
|
+
`${adminUrl}/forgot-password`,
|
|
303
|
+
`${adminUrl}/reset-password`
|
|
304
|
+
];
|
|
291
305
|
const enforceOidcMiddleware = async (ctx, next) => {
|
|
292
306
|
const path = ctx.request.path;
|
|
293
307
|
const isPost = ctx.request.method === "POST";
|
|
294
308
|
const isAuthRoute = AUTH_ROUTES.some((r) => path.includes(r));
|
|
295
309
|
const isTokenRefresh = path === tokenRefreshPath;
|
|
310
|
+
const config2 = getPluginConfig();
|
|
311
|
+
const isAdminPage = ctx.request.method === "GET" && (path === adminUrl || path.startsWith(`${adminUrl}/`)) && !STATIC_EXTENSIONS.some((ext) => path.endsWith(ext));
|
|
312
|
+
if (isAdminPage) {
|
|
313
|
+
ctx.cookies.set(COOKIE_NAMES.skipLoginPage, config2.OIDC_SKIP_LOGIN_PAGE ? "1" : "0", {
|
|
314
|
+
httpOnly: false,
|
|
315
|
+
signed: false,
|
|
316
|
+
sameSite: "lax",
|
|
317
|
+
path: adminUrl,
|
|
318
|
+
secure: false
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (config2.OIDC_SKIP_LOGIN_PAGE && isAdminPage && !EXCLUDED_ADMIN_PATHS.includes(path) && !ctx.cookies.get(COOKIE_NAMES.adminRefresh)) {
|
|
322
|
+
ctx.redirect(OIDC_SIGN_IN_PATH);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
296
325
|
if (isAuthRoute && isPost || isTokenRefresh) {
|
|
297
326
|
try {
|
|
298
327
|
const whitelistService2 = getWhitelistService();
|
|
@@ -412,6 +441,7 @@ const config = {
|
|
|
412
441
|
OIDC_REQUIRE_EMAIL_VERIFIED: true,
|
|
413
442
|
OIDC_TRUSTED_IP_HEADER: "",
|
|
414
443
|
OIDC_FORCE_SECURE_COOKIES: false,
|
|
444
|
+
OIDC_SKIP_LOGIN_PAGE: false,
|
|
415
445
|
// Populated at bootstrap from OIDC_ISSUER — not user-configurable directly
|
|
416
446
|
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
417
447
|
OIDC_TOKEN_ENDPOINT: "",
|
|
@@ -4011,12 +4041,13 @@ async function logout(ctx) {
|
|
|
4011
4041
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
4012
4042
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
4013
4043
|
const loginUrl = `${adminPanelUrl}/auth/login`;
|
|
4044
|
+
const fallbackUrl = config2.OIDC_SKIP_LOGIN_PAGE ? OIDC_SIGN_IN_PATH : loginUrl;
|
|
4014
4045
|
const isOidcSession = !!ctx.cookies.get(COOKIE_NAMES.authenticated);
|
|
4015
4046
|
const accessToken = ctx.cookies.get(COOKIE_NAMES.accessToken);
|
|
4016
4047
|
const userEmail = ctx.cookies.get(COOKIE_NAMES.userEmail) ?? void 0;
|
|
4017
4048
|
clearAuthCookies(strapi, ctx);
|
|
4018
4049
|
if (!isOidcSession) {
|
|
4019
|
-
return ctx.redirect(
|
|
4050
|
+
return ctx.redirect(fallbackUrl);
|
|
4020
4051
|
}
|
|
4021
4052
|
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: getClientIp(ctx) }) : Promise.resolve();
|
|
4022
4053
|
if (logoutUrl && accessToken) {
|
|
@@ -4025,7 +4056,7 @@ async function logout(ctx) {
|
|
|
4025
4056
|
await logAudit("session_expired").catch((err) => {
|
|
4026
4057
|
strapi.log.error("[strapi-plugin-oidc] Audit log failed on session expiry:", err);
|
|
4027
4058
|
});
|
|
4028
|
-
return ctx.redirect(
|
|
4059
|
+
return ctx.redirect(fallbackUrl);
|
|
4029
4060
|
}
|
|
4030
4061
|
logAudit("logout").catch((err) => {
|
|
4031
4062
|
strapi.log.error("[strapi-plugin-oidc] Audit log failed on logout:", err);
|
|
@@ -4035,7 +4066,7 @@ async function logout(ctx) {
|
|
|
4035
4066
|
await logAudit("logout").catch((err) => {
|
|
4036
4067
|
strapi.log.error("[strapi-plugin-oidc] Audit log failed on logout:", err);
|
|
4037
4068
|
});
|
|
4038
|
-
ctx.redirect(logoutUrl ||
|
|
4069
|
+
ctx.redirect(logoutUrl || fallbackUrl);
|
|
4039
4070
|
}
|
|
4040
4071
|
const oidc = {
|
|
4041
4072
|
oidcSignIn,
|
|
@@ -4157,7 +4188,8 @@ async function publicSettings(ctx) {
|
|
|
4157
4188
|
const config2 = getPluginConfig();
|
|
4158
4189
|
ctx.body = {
|
|
4159
4190
|
enforceOIDC: resolveEnforceOIDC(strapi, settings.enforceOIDC),
|
|
4160
|
-
ssoButtonText: config2.OIDC_SSO_BUTTON_TEXT
|
|
4191
|
+
ssoButtonText: config2.OIDC_SSO_BUTTON_TEXT,
|
|
4192
|
+
skipLoginPage: config2.OIDC_SKIP_LOGIN_PAGE
|
|
4161
4193
|
};
|
|
4162
4194
|
}
|
|
4163
4195
|
async function register(ctx) {
|
package/dist/server/index.mjs
CHANGED
|
@@ -116,7 +116,8 @@ const pluginConfigSchema = z.object({
|
|
|
116
116
|
OIDC_TRUSTED_IP_HEADER: z.string().default(""),
|
|
117
117
|
OIDC_JWKS_URI: z.string().default(""),
|
|
118
118
|
OIDC_ISSUER: z.string().default(""),
|
|
119
|
-
OIDC_FORCE_SECURE_COOKIES: coerceBool(false)
|
|
119
|
+
OIDC_FORCE_SECURE_COOKIES: coerceBool(false),
|
|
120
|
+
OIDC_SKIP_LOGIN_PAGE: coerceBool(false)
|
|
120
121
|
});
|
|
121
122
|
function parseGroupRoleMap(raw) {
|
|
122
123
|
if (typeof raw !== "string") {
|
|
@@ -167,6 +168,7 @@ const DEFAULT_RETENTION_DAYS = 90;
|
|
|
167
168
|
const DAY_MS = 864e5;
|
|
168
169
|
const DISCOVERY_TIMEOUT_MS = 5e3;
|
|
169
170
|
const OIDC_DISCOVERY_PATH = "/.well-known/openid-configuration";
|
|
171
|
+
const OIDC_SIGN_IN_PATH = "/strapi-plugin-oidc/oidc";
|
|
170
172
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
171
173
|
function getPluginConfig() {
|
|
172
174
|
return pluginConfigSchema.parse(strapi.config.get("plugin::strapi-plugin-oidc") ?? {});
|
|
@@ -246,7 +248,8 @@ const COOKIE_NAMES = {
|
|
|
246
248
|
accessToken: "oidc_access_token",
|
|
247
249
|
userEmail: "oidc_user_email",
|
|
248
250
|
adminRefresh: "strapi_admin_refresh",
|
|
249
|
-
authenticated: "oidc_authenticated"
|
|
251
|
+
authenticated: "oidc_authenticated",
|
|
252
|
+
skipLoginPage: "oidc_skip_login_page"
|
|
250
253
|
};
|
|
251
254
|
function shouldMarkSecure(strapi2, ctx) {
|
|
252
255
|
const isProduction = strapi2.config.get("environment") === "production";
|
|
@@ -278,15 +281,41 @@ function clearAuthCookies(strapi2, ctx) {
|
|
|
278
281
|
ctx.cookies.set(COOKIE_NAMES.userEmail, "", rootPathOptions);
|
|
279
282
|
}
|
|
280
283
|
const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
|
|
284
|
+
const STATIC_EXTENSIONS = [".js", ".css", ".png", ".svg", ".ico", ".woff2", ".json", ".map"];
|
|
281
285
|
async function bootstrap({ strapi: strapi2 }) {
|
|
282
286
|
await applyDiscovery(strapi2);
|
|
283
287
|
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
284
288
|
const tokenRefreshPath = `${adminUrl}/token/refresh`;
|
|
289
|
+
const EXCLUDED_ADMIN_PATHS = [
|
|
290
|
+
`${adminUrl}/login`,
|
|
291
|
+
`${adminUrl}/access-token`,
|
|
292
|
+
`${adminUrl}/logout`,
|
|
293
|
+
`${adminUrl}/init`,
|
|
294
|
+
`${adminUrl}/register`,
|
|
295
|
+
`${adminUrl}/register-admin`,
|
|
296
|
+
`${adminUrl}/forgot-password`,
|
|
297
|
+
`${adminUrl}/reset-password`
|
|
298
|
+
];
|
|
285
299
|
const enforceOidcMiddleware = async (ctx, next) => {
|
|
286
300
|
const path = ctx.request.path;
|
|
287
301
|
const isPost = ctx.request.method === "POST";
|
|
288
302
|
const isAuthRoute = AUTH_ROUTES.some((r) => path.includes(r));
|
|
289
303
|
const isTokenRefresh = path === tokenRefreshPath;
|
|
304
|
+
const config2 = getPluginConfig();
|
|
305
|
+
const isAdminPage = ctx.request.method === "GET" && (path === adminUrl || path.startsWith(`${adminUrl}/`)) && !STATIC_EXTENSIONS.some((ext) => path.endsWith(ext));
|
|
306
|
+
if (isAdminPage) {
|
|
307
|
+
ctx.cookies.set(COOKIE_NAMES.skipLoginPage, config2.OIDC_SKIP_LOGIN_PAGE ? "1" : "0", {
|
|
308
|
+
httpOnly: false,
|
|
309
|
+
signed: false,
|
|
310
|
+
sameSite: "lax",
|
|
311
|
+
path: adminUrl,
|
|
312
|
+
secure: false
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
if (config2.OIDC_SKIP_LOGIN_PAGE && isAdminPage && !EXCLUDED_ADMIN_PATHS.includes(path) && !ctx.cookies.get(COOKIE_NAMES.adminRefresh)) {
|
|
316
|
+
ctx.redirect(OIDC_SIGN_IN_PATH);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
290
319
|
if (isAuthRoute && isPost || isTokenRefresh) {
|
|
291
320
|
try {
|
|
292
321
|
const whitelistService2 = getWhitelistService();
|
|
@@ -406,6 +435,7 @@ const config = {
|
|
|
406
435
|
OIDC_REQUIRE_EMAIL_VERIFIED: true,
|
|
407
436
|
OIDC_TRUSTED_IP_HEADER: "",
|
|
408
437
|
OIDC_FORCE_SECURE_COOKIES: false,
|
|
438
|
+
OIDC_SKIP_LOGIN_PAGE: false,
|
|
409
439
|
// Populated at bootstrap from OIDC_ISSUER — not user-configurable directly
|
|
410
440
|
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
411
441
|
OIDC_TOKEN_ENDPOINT: "",
|
|
@@ -4005,12 +4035,13 @@ async function logout(ctx) {
|
|
|
4005
4035
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
4006
4036
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
4007
4037
|
const loginUrl = `${adminPanelUrl}/auth/login`;
|
|
4038
|
+
const fallbackUrl = config2.OIDC_SKIP_LOGIN_PAGE ? OIDC_SIGN_IN_PATH : loginUrl;
|
|
4008
4039
|
const isOidcSession = !!ctx.cookies.get(COOKIE_NAMES.authenticated);
|
|
4009
4040
|
const accessToken = ctx.cookies.get(COOKIE_NAMES.accessToken);
|
|
4010
4041
|
const userEmail = ctx.cookies.get(COOKIE_NAMES.userEmail) ?? void 0;
|
|
4011
4042
|
clearAuthCookies(strapi, ctx);
|
|
4012
4043
|
if (!isOidcSession) {
|
|
4013
|
-
return ctx.redirect(
|
|
4044
|
+
return ctx.redirect(fallbackUrl);
|
|
4014
4045
|
}
|
|
4015
4046
|
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: getClientIp(ctx) }) : Promise.resolve();
|
|
4016
4047
|
if (logoutUrl && accessToken) {
|
|
@@ -4019,7 +4050,7 @@ async function logout(ctx) {
|
|
|
4019
4050
|
await logAudit("session_expired").catch((err) => {
|
|
4020
4051
|
strapi.log.error("[strapi-plugin-oidc] Audit log failed on session expiry:", err);
|
|
4021
4052
|
});
|
|
4022
|
-
return ctx.redirect(
|
|
4053
|
+
return ctx.redirect(fallbackUrl);
|
|
4023
4054
|
}
|
|
4024
4055
|
logAudit("logout").catch((err) => {
|
|
4025
4056
|
strapi.log.error("[strapi-plugin-oidc] Audit log failed on logout:", err);
|
|
@@ -4029,7 +4060,7 @@ async function logout(ctx) {
|
|
|
4029
4060
|
await logAudit("logout").catch((err) => {
|
|
4030
4061
|
strapi.log.error("[strapi-plugin-oidc] Audit log failed on logout:", err);
|
|
4031
4062
|
});
|
|
4032
|
-
ctx.redirect(logoutUrl ||
|
|
4063
|
+
ctx.redirect(logoutUrl || fallbackUrl);
|
|
4033
4064
|
}
|
|
4034
4065
|
const oidc = {
|
|
4035
4066
|
oidcSignIn,
|
|
@@ -4151,7 +4182,8 @@ async function publicSettings(ctx) {
|
|
|
4151
4182
|
const config2 = getPluginConfig();
|
|
4152
4183
|
ctx.body = {
|
|
4153
4184
|
enforceOIDC: resolveEnforceOIDC(strapi, settings.enforceOIDC),
|
|
4154
|
-
ssoButtonText: config2.OIDC_SSO_BUTTON_TEXT
|
|
4185
|
+
ssoButtonText: config2.OIDC_SSO_BUTTON_TEXT,
|
|
4186
|
+
skipLoginPage: config2.OIDC_SKIP_LOGIN_PAGE
|
|
4155
4187
|
};
|
|
4156
4188
|
}
|
|
4157
4189
|
async function register(ctx) {
|
package/package.json
CHANGED