strapi-plugin-oidc 1.5.1 → 1.5.3

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
@@ -36,27 +36,15 @@ module.exports = ({ env }) => ({
36
36
  OIDC_TOKEN_ENDPOINT: env('OIDC_TOKEN_ENDPOINT'),
37
37
  OIDC_USERINFO_ENDPOINT: env('OIDC_USERINFO_ENDPOINT'),
38
38
 
39
- // Optional
39
+ // Optional — defaults shown
40
40
  OIDC_SCOPE: 'openid profile email',
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_END_SESSION_ENDPOINT: '', // Provider end-session URL; omit to redirect to Strapi login
44
45
  OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
45
- OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override
46
- REMEMBER_ME: false,
47
-
48
- // Optional — RP-Initiated Logout
49
- // Redirects the browser to the provider's end-session page on logout.
50
- // Both found in your provider's /.well-known/openid-configuration.
51
- OIDC_END_SESSION_ENDPOINT: env('OIDC_END_SESSION_ENDPOINT', ''),
52
- OIDC_POST_LOGOUT_REDIRECT_URI: env('OIDC_POST_LOGOUT_REDIRECT_URI', ''),
53
-
54
- // Optional — Backchannel Logout
55
- // Allows the provider to revoke Strapi sessions server-to-server.
56
- // Set your provider's logout URI to: https://your-strapi.com/strapi-plugin-oidc/logout
57
- // Both found in your provider's /.well-known/openid-configuration.
58
- OIDC_ISSUER: env('OIDC_ISSUER', ''),
59
- OIDC_JWKS_URI: env('OIDC_JWKS_URI', ''),
46
+ OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
47
+ REMEMBER_ME: false, // Persist session across browser restarts
60
48
  },
61
49
  },
62
50
  });
