strapi-plugin-oidc 1.7.5 → 1.8.0
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 +77 -41
- package/dist/admin/{index-BjmTHbr9.mjs → index-8YTLPV3h.mjs} +113 -100
- package/dist/admin/{index-BfX_taLq.mjs → index-B-K4X_N9.mjs} +8 -2
- package/dist/admin/{index-CLUIKIK3.js → index-BSgVStns.js} +8 -2
- package/dist/admin/{index-CacQfQ3a.js → index-CgG_mHzZ.js} +112 -99
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +188 -49
- package/dist/server/index.mjs +188 -49
- package/package.json +2 -1
package/dist/server/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID, randomBytes, createHash } from "node:crypto";
|
|
2
2
|
import pkceChallenge from "pkce-challenge";
|
|
3
|
+
import { jwtVerify, errors, createRemoteJWKSet } from "jose";
|
|
3
4
|
import { Readable } from "node:stream";
|
|
4
5
|
import strapiUtils from "@strapi/utils";
|
|
5
6
|
import generator from "generate-password";
|
|
@@ -93,6 +94,16 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
93
94
|
{ section: "plugins", displayName: "Update", uid: "update", pluginName: "strapi-plugin-oidc" }
|
|
94
95
|
];
|
|
95
96
|
await strapi2.admin.services.permission.actionProvider.registerMany(actions);
|
|
97
|
+
const contentApiScopeUids = [
|
|
98
|
+
"plugin::strapi-plugin-oidc.whitelist.read",
|
|
99
|
+
"plugin::strapi-plugin-oidc.whitelist.write",
|
|
100
|
+
"plugin::strapi-plugin-oidc.whitelist.delete",
|
|
101
|
+
"plugin::strapi-plugin-oidc.audit.read",
|
|
102
|
+
"plugin::strapi-plugin-oidc.audit.delete"
|
|
103
|
+
];
|
|
104
|
+
for (const uid of contentApiScopeUids) {
|
|
105
|
+
strapi2.contentAPI.permissions.providers.action.register(uid, { uid });
|
|
106
|
+
}
|
|
96
107
|
const enforceOIDCConfig = getEnforceOIDCConfig(strapi2);
|
|
97
108
|
if (enforceOIDCConfig !== null) {
|
|
98
109
|
try {
|
|
@@ -156,7 +167,12 @@ const config = {
|
|
|
156
167
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
157
168
|
AUDIT_LOG_RETENTION_DAYS: 90,
|
|
158
169
|
OIDC_GROUP_FIELD: "groups",
|
|
159
|
-
OIDC_GROUP_ROLE_MAP: "{}"
|
|
170
|
+
OIDC_GROUP_ROLE_MAP: "{}",
|
|
171
|
+
OIDC_REQUIRE_EMAIL_VERIFIED: true,
|
|
172
|
+
OIDC_TRUSTED_IP_HEADER: "",
|
|
173
|
+
OIDC_JWKS_URI: "",
|
|
174
|
+
OIDC_ISSUER: "",
|
|
175
|
+
OIDC_FORCE_SECURE_COOKIES: false
|
|
160
176
|
},
|
|
161
177
|
validator() {
|
|
162
178
|
}
|
|
@@ -205,11 +221,21 @@ const contentTypes = {
|
|
|
205
221
|
whitelists,
|
|
206
222
|
"audit-log": auditLog$1
|
|
207
223
|
};
|
|
208
|
-
function
|
|
224
|
+
function shouldMarkSecure(strapi2, ctx) {
|
|
209
225
|
const isProduction = strapi2.config.get("environment") === "production";
|
|
226
|
+
if (!isProduction) return false;
|
|
227
|
+
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc") ?? {};
|
|
228
|
+
if (config2.OIDC_FORCE_SECURE_COOKIES === true) return true;
|
|
229
|
+
if (ctx.request.secure) return true;
|
|
230
|
+
const proxyTrusted = ctx.app?.proxy === true;
|
|
231
|
+
if (proxyTrusted && typeof ctx.get === "function" && ctx.get("x-forwarded-proto") === "https")
|
|
232
|
+
return true;
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
function getExpiredCookieOptions(strapi2, ctx) {
|
|
210
236
|
return {
|
|
211
237
|
httpOnly: true,
|
|
212
|
-
secure:
|
|
238
|
+
secure: shouldMarkSecure(strapi2, ctx),
|
|
213
239
|
path: strapi2.config.get("admin.auth.cookie.path", "/admin"),
|
|
214
240
|
domain: strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain"),
|
|
215
241
|
sameSite: strapi2.config.get("admin.auth.cookie.sameSite", "lax"),
|
|
@@ -236,7 +262,9 @@ const errorCodes = {
|
|
|
236
262
|
NONCE_MISMATCH: "NONCE_MISMATCH",
|
|
237
263
|
ROLE_UPDATE_FAILED: "ROLE_UPDATE_FAILED",
|
|
238
264
|
USER_CREATION_FAILED: "USER_CREATION_FAILED",
|
|
239
|
-
WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED"
|
|
265
|
+
WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED",
|
|
266
|
+
EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED",
|
|
267
|
+
ID_TOKEN_INVALID: "ID_TOKEN_INVALID"
|
|
240
268
|
};
|
|
241
269
|
const ERROR_DETAIL_TEMPLATES = {
|
|
242
270
|
token_exchange_failed: "Token exchange failed with HTTP status {status}",
|
|
@@ -246,6 +274,8 @@ const ERROR_DETAIL_TEMPLATES = {
|
|
|
246
274
|
id_token_parse_failed: "ID token parse failed: {error}",
|
|
247
275
|
sign_in_unknown: "Unknown sign-in error: {error}",
|
|
248
276
|
invalid_email: "Invalid email address received from OIDC provider",
|
|
277
|
+
email_not_verified: "Email address has not been verified by the OIDC provider",
|
|
278
|
+
id_token_invalid: "ID token verification failed: {error}",
|
|
249
279
|
whitelist_not_present: "Email not present in whitelist",
|
|
250
280
|
session_manager_unsupported: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
251
281
|
missing_config: "Missing required config keys: {keys}"
|
|
@@ -265,6 +295,8 @@ const errorMessages = {
|
|
|
265
295
|
ID_TOKEN_PARSE_FAILED: "Failed to parse ID token",
|
|
266
296
|
NONCE_MISMATCH: "Nonce mismatch",
|
|
267
297
|
INVALID_EMAIL: "Invalid email address received from OIDC provider",
|
|
298
|
+
EMAIL_NOT_VERIFIED: "Email address has not been verified by the OIDC provider",
|
|
299
|
+
ID_TOKEN_INVALID: "ID token verification failed",
|
|
268
300
|
WHITELIST_NOT_PRESENT: "Not present in whitelist",
|
|
269
301
|
SESSION_MANAGER_UNSUPPORTED: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
270
302
|
MISSING_CONFIG: (keys) => `Missing required config keys: ${keys}`
|
|
@@ -362,6 +394,8 @@ const en = {
|
|
|
362
394
|
"auditlog.action.nonce_mismatch": "The nonce in the ID token did not match the one generated at login. This may indicate a token replay attack.",
|
|
363
395
|
"auditlog.action.token_exchange_failed": "The authorisation code could not be exchanged for tokens. The OIDC provider rejected the request.",
|
|
364
396
|
"auditlog.action.whitelist_rejected": "The user's email address is not on the whitelist. Access was denied.",
|
|
397
|
+
"auditlog.action.email_not_verified": "The OIDC provider did not confirm the user's email address as verified. Access was denied.",
|
|
398
|
+
"auditlog.action.id_token_invalid": "The ID token failed signature, issuer, audience, or expiry validation. Access was denied.",
|
|
365
399
|
"auth.page.authenticating.title": "Authenticating...",
|
|
366
400
|
"auth.page.authenticating.noscript.heading": "JavaScript Required",
|
|
367
401
|
"auth.page.authenticating.noscript.body": "JavaScript must be enabled for authentication to complete.",
|
|
@@ -471,24 +505,42 @@ const OIDC_ERROR_DISPATCH = {
|
|
|
471
505
|
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
472
506
|
key: "sign_in_unknown"
|
|
473
507
|
},
|
|
508
|
+
email_not_verified: {
|
|
509
|
+
action: "email_not_verified",
|
|
510
|
+
code: errorCodes.EMAIL_NOT_VERIFIED,
|
|
511
|
+
key: "email_not_verified"
|
|
512
|
+
},
|
|
513
|
+
id_token_invalid: {
|
|
514
|
+
action: "id_token_invalid",
|
|
515
|
+
code: errorCodes.ID_TOKEN_INVALID,
|
|
516
|
+
key: "id_token_invalid"
|
|
517
|
+
},
|
|
474
518
|
unknown: {
|
|
475
519
|
action: "login_failure",
|
|
476
520
|
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
477
521
|
key: "sign_in_unknown"
|
|
478
522
|
}
|
|
479
523
|
};
|
|
524
|
+
const TRUSTED_HEADER_WHITELIST = /* @__PURE__ */ new Set(["cf-connecting-ip"]);
|
|
525
|
+
function getTrustedHeaderName() {
|
|
526
|
+
const config2 = strapi.config.get("plugin::strapi-plugin-oidc") ?? {};
|
|
527
|
+
const raw = config2.OIDC_TRUSTED_IP_HEADER;
|
|
528
|
+
if (typeof raw !== "string" || !raw) return void 0;
|
|
529
|
+
const normalized = raw.trim().toLowerCase();
|
|
530
|
+
return TRUSTED_HEADER_WHITELIST.has(normalized) ? normalized : void 0;
|
|
531
|
+
}
|
|
480
532
|
function getClientIp(ctx) {
|
|
481
|
-
const
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
533
|
+
const proxyTrusted = ctx.app?.proxy === true;
|
|
534
|
+
if (proxyTrusted) {
|
|
535
|
+
const trustedHeader = getTrustedHeaderName();
|
|
536
|
+
if (trustedHeader) {
|
|
537
|
+
const value = ctx.get(trustedHeader);
|
|
538
|
+
if (value) return value.split(",")[0].trim();
|
|
539
|
+
}
|
|
540
|
+
const forwarded = ctx.request.ips;
|
|
541
|
+
if (forwarded && forwarded.length > 0) {
|
|
542
|
+
return forwarded[0];
|
|
543
|
+
}
|
|
492
544
|
}
|
|
493
545
|
return ctx.ip;
|
|
494
546
|
}
|
|
@@ -505,6 +557,43 @@ const REQUIRED_CONFIG_KEYS = [
|
|
|
505
557
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
506
558
|
];
|
|
507
559
|
const LOGOUT_USERINFO_TIMEOUT_MS = 3e3;
|
|
560
|
+
const jwksCache = /* @__PURE__ */ new Map();
|
|
561
|
+
let jwksDisabledWarned = false;
|
|
562
|
+
function getJwks(uri) {
|
|
563
|
+
let jwks = jwksCache.get(uri);
|
|
564
|
+
if (!jwks) {
|
|
565
|
+
jwks = createRemoteJWKSet(new URL(uri));
|
|
566
|
+
jwksCache.set(uri, jwks);
|
|
567
|
+
}
|
|
568
|
+
return jwks;
|
|
569
|
+
}
|
|
570
|
+
async function verifyIdToken(idToken, config2) {
|
|
571
|
+
const jwksUri = config2.OIDC_JWKS_URI;
|
|
572
|
+
const issuer = config2.OIDC_ISSUER;
|
|
573
|
+
if (!jwksUri) {
|
|
574
|
+
if (!jwksDisabledWarned) {
|
|
575
|
+
jwksDisabledWarned = true;
|
|
576
|
+
strapi.log.warn(
|
|
577
|
+
"[OIDC] OIDC_JWKS_URI is not configured — ID token signature verification is disabled. Set OIDC_JWKS_URI and OIDC_ISSUER from your provider's discovery document."
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
const jwks = getJwks(jwksUri);
|
|
584
|
+
const { payload } = await jwtVerify(idToken, jwks, {
|
|
585
|
+
issuer: issuer || void 0,
|
|
586
|
+
audience: config2.OIDC_CLIENT_ID
|
|
587
|
+
});
|
|
588
|
+
return payload;
|
|
589
|
+
} catch (e) {
|
|
590
|
+
if (e instanceof errors.JWTClaimValidationFailed || e instanceof errors.JWSSignatureVerificationFailed || e instanceof errors.JWTExpired || e instanceof errors.JWTInvalid || e instanceof errors.JWSInvalid) {
|
|
591
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
592
|
+
throw new OidcError("id_token_invalid", msg, e);
|
|
593
|
+
}
|
|
594
|
+
throw e;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
508
597
|
function configValidation() {
|
|
509
598
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
510
599
|
const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
|
|
@@ -518,11 +607,10 @@ async function oidcSignIn(ctx) {
|
|
|
518
607
|
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
|
|
519
608
|
const state = randomBytes(32).toString("base64url");
|
|
520
609
|
const nonce = randomBytes(32).toString("base64url");
|
|
521
|
-
const isProduction = strapi.config.get("environment") === "production";
|
|
522
610
|
const cookieOptions = {
|
|
523
611
|
httpOnly: true,
|
|
524
612
|
maxAge: 6e5,
|
|
525
|
-
secure:
|
|
613
|
+
secure: shouldMarkSecure(strapi, ctx),
|
|
526
614
|
sameSite: "lax"
|
|
527
615
|
};
|
|
528
616
|
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
@@ -555,14 +643,16 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
555
643
|
}
|
|
556
644
|
const tokenData = await response.json();
|
|
557
645
|
if (tokenData.id_token) {
|
|
646
|
+
const verifiedPayload = await verifyIdToken(tokenData.id_token, config2);
|
|
558
647
|
try {
|
|
559
|
-
const
|
|
560
|
-
|
|
648
|
+
const idTokenPayload = verifiedPayload ?? JSON.parse(
|
|
649
|
+
Buffer.from(tokenData.id_token.split(".")[1], "base64url").toString("utf8")
|
|
650
|
+
);
|
|
561
651
|
if (idTokenPayload.nonce !== expectedNonce) {
|
|
562
652
|
throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
|
|
563
653
|
}
|
|
564
654
|
} catch (e) {
|
|
565
|
-
if (e instanceof OidcError
|
|
655
|
+
if (e instanceof OidcError) throw e;
|
|
566
656
|
throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
|
|
567
657
|
}
|
|
568
658
|
}
|
|
@@ -708,6 +798,13 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
|
|
|
708
798
|
if (!email || !isValidEmail(email)) {
|
|
709
799
|
throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
|
|
710
800
|
}
|
|
801
|
+
if (config2.OIDC_REQUIRE_EMAIL_VERIFIED !== false) {
|
|
802
|
+
const emailVerified = userResponseData.email_verified;
|
|
803
|
+
const isVerified = emailVerified === true || emailVerified === "true";
|
|
804
|
+
if (!isVerified) {
|
|
805
|
+
throw new OidcError("email_not_verified", errorMessages.EMAIL_NOT_VERIFIED);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
711
808
|
await whitelistService2.checkWhitelistForEmail(email);
|
|
712
809
|
const resolved = await resolveRoles(userResponseData, config2, roleService2);
|
|
713
810
|
const { user, userCreated, rolesUpdated } = await ensureUser(
|
|
@@ -734,7 +831,7 @@ function classifyOidcError(e, userInfo) {
|
|
|
734
831
|
const dispatch = OIDC_ERROR_DISPATCH[kind];
|
|
735
832
|
const msg = e instanceof Error ? e.message : String(e);
|
|
736
833
|
let params;
|
|
737
|
-
if (kind === "id_token_parse_failed" || kind === "unknown") {
|
|
834
|
+
if (kind === "id_token_parse_failed" || kind === "id_token_invalid" || kind === "unknown") {
|
|
738
835
|
params = { error: msg };
|
|
739
836
|
} else if (kind === "user_creation_failed" && userInfo?.email) {
|
|
740
837
|
params = { email: userInfo.email, error: msg };
|
|
@@ -829,8 +926,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
829
926
|
try {
|
|
830
927
|
const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
831
928
|
userInfo = exchangeResult.userInfo;
|
|
832
|
-
const
|
|
833
|
-
const secureFlag = isProduction && ctx.request.secure;
|
|
929
|
+
const secureFlag = shouldMarkSecure(strapi, ctx);
|
|
834
930
|
ctx.cookies.set("oidc_access_token", exchangeResult.accessToken, {
|
|
835
931
|
httpOnly: true,
|
|
836
932
|
maxAge: 3e5,
|
|
@@ -1000,16 +1096,34 @@ async function register(ctx) {
|
|
|
1000
1096
|
return;
|
|
1001
1097
|
}
|
|
1002
1098
|
const rawEmails = Array.isArray(email) ? email : email.split(",");
|
|
1003
|
-
const
|
|
1099
|
+
const normalized = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
|
|
1100
|
+
const rejectedEmails = [];
|
|
1101
|
+
const validEmails = [];
|
|
1102
|
+
for (const e of normalized) {
|
|
1103
|
+
if (isValidEmail(e)) {
|
|
1104
|
+
validEmails.push(e);
|
|
1105
|
+
} else {
|
|
1106
|
+
rejectedEmails.push(e);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (validEmails.length === 0) {
|
|
1110
|
+
ctx.status = 400;
|
|
1111
|
+
ctx.body = { error: "No valid email addresses supplied", rejectedEmails };
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1004
1114
|
const whitelistService2 = getWhitelistService();
|
|
1005
|
-
|
|
1006
|
-
|
|
1115
|
+
let acceptedCount = 0;
|
|
1116
|
+
let alreadyWhitelistedCount = 0;
|
|
1117
|
+
for (const singleEmail of validEmails) {
|
|
1007
1118
|
const alreadyWhitelisted = await whitelistService2.hasUser(singleEmail);
|
|
1008
|
-
if (
|
|
1119
|
+
if (alreadyWhitelisted) {
|
|
1120
|
+
alreadyWhitelistedCount++;
|
|
1121
|
+
} else {
|
|
1009
1122
|
await whitelistService2.registerUser(singleEmail);
|
|
1123
|
+
acceptedCount++;
|
|
1010
1124
|
}
|
|
1011
1125
|
}
|
|
1012
|
-
ctx.body = {
|
|
1126
|
+
ctx.body = { acceptedCount, alreadyWhitelistedCount, rejectedEmails };
|
|
1013
1127
|
}
|
|
1014
1128
|
async function removeEmail(ctx) {
|
|
1015
1129
|
const { email } = ctx.params;
|
|
@@ -1065,7 +1179,7 @@ async function syncUsers(ctx) {
|
|
|
1065
1179
|
await whitelistService2.registerUser(email);
|
|
1066
1180
|
}
|
|
1067
1181
|
}
|
|
1068
|
-
ctx.body = {
|
|
1182
|
+
ctx.body = {};
|
|
1069
1183
|
}
|
|
1070
1184
|
const whitelist = {
|
|
1071
1185
|
info,
|
|
@@ -1086,6 +1200,8 @@ const AUDIT_ACTIONS = [
|
|
|
1086
1200
|
"nonce_mismatch",
|
|
1087
1201
|
"token_exchange_failed",
|
|
1088
1202
|
"whitelist_rejected",
|
|
1203
|
+
"email_not_verified",
|
|
1204
|
+
"id_token_invalid",
|
|
1089
1205
|
"logout",
|
|
1090
1206
|
"session_expired",
|
|
1091
1207
|
"user_created"
|
|
@@ -1285,6 +1401,22 @@ const controllers = {
|
|
|
1285
1401
|
const rateLimitMap = /* @__PURE__ */ new Map();
|
|
1286
1402
|
const RATE_LIMIT_WINDOW = 6e4;
|
|
1287
1403
|
const MAX_REQUESTS = 1e3;
|
|
1404
|
+
const MAX_MAP_SIZE = 1e4;
|
|
1405
|
+
const PRUNE_THRESHOLD = 1e3;
|
|
1406
|
+
function pruneExpiredEntries(now) {
|
|
1407
|
+
const windowStart = now - RATE_LIMIT_WINDOW;
|
|
1408
|
+
for (const [key, stamps] of rateLimitMap) {
|
|
1409
|
+
if (stamps.length === 0 || stamps[stamps.length - 1] <= windowStart) {
|
|
1410
|
+
rateLimitMap.delete(key);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function evictOldestEntry() {
|
|
1415
|
+
const oldest = rateLimitMap.keys().next().value;
|
|
1416
|
+
if (oldest !== void 0) {
|
|
1417
|
+
rateLimitMap.delete(oldest);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1288
1420
|
function getRateLimitKey(ctx) {
|
|
1289
1421
|
const ip = getClientIp(ctx);
|
|
1290
1422
|
const ua = ctx.request.header["user-agent"] ?? "";
|
|
@@ -1295,6 +1427,9 @@ function rateLimitMiddleware(ctx, next) {
|
|
|
1295
1427
|
const key = getRateLimitKey(ctx);
|
|
1296
1428
|
const now = Date.now();
|
|
1297
1429
|
const windowStart = now - RATE_LIMIT_WINDOW;
|
|
1430
|
+
if (rateLimitMap.size > PRUNE_THRESHOLD) {
|
|
1431
|
+
pruneExpiredEntries(now);
|
|
1432
|
+
}
|
|
1298
1433
|
const requestStamps = (rateLimitMap.get(key) ?? []).filter((ts) => ts > windowStart);
|
|
1299
1434
|
if (requestStamps.length >= MAX_REQUESTS) {
|
|
1300
1435
|
ctx.status = 429;
|
|
@@ -1302,6 +1437,9 @@ function rateLimitMiddleware(ctx, next) {
|
|
|
1302
1437
|
return;
|
|
1303
1438
|
}
|
|
1304
1439
|
requestStamps.push(now);
|
|
1440
|
+
if (!rateLimitMap.has(key) && rateLimitMap.size >= MAX_MAP_SIZE) {
|
|
1441
|
+
evictOldestEntry();
|
|
1442
|
+
}
|
|
1305
1443
|
rateLimitMap.set(key, requestStamps);
|
|
1306
1444
|
return next();
|
|
1307
1445
|
}
|
|
@@ -1345,7 +1483,7 @@ const routes = {
|
|
|
1345
1483
|
config: { auth: false, middlewares: [rateLimitMiddleware] }
|
|
1346
1484
|
},
|
|
1347
1485
|
{
|
|
1348
|
-
method: "
|
|
1486
|
+
method: "POST",
|
|
1349
1487
|
path: "/logout",
|
|
1350
1488
|
handler: "oidc.logout",
|
|
1351
1489
|
config: { auth: false }
|
|
@@ -1427,53 +1565,63 @@ const routes = {
|
|
|
1427
1565
|
// API-token-authenticated routes for programmatic whitelist management.
|
|
1428
1566
|
// Accessible at /strapi-plugin-oidc/... using a Strapi API token
|
|
1429
1567
|
// (full-access or custom) in the Authorization: Bearer <token> header.
|
|
1568
|
+
// Custom tokens must be granted one or more of the semantic scopes below.
|
|
1430
1569
|
"content-api": {
|
|
1431
1570
|
type: "content-api",
|
|
1432
1571
|
routes: [
|
|
1433
1572
|
{
|
|
1434
1573
|
method: "GET",
|
|
1435
1574
|
path: "/whitelist",
|
|
1436
|
-
handler: "whitelist.info"
|
|
1575
|
+
handler: "whitelist.info",
|
|
1576
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.read"] } }
|
|
1437
1577
|
},
|
|
1438
1578
|
{
|
|
1439
1579
|
method: "POST",
|
|
1440
1580
|
path: "/whitelist",
|
|
1441
|
-
handler: "whitelist.register"
|
|
1581
|
+
handler: "whitelist.register",
|
|
1582
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.write"] } }
|
|
1442
1583
|
},
|
|
1443
1584
|
{
|
|
1444
1585
|
method: "POST",
|
|
1445
1586
|
path: "/whitelist/import",
|
|
1446
|
-
handler: "whitelist.importUsers"
|
|
1587
|
+
handler: "whitelist.importUsers",
|
|
1588
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.write"] } }
|
|
1447
1589
|
},
|
|
1448
1590
|
{
|
|
1449
1591
|
method: "DELETE",
|
|
1450
1592
|
path: "/whitelist/:email",
|
|
1451
|
-
handler: "whitelist.removeEmail"
|
|
1593
|
+
handler: "whitelist.removeEmail",
|
|
1594
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.delete"] } }
|
|
1452
1595
|
},
|
|
1453
1596
|
{
|
|
1454
1597
|
method: "DELETE",
|
|
1455
1598
|
path: "/whitelist",
|
|
1456
|
-
handler: "whitelist.deleteAll"
|
|
1599
|
+
handler: "whitelist.deleteAll",
|
|
1600
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.delete"] } }
|
|
1457
1601
|
},
|
|
1458
1602
|
{
|
|
1459
1603
|
method: "GET",
|
|
1460
1604
|
path: "/whitelist/export",
|
|
1461
|
-
handler: "whitelist.exportWhitelist"
|
|
1605
|
+
handler: "whitelist.exportWhitelist",
|
|
1606
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.read"] } }
|
|
1462
1607
|
},
|
|
1463
1608
|
{
|
|
1464
1609
|
method: "GET",
|
|
1465
1610
|
path: "/audit-logs",
|
|
1466
|
-
handler: "auditLog.find"
|
|
1611
|
+
handler: "auditLog.find",
|
|
1612
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.audit.read"] } }
|
|
1467
1613
|
},
|
|
1468
1614
|
{
|
|
1469
1615
|
method: "GET",
|
|
1470
1616
|
path: "/audit-logs/export",
|
|
1471
|
-
handler: "auditLog.export"
|
|
1617
|
+
handler: "auditLog.export",
|
|
1618
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.audit.read"] } }
|
|
1472
1619
|
},
|
|
1473
1620
|
{
|
|
1474
1621
|
method: "DELETE",
|
|
1475
1622
|
path: "/audit-logs",
|
|
1476
|
-
handler: "auditLog.clearAll"
|
|
1623
|
+
handler: "auditLog.clearAll",
|
|
1624
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.audit.delete"] } }
|
|
1477
1625
|
}
|
|
1478
1626
|
]
|
|
1479
1627
|
}
|
|
@@ -1725,13 +1873,12 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1725
1873
|
type: rememberMe ? "refresh" : "session"
|
|
1726
1874
|
}
|
|
1727
1875
|
);
|
|
1728
|
-
const isProduction = strapi2.config.get("environment") === "production";
|
|
1729
1876
|
const domain = strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain");
|
|
1730
1877
|
const path = strapi2.config.get("admin.auth.cookie.path", "/admin");
|
|
1731
1878
|
const sameSite = strapi2.config.get("admin.auth.cookie.sameSite", "lax");
|
|
1732
1879
|
const cookieOptions = {
|
|
1733
1880
|
httpOnly: true,
|
|
1734
|
-
secure:
|
|
1881
|
+
secure: shouldMarkSecure(strapi2, ctx),
|
|
1735
1882
|
overwrite: true,
|
|
1736
1883
|
domain,
|
|
1737
1884
|
path,
|
|
@@ -1850,14 +1997,6 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1850
1997
|
},
|
|
1851
1998
|
async deleteAllUsers() {
|
|
1852
1999
|
await getWhitelistQuery().deleteMany({});
|
|
1853
|
-
},
|
|
1854
|
-
async countAdminUsersByEmails(emails) {
|
|
1855
|
-
if (emails.length === 0) return 0;
|
|
1856
|
-
const rows = await strapi2.query("admin::user").findMany({
|
|
1857
|
-
where: { email: { $in: emails } },
|
|
1858
|
-
select: ["id"]
|
|
1859
|
-
});
|
|
1860
|
-
return rows.length;
|
|
1861
2000
|
}
|
|
1862
2001
|
};
|
|
1863
2002
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-oidc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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,6 +50,7 @@
|
|
|
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",
|
|
53
54
|
"lucide-react": "^1.8.0",
|
|
54
55
|
"pkce-challenge": "^6.0.0",
|
|
55
56
|
"react-intl": "^6.8.9"
|