strapi-plugin-oidc 1.5.0 → 1.5.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
@@ -36,29 +36,27 @@ 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 — defaults shown
39
+ // Optional
40
40
  OIDC_SCOPE: 'openid profile email',
41
41
  OIDC_GRANT_TYPE: 'authorization_code',
42
- OIDC_FAMILY_NAME_FIELD: 'family_name', // claim name for the user's last name
43
- OIDC_GIVEN_NAME_FIELD: 'given_name', // claim name for the user's first name
42
+ OIDC_FAMILY_NAME_FIELD: 'family_name',
43
+ OIDC_GIVEN_NAME_FIELD: 'given_name',
44
44
  OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
45
- OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
46
- REMEMBER_ME: false, // Persist session across browser restarts
45
+ OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override
46
+ REMEMBER_ME: false,
47
47
 
48
- // Optional — RP-Initiated Logout (GET /strapi-plugin-oidc/logout)
49
- // Set OIDC_END_SESSION_ENDPOINT to redirect OIDC sessions to your provider's
50
- // end-session page on logout. Without it, logout only clears the local session.
51
- // Find this URL in your provider's /.well-known/openid-configuration as end_session_endpoint.
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.
52
51
  OIDC_END_SESSION_ENDPOINT: env('OIDC_END_SESSION_ENDPOINT', ''),
53
- OIDC_POST_LOGOUT_REDIRECT_URI: env('OIDC_POST_LOGOUT_REDIRECT_URI', ''), // where to land after the provider logs the user out
54
-
55
- // Optional — Backchannel Logout (POST /strapi-plugin-oidc/logout)
56
- // When configured, your provider can notify Strapi when a user logs out elsewhere
57
- // (e.g. from another app or directly from the provider UI), revoking their Strapi session.
58
- // Set the logout URI in your provider to: https://your-strapi.com/strapi-plugin-oidc/logout
59
- // Both values are required together — find them in your provider's /.well-known/openid-configuration.
60
- OIDC_ISSUER: env('OIDC_ISSUER', ''), // validates the iss claim; required for backchannel logout
61
- OIDC_JWKS_URI: env('OIDC_JWKS_URI', ''), // verifies logout token signatures; required for backchannel logout
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', ''),
62
60
  },
63
61
  },
64
62
  });
@@ -148,7 +146,6 @@ This plugin is a hard fork of [`strapi-plugin-sso`](https://github.com/yasudaclo
148
146
 
149
147
  - Removed alternative SSO methods to simplify the plugin.
150
148
  - Redesigned the Whitelist and Role management UI (switched to native Strapi cards, added pagination, etc.).
151
- - Added an OIDC logout redirect URL.
152
149
  - Added an option to "Enforce OIDC login" with an admin toggle (automatically disabled if the whitelist is empty).
153
150
  - Migrated the testing framework to Vitest and added comprehensive test coverage for controllers and services.
154
151
  - Cleaned up dead code and unused dependencies to improve maintainability.
@@ -160,4 +157,8 @@ This plugin is a hard fork of [`strapi-plugin-sso`](https://github.com/yasudaclo
160
157
  - Bulk delete all entries with a confirmation dialog.
161
158
  - Unsaved changes confirmation when navigating away from the settings page.
162
159
  - 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.
163
164
  - Added misc. quality of life improvements and bug fixes.
@@ -25,6 +25,17 @@ function resolveEnforceOIDC(strapi2, dbValue) {
25
25
  if (configValue !== null) return configValue;
26
26
  return dbValue ?? false;
27
27
  }
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
+ }
28
39
  async function bootstrap({ strapi: strapi2 }) {
29
40
  const enforceOidcMiddleware = async (ctx, next) => {
30
41
  const adminUrl = strapi2.config.get("admin.url", "/admin");
@@ -77,9 +88,36 @@ async function bootstrap({ strapi: strapi2 }) {
77
88
  }
78
89
  await next();
79
90
  };
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
+ };
80
116
  if (strapi2.server.app && Array.isArray(strapi2.server.app.middleware)) {
81
117
  strapi2.server.app.middleware.unshift(enforceOidcMiddleware);
118
+ strapi2.server.app.middleware.unshift(denylistMiddleware);
82
119
  } else {
120
+ strapi2.server.use(denylistMiddleware);
83
121
  strapi2.server.use(enforceOidcMiddleware);
84
122
  }
85
123
  const actions = [
@@ -479,6 +517,7 @@ async function backchannelLogout(ctx) {
479
517
  const userService = strapi.service("admin::user");
480
518
  const user = await userService.findOneByEmail(payload.sub);
481
519
  if (user) {
520
+ revokeUser(String(user.id));
482
521
  const sessionManager = strapi.sessionManager;
483
522
  if (sessionManager) {
484
523
  await sessionManager("admin").invalidateRefreshToken(String(user.id));
@@ -19,6 +19,17 @@ function resolveEnforceOIDC(strapi2, dbValue) {
19
19
  if (configValue !== null) return configValue;
20
20
  return dbValue ?? false;
21
21
  }
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
+ }
22
33
  async function bootstrap({ strapi: strapi2 }) {
23
34
  const enforceOidcMiddleware = async (ctx, next) => {
24
35
  const adminUrl = strapi2.config.get("admin.url", "/admin");
@@ -71,9 +82,36 @@ async function bootstrap({ strapi: strapi2 }) {
71
82
  }
72
83
  await next();
73
84
  };
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
+ };
74
110
  if (strapi2.server.app && Array.isArray(strapi2.server.app.middleware)) {
75
111
  strapi2.server.app.middleware.unshift(enforceOidcMiddleware);
112
+ strapi2.server.app.middleware.unshift(denylistMiddleware);
76
113
  } else {
114
+ strapi2.server.use(denylistMiddleware);
77
115
  strapi2.server.use(enforceOidcMiddleware);
78
116
  }
79
117
  const actions = [
@@ -473,6 +511,7 @@ async function backchannelLogout(ctx) {
473
511
  const userService = strapi.service("admin::user");
474
512
  const user = await userService.findOneByEmail(payload.sub);
475
513
  if (user) {
514
+ revokeUser(String(user.id));
476
515
  const sessionManager = strapi.sessionManager;
477
516
  if (sessionManager) {
478
517
  await sessionManager("admin").invalidateRefreshToken(String(user.id));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-oidc",
3
- "version": "1.5.0",
3
+ "version": "1.5.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",