@@ -146,6 +134,7 @@ This plugin is a hard fork of [`strapi-plugin-sso`](https://github.com/yasudaclo
146
134
 
147
135
  - Removed alternative SSO methods to simplify the plugin.
148
136
  - Redesigned the Whitelist and Role management UI (switched to native Strapi cards, added pagination, etc.).
137
+ - Added an OIDC logout redirect URL.
149
138
  - Added an option to "Enforce OIDC login" with an admin toggle (automatically disabled if the whitelist is empty).
150
139
  - Migrated the testing framework to Vitest and added comprehensive test coverage for controllers and services.
151
140
  - Cleaned up dead code and unused dependencies to improve maintainability.
@@ -157,8 +146,4 @@ This plugin is a hard fork of [`strapi-plugin-sso`](https://github.com/yasudaclo
157
146
  - Bulk delete all entries with a confirmation dialog.
158
147
  - Unsaved changes confirmation when navigating away from the settings page.
159
148
  - Programmatic API for managing the whitelist via Strapi API tokens (list, register, import, delete, delete all).
160
- - **RP-Initiated Logout** (OpenID Connect RP-Initiated Logout 1.0): on logout, Strapi redirects the browser to the provider's end-session endpoint with `id_token_hint` and `post_logout_redirect_uri`, cleanly terminating the SSO session. Configured via `OIDC_END_SESSION_ENDPOINT` and `OIDC_POST_LOGOUT_REDIRECT_URI`.
161
- - **Backchannel Logout** (OIDC Back-Channel Logout 1.0): `POST /strapi-plugin-oidc/logout` accepts a signed logout token from the provider, validates it, and revokes the user's Strapi admin session — keeping Strapi in sync when a user logs out elsewhere. Configured via `OIDC_ISSUER` and `OIDC_JWKS_URI`.
162
- - Renamed config keys to match OIDC discovery document field names: `OIDC_LOGOUT_URL` → `OIDC_END_SESSION_ENDPOINT`, `OIDC_USER_INFO_ENDPOINT` → `OIDC_USERINFO_ENDPOINT`, `OIDC_SCOPES` → `OIDC_SCOPE`.
163
- - Security hardening: PKCE (`S256`), server-side `state` generation (CSRF protection), nonce validation (ID token replay prevention), `Authorization: Bearer` header for userinfo requests, generic error messages on callback failure.
164
149
  - Added misc. quality of life improvements and bug fixes.
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
3
  const node_crypto = require("node:crypto");
4
- const jose = require("jose");
5
4
  const pkceChallenge = require("pkce-challenge");
6
5
  const strapiUtils = require("@strapi/utils");
7
6
  const generator = require("generate-password");
@@ -25,17 +24,6 @@ function resolveEnforceOIDC(strapi2, dbValue) {
25
24
  if (configValue !== null) return configValue;
26
25
  return dbValue ?? false;
27
26
  }
28
- const revoked = /* @__PURE__ */ new Map();
29
- const CLEANUP_DELAY_MS = 24 * 60 * 60 * 1e3;
30
- function revokeUser(userId) {
31
- revoked.set(userId, Date.now());
32
- setTimeout(() => revoked.delete(userId), CLEANUP_DELAY_MS);
33
- }
34
- function isUserRevoked(userId, iat) {
35
- const logoutTime = revoked.get(userId);
36
- if (!logoutTime) return false;
37
- return iat * 1e3 <= logoutTime;
38
- }
39
27
  async function bootstrap({ strapi: strapi2 }) {
40
28
  const enforceOidcMiddleware = async (ctx, next) => {
41
29
  const adminUrl = strapi2.config.get("admin.url", "/admin");
@@ -88,36 +76,9 @@ async function bootstrap({ strapi: strapi2 }) {
88
76
  }
89
77
  await next();
90
78
  };
91
- const denylistMiddleware = async (ctx, next) => {
92
- const auth = ctx.request.headers.authorization;
93
- if (auth?.startsWith("Bearer ")) {
94
- try {
95
- const payload = JSON.parse(
96
- Buffer.from(auth.slice(7).split(".")[1], "base64url").toString()
97
- );
98
- if (payload.id && isUserRevoked(String(payload.id), payload.iat ?? 0)) {
99
- ctx.status = 401;
100
- ctx.body = {
101
- data: null,
102
- error: {
103
- status: 401,
104
- name: "UnauthorizedError",
105
- message: "Session revoked",
106
- details: {}
107
- }
108
- };
109
- return;
110
- }
111
- } catch {
112
- }
113
- }
114
- await next();
115
- };
116
79
  if (strapi2.server.app && Array.isArray(strapi2.server.app.middleware)) {
117
80
  strapi2.server.app.middleware.unshift(enforceOidcMiddleware);
118
- strapi2.server.app.middleware.unshift(denylistMiddleware);
119
81
  } else {
120
- strapi2.server.use(denylistMiddleware);
121
82
  strapi2.server.use(enforceOidcMiddleware);
122
83
  }
123
84
  const actions = [
@@ -208,12 +169,6 @@ const config = {
208
169
  OIDC_FAMILY_NAME_FIELD: "family_name",
209
170
  OIDC_GIVEN_NAME_FIELD: "given_name",
210
171
  OIDC_END_SESSION_ENDPOINT: "",
211
- OIDC_POST_LOGOUT_REDIRECT_URI: "",
212
- // Where to land after the provider has logged the user out (RP-Initiated Logout)
213
- OIDC_ISSUER: "",
214
- // Provider issuer URL — used to validate iss claim in backchannel logout tokens
215
- OIDC_JWKS_URI: "",
216
- // Provider JWKS endpoint — required for backchannel logout token signature verification
217
172
  OIDC_SSO_BUTTON_TEXT: "Login via SSO",
218
173
  OIDC_ENFORCE: null
219
174
  // null = use DB setting; true/false = override DB (useful for lockout recovery)
@@ -266,9 +221,8 @@ function getExpiredCookieOptions(strapi2, ctx) {
266
221
  function clearAuthCookies(strapi2, ctx) {
267
222
  const options2 = getExpiredCookieOptions(strapi2, ctx);
268
223
  ctx.cookies.set("strapi_admin_refresh", "", options2);
269
- const rootPathOptions = { ...options2, path: "/" };
270
- ctx.cookies.set("oidc_authenticated", "", rootPathOptions);
271
- ctx.cookies.set("oidc_id_token", "", rootPathOptions);
224
+ ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
225
+ ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
272
226
  }
273
227
  const REQUIRED_CONFIG_KEYS = [
274
228
  "OIDC_CLIENT_ID",
@@ -351,7 +305,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
351
305
  throw new Error("Failed to fetch user info");
352
306
  }
353
307
  const userInfo = await userResponse.json();
354
- return { userInfo, idToken: tokenData.id_token };
308
+ return { userInfo, accessToken: tokenData.access_token };
355
309
  }
356
310
  async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
357
311
  let roles2 = [];
@@ -417,29 +371,28 @@ async function oidcSignInCallback(ctx) {
417
371
  params.append("grant_type", config2.OIDC_GRANT_TYPE);
418
372
  params.append("code_verifier", codeVerifier ?? "");
419
373
  try {
420
- const { userInfo: userResponseData, idToken } = await exchangeTokenAndFetchUserInfo(
374
+ const { userInfo, accessToken } = await exchangeTokenAndFetchUserInfo(
421
375
  config2,
422
376
  params,
423
377
  oidcNonce ?? ""
424
378
  );
379
+ const isProduction = strapi.config.get("environment") === "production";
380
+ ctx.cookies.set("oidc_access_token", accessToken, {
381
+ httpOnly: true,
382
+ maxAge: 3e5,
383
+ // 5 minutes — matches typical provider access token lifetime
384
+ secure: isProduction && ctx.request.secure,
385
+ sameSite: "lax"
386
+ });
425
387
  const { activateUser, jwtToken } = await handleUserAuthentication(
426
388
  userService,
427
389
  oauthService2,
428
390
  roleService2,
429
391
  whitelistService2,
430
- userResponseData,
392
+ userInfo,
431
393
  config2,
432
394
  ctx
433
395
  );
434
- if (idToken) {
435
- const isProduction = strapi.config.get("environment") === "production";
436
- ctx.cookies.set("oidc_id_token", idToken, {
437
- httpOnly: true,
438
- secure: isProduction && ctx.request.secure,
439
- path: "/",
440
- sameSite: "lax"
441
- });
442
- }
443
396
  const nonce = node_crypto.randomUUID();
444
397
  const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
445
398
  ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
@@ -452,91 +405,31 @@ async function oidcSignInCallback(ctx) {
452
405
  async function logout(ctx) {
453
406
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
454
407
  const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
408
+ const adminPanelUrl = strapi.config.get("admin.url", "/admin");
455
409
  const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
456
- const idToken = ctx.cookies.get("oidc_id_token");
410
+ const accessToken = ctx.cookies.get("oidc_access_token");
457
411
  clearAuthCookies(strapi, ctx);
458
- if (logoutUrl && isOidcSession) {
459
- const url = new URL(logoutUrl);
460
- if (idToken) url.searchParams.set("id_token_hint", idToken);
461
- if (config2.OIDC_POST_LOGOUT_REDIRECT_URI) {
462
- url.searchParams.set("post_logout_redirect_uri", config2.OIDC_POST_LOGOUT_REDIRECT_URI);
463
- }
464
- ctx.redirect(url.toString());
465
- } else {
466
- const adminPanelUrl = strapi.config.get("admin.url", "/admin");
467
- ctx.redirect(`${adminPanelUrl}/auth/login`);
468
- }
469
- }
470
- const jwksCache = /* @__PURE__ */ new Map();
471
- function getJWKS(uri) {
472
- const cached = jwksCache.get(uri);
473
- if (cached) return cached;
474
- const jwks = jose.createRemoteJWKSet(new URL(uri));
475
- jwksCache.set(uri, jwks);
476
- return jwks;
477
- }
478
- async function backchannelLogout(ctx) {
479
- const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
480
- const logoutToken = ctx.request.body?.logout_token;
481
- if (!logoutToken) {
482
- ctx.status = 400;
483
- ctx.body = { error: "Missing logout_token" };
484
- return;
485
- }
486
- if (!config2.OIDC_JWKS_URI || !config2.OIDC_ISSUER) {
487
- ctx.status = 501;
488
- ctx.body = {
489
- error: "OIDC_JWKS_URI and OIDC_ISSUER must both be configured to enable backchannel logout"
490
- };
491
- return;
492
- }
493
- try {
494
- const JWKS = getJWKS(config2.OIDC_JWKS_URI);
495
- const verifyOptions = {
496
- issuer: config2.OIDC_ISSUER,
497
- audience: config2.OIDC_CLIENT_ID || void 0
498
- };
499
- const { payload } = await jose.jwtVerify(logoutToken, JWKS, verifyOptions);
500
- if ("nonce" in payload) {
501
- ctx.status = 400;
502
- ctx.body = { error: "logout_token must not contain nonce" };
503
- return;
504
- }
505
- const events = payload.events;
506
- if (!events?.["http://schemas.openid.net/event/backchannel-logout"]) {
507
- ctx.status = 400;
508
- ctx.body = { error: "logout_token missing backchannel-logout event" };
509
- return;
510
- }
511
- if (!payload.sub && !("sid" in payload)) {
512
- ctx.status = 400;
513
- ctx.body = { error: "logout_token must contain sub or sid" };
514
- return;
515
- }
516
- if (payload.sub) {
517
- const userService = strapi.service("admin::user");
518
- const user = await userService.findOneByEmail(payload.sub);
519
- if (user) {
520
- revokeUser(String(user.id));
521
- const sessionManager = strapi.sessionManager;
522
- if (sessionManager) {
523
- await sessionManager("admin").invalidateRefreshToken(String(user.id));
524
- }
412
+ if (logoutUrl && isOidcSession && accessToken) {
413
+ try {
414
+ const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
415
+ headers: { Authorization: `Bearer ${accessToken}` }
416
+ });
417
+ if (response.ok) {
418
+ return ctx.redirect(logoutUrl);
525
419
  }
420
+ } catch {
526
421
  }
527
- ctx.status = 200;
528
- ctx.body = "";
529
- } catch (e) {
530
- strapi.log.error("Backchannel logout failed:", e);
531
- ctx.status = 400;
532
- ctx.body = { error: "Invalid logout_token" };
422
+ return ctx.redirect(`${adminPanelUrl}/auth/login`);
423
+ }
424
+ if (logoutUrl && isOidcSession) {
425
+ return ctx.redirect(logoutUrl);
533
426
  }
427
+ ctx.redirect(`${adminPanelUrl}/auth/login`);
534
428
  }
535
429
  const oidc = {
536
430
  oidcSignIn,
537
431
  oidcSignInCallback,
538
- logout,
539
- backchannelLogout
432
+ logout
540
433
  };
541
434
  async function find(ctx) {
542
435
  const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
@@ -793,12 +686,6 @@ const routes = {
793
686
  handler: "oidc.logout",
794
687
  config: { auth: false }
795
688
  },
796
- {
797
- method: "POST",
798
- path: "/logout",
799
- handler: "oidc.backchannelLogout",
800
- config: { auth: false }
801
- },
802
689
  {
803
690
  method: "GET",
804
691
  path: "/whitelist",
@@ -1,5 +1,4 @@
1
1
  import { randomUUID, randomBytes } from "node:crypto";
2
- import { jwtVerify, createRemoteJWKSet } from "jose";
3
2
  import pkceChallenge from "pkce-challenge";
4
3
  import strapiUtils from "@strapi/utils";
5
4
  import generator from "generate-password";
@@ -19,17 +18,6 @@ function resolveEnforceOIDC(strapi2, dbValue) {
19
18
  if (configValue !== null) return configValue;
20
19
  return dbValue ?? false;
21
20
  }
22
- const revoked = /* @__PURE__ */ new Map();
23
- const CLEANUP_DELAY_MS = 24 * 60 * 60 * 1e3;
24
- function revokeUser(userId) {
25
- revoked.set(userId, Date.now());
26
- setTimeout(() => revoked.delete(userId), CLEANUP_DELAY_MS);
27
- }
28
- function isUserRevoked(userId, iat) {
29
- const logoutTime = revoked.get(userId);
30
- if (!logoutTime) return false;
31
- return iat * 1e3 <= logoutTime;
32
- }
33
21
  async function bootstrap({ strapi: strapi2 }) {
34
22
  const enforceOidcMiddleware = async (ctx, next) => {
35
23
  const adminUrl = strapi2.config.get("admin.url", "/admin");
@@ -82,36 +70,9 @@ async function bootstrap({ strapi: strapi2 }) {
82
70
  }
83
71
  await next();
84
72
  };
85
- const denylistMiddleware = async (ctx, next) => {
86
- const auth = ctx.request.headers.authorization;
87
- if (auth?.startsWith("Bearer ")) {
88
- try {
89
- const payload = JSON.parse(
90
- Buffer.from(auth.slice(7).split(".")[1], "base64url").toString()
91
- );
92
- if (payload.id && isUserRevoked(String(payload.id), payload.iat ?? 0)) {
93
- ctx.status = 401;
94
- ctx.body = {
95
- data: null,
96
- error: {
97
- status: 401,
98
- name: "UnauthorizedError",
99
- message: "Session revoked",
100
- details: {}
101
- }
102
- };
103
- return;
104
- }
105
- } catch {
106
- }
107
- }
108
- await next();
109
- };
110
73
  if (strapi2.server.app && Array.isArray(strapi2.server.app.middleware)) {
111
74
  strapi2.server.app.middleware.unshift(enforceOidcMiddleware);
112
- strapi2.server.app.middleware.unshift(denylistMiddleware);
113
75
  } else {
114
- strapi2.server.use(denylistMiddleware);
115
76
  strapi2.server.use(enforceOidcMiddleware);
116
77
  }
117
78
  const actions = [
@@ -202,12 +163,6 @@ const config = {
202
163
  OIDC_FAMILY_NAME_FIELD: "family_name",
203
164
  OIDC_GIVEN_NAME_FIELD: "given_name",
204
165
  OIDC_END_SESSION_ENDPOINT: "",
205
- OIDC_POST_LOGOUT_REDIRECT_URI: "",
206
- // Where to land after the provider has logged the user out (RP-Initiated Logout)
207
- OIDC_ISSUER: "",
208
- // Provider issuer URL — used to validate iss claim in backchannel logout tokens
209
- OIDC_JWKS_URI: "",
210
- // Provider JWKS endpoint — required for backchannel logout token signature verification
211
166
  OIDC_SSO_BUTTON_TEXT: "Login via SSO",
212
167
  OIDC_ENFORCE: null
213
168
  // null = use DB setting; true/false = override DB (useful for lockout recovery)
@@ -260,9 +215,8 @@ function getExpiredCookieOptions(strapi2, ctx) {
260
215
  function clearAuthCookies(strapi2, ctx) {
261
216
  const options2 = getExpiredCookieOptions(strapi2, ctx);
262
217
  ctx.cookies.set("strapi_admin_refresh", "", options2);
263
- const rootPathOptions = { ...options2, path: "/" };
264
- ctx.cookies.set("oidc_authenticated", "", rootPathOptions);
265
- ctx.cookies.set("oidc_id_token", "", rootPathOptions);
218
+ ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
219
+ ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
266
220
  }
267
221
  const REQUIRED_CONFIG_KEYS = [
268
222
  "OIDC_CLIENT_ID",
@@ -345,7 +299,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
345
299
  throw new Error("Failed to fetch user info");
346
300
  }
347
301
  const userInfo = await userResponse.json();
348
- return { userInfo, idToken: tokenData.id_token };
302
+ return { userInfo, accessToken: tokenData.access_token };
349
303
  }
350
304
  async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
351
305
  let roles2 = [];
@@ -411,29 +365,28 @@ async function oidcSignInCallback(ctx) {
411
365
  params.append("grant_type", config2.OIDC_GRANT_TYPE);
412
366
  params.append("code_verifier", codeVerifier ?? "");
413
367
  try {
414
- const { userInfo: userResponseData, idToken } = await exchangeTokenAndFetchUserInfo(
368
+ const { userInfo, accessToken } = await exchangeTokenAndFetchUserInfo(
415
369
  config2,
416
370
  params,
417
371
  oidcNonce ?? ""
418
372
  );
373
+ const isProduction = strapi.config.get("environment") === "production";
374
+ ctx.cookies.set("oidc_access_token", accessToken, {
375
+ httpOnly: true,
376
+ maxAge: 3e5,
377
+ // 5 minutes — matches typical provider access token lifetime
378
+ secure: isProduction && ctx.request.secure,
379
+ sameSite: "lax"
380
+ });
419
381
  const { activateUser, jwtToken } = await handleUserAuthentication(
420
382
  userService,
421
383
  oauthService2,
422
384
  roleService2,
423
385
  whitelistService2,
424
- userResponseData,
386
+ userInfo,
425
387
  config2,
426
388
  ctx
427
389
  );
428
- if (idToken) {
429
- const isProduction = strapi.config.get("environment") === "production";
430
- ctx.cookies.set("oidc_id_token", idToken, {
431
- httpOnly: true,
432
- secure: isProduction && ctx.request.secure,
433
- path: "/",
434
- sameSite: "lax"
435
- });
436
- }
437
390
  const nonce = randomUUID();
438
391
  const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
439
392
  ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
@@ -446,91 +399,31 @@ async function oidcSignInCallback(ctx) {
446
399
  async function logout(ctx) {
447
400
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
448
401
  const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
402
+ const adminPanelUrl = strapi.config.get("admin.url", "/admin");
449
403
  const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
450
- const idToken = ctx.cookies.get("oidc_id_token");
404
+ const accessToken = ctx.cookies.get("oidc_access_token");
451
405
  clearAuthCookies(strapi, ctx);
452
- if (logoutUrl && isOidcSession) {
453
- const url = new URL(logoutUrl);
454
- if (idToken) url.searchParams.set("id_token_hint", idToken);
455
- if (config2.OIDC_POST_LOGOUT_REDIRECT_URI) {
456
- url.searchParams.set("post_logout_redirect_uri", config2.OIDC_POST_LOGOUT_REDIRECT_URI);
457
- }
458
- ctx.redirect(url.toString());
459
- } else {
460
- const adminPanelUrl = strapi.config.get("admin.url", "/admin");
461
- ctx.redirect(`${adminPanelUrl}/auth/login`);
462
- }
463
- }
464
- const jwksCache = /* @__PURE__ */ new Map();
465
- function getJWKS(uri) {
466
- const cached = jwksCache.get(uri);
467
- if (cached) return cached;
468
- const jwks = createRemoteJWKSet(new URL(uri));
469
- jwksCache.set(uri, jwks);
470
- return jwks;
471
- }
472
- async function backchannelLogout(ctx) {
473
- const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
474
- const logoutToken = ctx.request.body?.logout_token;
475
- if (!logoutToken) {
476
- ctx.status = 400;
477
- ctx.body = { error: "Missing logout_token" };
478
- return;
479
- }
480
- if (!config2.OIDC_JWKS_URI || !config2.OIDC_ISSUER) {
481
- ctx.status = 501;
482
- ctx.body = {
483
- error: "OIDC_JWKS_URI and OIDC_ISSUER must both be configured to enable backchannel logout"
484
- };
485
- return;
486
- }
487
- try {
488
- const JWKS = getJWKS(config2.OIDC_JWKS_URI);
489
- const verifyOptions = {
490
- issuer: config2.OIDC_ISSUER,
491
- audience: config2.OIDC_CLIENT_ID || void 0
492
- };
493
- const { payload } = await jwtVerify(logoutToken, JWKS, verifyOptions);
494
- if ("nonce" in payload) {
495
- ctx.status = 400;
496
- ctx.body = { error: "logout_token must not contain nonce" };
497
- return;
498
- }
499
- const events = payload.events;
500
- if (!events?.["http://schemas.openid.net/event/backchannel-logout"]) {
501
- ctx.status = 400;
502
- ctx.body = { error: "logout_token missing backchannel-logout event" };
503
- return;
504
- }
505
- if (!payload.sub && !("sid" in payload)) {
506
- ctx.status = 400;
507
- ctx.body = { error: "logout_token must contain sub or sid" };
508
- return;
509
- }
510
- if (payload.sub) {
511
- const userService = strapi.service("admin::user");
512
- const user = await userService.findOneByEmail(payload.sub);
513
- if (user) {
514
- revokeUser(String(user.id));
515
- const sessionManager = strapi.sessionManager;
516
- if (sessionManager) {
517
- await sessionManager("admin").invalidateRefreshToken(String(user.id));
518
- }
406
+ if (logoutUrl && isOidcSession && accessToken) {
407
+ try {
408
+ const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
409
+ headers: { Authorization: `Bearer ${accessToken}` }
410
+ });
411
+ if (response.ok) {
412
+ return ctx.redirect(logoutUrl);
519
413
  }
414
+ } catch {
520
415
  }
521
- ctx.status = 200;
522
- ctx.body = "";
523
- } catch (e) {
524
- strapi.log.error("Backchannel logout failed:", e);
525
- ctx.status = 400;
526
- ctx.body = { error: "Invalid logout_token" };
416
+ return ctx.redirect(`${adminPanelUrl}/auth/login`);
417
+ }
418
+ if (logoutUrl && isOidcSession) {
419
+ return ctx.redirect(logoutUrl);
527
420
  }
421
+ ctx.redirect(`${adminPanelUrl}/auth/login`);
528
422
  }
529
423
  const oidc = {
530
424
  oidcSignIn,
531
425
  oidcSignInCallback,
532
- logout,
533
- backchannelLogout
426
+ logout
534
427
  };
535
428
  async function find(ctx) {
536
429
  const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
@@ -787,12 +680,6 @@ const routes = {
787
680
  handler: "oidc.logout",
788
681
  config: { auth: false }
789
682
  },
790
- {
791
- method: "POST",
792
- path: "/logout",
793
- handler: "oidc.backchannelLogout",
794
- config: { auth: false }
795
- },
796
683
  {
797
684
  method: "GET",
798
685
  path: "/whitelist",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-oidc",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
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",
@@ -50,7 +50,6 @@
50
50
  "@strapi/icons": "^2.2.0",
51
51
  "@strapi/utils": "^5.41.1",
52
52
  "generate-password": "^1.7.1",
53
- "jose": "^6.2.2",
54
53
  "pkce-challenge": "^6.0.0",
55
54
  "react-intl": "^6.8.9"
56
55
  },
@@ -82,8 +81,6 @@
82
81
  "@eslint/eslintrc": "^3.3.5",
83
82
  "@eslint/js": "^10.0.1",
84
83
  "@strapi/sdk-plugin": "^6.0.1",
85
- "@strapi/types": "^5.41.1",
86
- "@types/koa": "^2.16.4",
87
84
  "@types/node": "^25.5.2",
88
85
  "@types/supertest": "^7.2.0",
89
86
  "@vitest/coverage-v8": "^4.1.2",