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 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-F7EShzfg.js")),
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 loginObserver = null;
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 = "/strapi-plugin-oidc/oidc";
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
- if (loginObserver) return;
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) loginObserver?.disconnect();
297
- };
298
- tick();
299
- loginObserver = new MutationObserver(tick);
300
- loginObserver.observe(document.body, { childList: true, subtree: true });
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-vkwBXPXn.mjs"),
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 loginObserver = null;
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 = "/strapi-plugin-oidc/oidc";
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
- if (loginObserver) return;
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) loginObserver?.disconnect();
294
- };
295
- tick();
296
- loginObserver = new MutationObserver(tick);
297
- loginObserver.observe(document.body, { childList: true, subtree: true });
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-CUVEK5GT.mjs";
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-BUuwz71P.js");
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 };
@@ -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-BUuwz71P.js");
3
+ const index = require("./index-BF9ZcATc.js");
4
4
  require("react");
5
5
  require("react-dom/client");
6
6
  exports.default = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "./index-CUVEK5GT.mjs";
1
+ import { i } from "./index-CpOrxYA4.mjs";
2
2
  import "react";
3
3
  import "react-dom/client";
4
4
  export {
@@ -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(loginUrl);
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(loginUrl);
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 || loginUrl);
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) {
@@ -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(loginUrl);
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(loginUrl);
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 || loginUrl);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-oidc",
3
- "version": "1.9.7",
3
+ "version": "1.10.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",