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 +20 -19
- package/dist/server/index.js +39 -0
- package/dist/server/index.mjs +39 -0
- package/package.json +1 -1
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
|
|
39
|
+
// Optional
|
|
40
40
|
OIDC_SCOPE: 'openid profile email',
|
|
41
41
|
OIDC_GRANT_TYPE: 'authorization_code',
|
|
42
|
-
OIDC_FAMILY_NAME_FIELD: 'family_name',
|
|
43
|
-
OIDC_GIVEN_NAME_FIELD: 'given_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
|
|
46
|
-
REMEMBER_ME: false,
|
|
45
|
+
OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override
|
|
46
|
+
REMEMBER_ME: false,
|
|
47
47
|
|
|
48
|
-
// Optional — RP-Initiated Logout
|
|
49
|
-
//
|
|
50
|
-
//
|
|
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', ''),
|
|
54
|
-
|
|
55
|
-
// Optional — Backchannel Logout
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
package/dist/server/index.js
CHANGED
|
@@ -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));
|
package/dist/server/index.mjs
CHANGED
|
@@ -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