strapi-security-suite 0.3.1 → 0.3.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/dist/server/index.js +52 -38
- package/dist/server/index.mjs +52 -38
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -39,7 +39,9 @@ const COOKIES = {
|
|
|
39
39
|
SESSION: "koa.sess",
|
|
40
40
|
SESSION_SIG: "koa.sess.sig",
|
|
41
41
|
/** Strapi v5 admin refresh-token cookie (managed by session manager). */
|
|
42
|
-
REFRESH_TOKEN: "strapi_admin_refresh"
|
|
42
|
+
REFRESH_TOKEN: "strapi_admin_refresh",
|
|
43
|
+
/** JWT access-token cookie set by Strapi EE SSO authentication flow. */
|
|
44
|
+
JWT_TOKEN: "jwtToken"
|
|
43
45
|
};
|
|
44
46
|
const HEADERS = {
|
|
45
47
|
/** Header that signals the frontend to force-reload (session revoked). */
|
|
@@ -69,6 +71,52 @@ const DEFAULT_SETTINGS = {
|
|
|
69
71
|
enablePasswordManagement: true
|
|
70
72
|
};
|
|
71
73
|
const VALID_SETTINGS_KEYS = new Set(Object.keys(DEFAULT_SETTINGS));
|
|
74
|
+
class PluginError extends Error {
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} message - Internal message (for logs)
|
|
77
|
+
* @param {string} sanitizedMessage - Safe message for the client
|
|
78
|
+
* @param {number} [statusCode=400] - HTTP status code
|
|
79
|
+
*/
|
|
80
|
+
constructor(message, sanitizedMessage, statusCode = HTTP_STATUS.BAD_REQUEST) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.name = "PluginError";
|
|
83
|
+
this.sanitizedMessage = sanitizedMessage;
|
|
84
|
+
this.statusCode = statusCode;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
class ValidationError extends PluginError {
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} message - Internal message
|
|
90
|
+
* @param {string} [sanitizedMessage='Validation failed.'] - Client-safe message
|
|
91
|
+
*/
|
|
92
|
+
constructor(message, sanitizedMessage = "Validation failed.") {
|
|
93
|
+
super(message, sanitizedMessage, HTTP_STATUS.BAD_REQUEST);
|
|
94
|
+
this.name = "ValidationError";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function clearSessionCookies(ctx) {
|
|
98
|
+
if (ctx.session !== void 0) {
|
|
99
|
+
ctx.session = null;
|
|
100
|
+
}
|
|
101
|
+
const expireOpts = { expires: /* @__PURE__ */ new Date(0), path: "/", httpOnly: true };
|
|
102
|
+
ctx.cookies.set(COOKIES.SESSION, "", expireOpts);
|
|
103
|
+
ctx.cookies.set(COOKIES.SESSION_SIG, "", expireOpts);
|
|
104
|
+
ctx.cookies.set(COOKIES.REFRESH_TOKEN, "", {
|
|
105
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
106
|
+
path: "/admin",
|
|
107
|
+
httpOnly: true
|
|
108
|
+
});
|
|
109
|
+
const configuredSecure = strapi.config.get("admin.auth.cookie.secure");
|
|
110
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
111
|
+
const jwtClearOpts = {
|
|
112
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
113
|
+
httpOnly: false,
|
|
114
|
+
secure: typeof configuredSecure === "boolean" ? configuredSecure : isProduction,
|
|
115
|
+
domain: strapi.config.get("admin.auth.domain"),
|
|
116
|
+
overwrite: true
|
|
117
|
+
};
|
|
118
|
+
ctx.cookies.set(COOKIES.JWT_TOKEN, "", jwtClearOpts);
|
|
119
|
+
}
|
|
72
120
|
async function trackActivity(ctx, next) {
|
|
73
121
|
const adminUser = ctx.state[CTX_ADMIN_USER];
|
|
74
122
|
let key = adminUser?.id ? `${adminUser.id}:${adminUser.email}` : null;
|
|
@@ -85,9 +133,7 @@ async function trackActivity(ctx, next) {
|
|
|
85
133
|
return;
|
|
86
134
|
}
|
|
87
135
|
if (ctx.path.includes(LOGOUT_PATH)) {
|
|
88
|
-
|
|
89
|
-
ctx.session = null;
|
|
90
|
-
}
|
|
136
|
+
clearSessionCookies(ctx);
|
|
91
137
|
key = null;
|
|
92
138
|
}
|
|
93
139
|
if (key) {
|
|
@@ -158,14 +204,7 @@ async function rejectRevokedTokens(ctx, next) {
|
|
|
158
204
|
if (adminEmail && revokedTokenSet.has(adminEmail)) {
|
|
159
205
|
ctx.set(HEADERS.ADMIN_TOKEN_SIGNAL, adminEmail);
|
|
160
206
|
ctx.set(HEADERS.EXPOSE_HEADERS, HEADERS.ADMIN_TOKEN_SIGNAL);
|
|
161
|
-
|
|
162
|
-
ctx.session = null;
|
|
163
|
-
}
|
|
164
|
-
ctx.cookies.set(COOKIES.REFRESH_TOKEN, "", {
|
|
165
|
-
expires: /* @__PURE__ */ new Date(0),
|
|
166
|
-
path: "/admin",
|
|
167
|
-
httpOnly: true
|
|
168
|
-
});
|
|
207
|
+
clearSessionCookies(ctx);
|
|
169
208
|
const bearerToken = ctx.get("authorization")?.split("Bearer ")[1];
|
|
170
209
|
if (bearerToken) {
|
|
171
210
|
revokedConnectionTokens.set(bearerToken, Date.now());
|
|
@@ -199,9 +238,7 @@ async function interceptRenewToken(ctx, next) {
|
|
|
199
238
|
if (bearerToken) {
|
|
200
239
|
revokedConnectionTokens.set(bearerToken, Date.now());
|
|
201
240
|
}
|
|
202
|
-
|
|
203
|
-
ctx.session = null;
|
|
204
|
-
}
|
|
241
|
+
clearSessionCookies(ctx);
|
|
205
242
|
sessionActivityMap.delete(`${adminUser.id}:${adminUser.email}`);
|
|
206
243
|
}
|
|
207
244
|
await next();
|
|
@@ -381,29 +418,6 @@ const securitySettings = {
|
|
|
381
418
|
const contentTypes = {
|
|
382
419
|
"security-settings": securitySettings
|
|
383
420
|
};
|
|
384
|
-
class PluginError extends Error {
|
|
385
|
-
/**
|
|
386
|
-
* @param {string} message - Internal message (for logs)
|
|
387
|
-
* @param {string} sanitizedMessage - Safe message for the client
|
|
388
|
-
* @param {number} [statusCode=400] - HTTP status code
|
|
389
|
-
*/
|
|
390
|
-
constructor(message, sanitizedMessage, statusCode = HTTP_STATUS.BAD_REQUEST) {
|
|
391
|
-
super(message);
|
|
392
|
-
this.name = "PluginError";
|
|
393
|
-
this.sanitizedMessage = sanitizedMessage;
|
|
394
|
-
this.statusCode = statusCode;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
class ValidationError extends PluginError {
|
|
398
|
-
/**
|
|
399
|
-
* @param {string} message - Internal message
|
|
400
|
-
* @param {string} [sanitizedMessage='Validation failed.'] - Client-safe message
|
|
401
|
-
*/
|
|
402
|
-
constructor(message, sanitizedMessage = "Validation failed.") {
|
|
403
|
-
super(message, sanitizedMessage, HTTP_STATUS.BAD_REQUEST);
|
|
404
|
-
this.name = "ValidationError";
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
421
|
const validateSettingsPayload = (body) => {
|
|
408
422
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
409
423
|
throw new ValidationError(
|
package/dist/server/index.mjs
CHANGED
|
@@ -36,7 +36,9 @@ const COOKIES = {
|
|
|
36
36
|
SESSION: "koa.sess",
|
|
37
37
|
SESSION_SIG: "koa.sess.sig",
|
|
38
38
|
/** Strapi v5 admin refresh-token cookie (managed by session manager). */
|
|
39
|
-
REFRESH_TOKEN: "strapi_admin_refresh"
|
|
39
|
+
REFRESH_TOKEN: "strapi_admin_refresh",
|
|
40
|
+
/** JWT access-token cookie set by Strapi EE SSO authentication flow. */
|
|
41
|
+
JWT_TOKEN: "jwtToken"
|
|
40
42
|
};
|
|
41
43
|
const HEADERS = {
|
|
42
44
|
/** Header that signals the frontend to force-reload (session revoked). */
|
|
@@ -66,6 +68,52 @@ const DEFAULT_SETTINGS = {
|
|
|
66
68
|
enablePasswordManagement: true
|
|
67
69
|
};
|
|
68
70
|
const VALID_SETTINGS_KEYS = new Set(Object.keys(DEFAULT_SETTINGS));
|
|
71
|
+
class PluginError extends Error {
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} message - Internal message (for logs)
|
|
74
|
+
* @param {string} sanitizedMessage - Safe message for the client
|
|
75
|
+
* @param {number} [statusCode=400] - HTTP status code
|
|
76
|
+
*/
|
|
77
|
+
constructor(message, sanitizedMessage, statusCode = HTTP_STATUS.BAD_REQUEST) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "PluginError";
|
|
80
|
+
this.sanitizedMessage = sanitizedMessage;
|
|
81
|
+
this.statusCode = statusCode;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
class ValidationError extends PluginError {
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} message - Internal message
|
|
87
|
+
* @param {string} [sanitizedMessage='Validation failed.'] - Client-safe message
|
|
88
|
+
*/
|
|
89
|
+
constructor(message, sanitizedMessage = "Validation failed.") {
|
|
90
|
+
super(message, sanitizedMessage, HTTP_STATUS.BAD_REQUEST);
|
|
91
|
+
this.name = "ValidationError";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function clearSessionCookies(ctx) {
|
|
95
|
+
if (ctx.session !== void 0) {
|
|
96
|
+
ctx.session = null;
|
|
97
|
+
}
|
|
98
|
+
const expireOpts = { expires: /* @__PURE__ */ new Date(0), path: "/", httpOnly: true };
|
|
99
|
+
ctx.cookies.set(COOKIES.SESSION, "", expireOpts);
|
|
100
|
+
ctx.cookies.set(COOKIES.SESSION_SIG, "", expireOpts);
|
|
101
|
+
ctx.cookies.set(COOKIES.REFRESH_TOKEN, "", {
|
|
102
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
103
|
+
path: "/admin",
|
|
104
|
+
httpOnly: true
|
|
105
|
+
});
|
|
106
|
+
const configuredSecure = strapi.config.get("admin.auth.cookie.secure");
|
|
107
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
108
|
+
const jwtClearOpts = {
|
|
109
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
110
|
+
httpOnly: false,
|
|
111
|
+
secure: typeof configuredSecure === "boolean" ? configuredSecure : isProduction,
|
|
112
|
+
domain: strapi.config.get("admin.auth.domain"),
|
|
113
|
+
overwrite: true
|
|
114
|
+
};
|
|
115
|
+
ctx.cookies.set(COOKIES.JWT_TOKEN, "", jwtClearOpts);
|
|
116
|
+
}
|
|
69
117
|
async function trackActivity(ctx, next) {
|
|
70
118
|
const adminUser = ctx.state[CTX_ADMIN_USER];
|
|
71
119
|
let key = adminUser?.id ? `${adminUser.id}:${adminUser.email}` : null;
|
|
@@ -82,9 +130,7 @@ async function trackActivity(ctx, next) {
|
|
|
82
130
|
return;
|
|
83
131
|
}
|
|
84
132
|
if (ctx.path.includes(LOGOUT_PATH)) {
|
|
85
|
-
|
|
86
|
-
ctx.session = null;
|
|
87
|
-
}
|
|
133
|
+
clearSessionCookies(ctx);
|
|
88
134
|
key = null;
|
|
89
135
|
}
|
|
90
136
|
if (key) {
|
|
@@ -155,14 +201,7 @@ async function rejectRevokedTokens(ctx, next) {
|
|
|
155
201
|
if (adminEmail && revokedTokenSet.has(adminEmail)) {
|
|
156
202
|
ctx.set(HEADERS.ADMIN_TOKEN_SIGNAL, adminEmail);
|
|
157
203
|
ctx.set(HEADERS.EXPOSE_HEADERS, HEADERS.ADMIN_TOKEN_SIGNAL);
|
|
158
|
-
|
|
159
|
-
ctx.session = null;
|
|
160
|
-
}
|
|
161
|
-
ctx.cookies.set(COOKIES.REFRESH_TOKEN, "", {
|
|
162
|
-
expires: /* @__PURE__ */ new Date(0),
|
|
163
|
-
path: "/admin",
|
|
164
|
-
httpOnly: true
|
|
165
|
-
});
|
|
204
|
+
clearSessionCookies(ctx);
|
|
166
205
|
const bearerToken = ctx.get("authorization")?.split("Bearer ")[1];
|
|
167
206
|
if (bearerToken) {
|
|
168
207
|
revokedConnectionTokens.set(bearerToken, Date.now());
|
|
@@ -196,9 +235,7 @@ async function interceptRenewToken(ctx, next) {
|
|
|
196
235
|
if (bearerToken) {
|
|
197
236
|
revokedConnectionTokens.set(bearerToken, Date.now());
|
|
198
237
|
}
|
|
199
|
-
|
|
200
|
-
ctx.session = null;
|
|
201
|
-
}
|
|
238
|
+
clearSessionCookies(ctx);
|
|
202
239
|
sessionActivityMap.delete(`${adminUser.id}:${adminUser.email}`);
|
|
203
240
|
}
|
|
204
241
|
await next();
|
|
@@ -378,29 +415,6 @@ const securitySettings = {
|
|
|
378
415
|
const contentTypes = {
|
|
379
416
|
"security-settings": securitySettings
|
|
380
417
|
};
|
|
381
|
-
class PluginError extends Error {
|
|
382
|
-
/**
|
|
383
|
-
* @param {string} message - Internal message (for logs)
|
|
384
|
-
* @param {string} sanitizedMessage - Safe message for the client
|
|
385
|
-
* @param {number} [statusCode=400] - HTTP status code
|
|
386
|
-
*/
|
|
387
|
-
constructor(message, sanitizedMessage, statusCode = HTTP_STATUS.BAD_REQUEST) {
|
|
388
|
-
super(message);
|
|
389
|
-
this.name = "PluginError";
|
|
390
|
-
this.sanitizedMessage = sanitizedMessage;
|
|
391
|
-
this.statusCode = statusCode;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
class ValidationError extends PluginError {
|
|
395
|
-
/**
|
|
396
|
-
* @param {string} message - Internal message
|
|
397
|
-
* @param {string} [sanitizedMessage='Validation failed.'] - Client-safe message
|
|
398
|
-
*/
|
|
399
|
-
constructor(message, sanitizedMessage = "Validation failed.") {
|
|
400
|
-
super(message, sanitizedMessage, HTTP_STATUS.BAD_REQUEST);
|
|
401
|
-
this.name = "ValidationError";
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
418
|
const validateSettingsPayload = (body) => {
|
|
405
419
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
406
420
|
throw new ValidationError(
|
package/package.json
CHANGED