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.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
3
|
const node_crypto = require("node:crypto");
|
|
4
4
|
const pkceChallenge = require("pkce-challenge");
|
|
5
|
+
const jose = require("jose");
|
|
5
6
|
const node_stream = require("node:stream");
|
|
6
7
|
const strapiUtils = require("@strapi/utils");
|
|
7
8
|
const generator = require("generate-password");
|
|
@@ -99,6 +100,16 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
99
100
|
{ section: "plugins", displayName: "Update", uid: "update", pluginName: "strapi-plugin-oidc" }
|
|
100
101
|
];
|
|
101
102
|
await strapi2.admin.services.permission.actionProvider.registerMany(actions);
|
|
103
|
+
const contentApiScopeUids = [
|
|
104
|
+
"plugin::strapi-plugin-oidc.whitelist.read",
|
|
105
|
+
"plugin::strapi-plugin-oidc.whitelist.write",
|
|
106
|
+
"plugin::strapi-plugin-oidc.whitelist.delete",
|
|
107
|
+
"plugin::strapi-plugin-oidc.audit.read",
|
|
108
|
+
"plugin::strapi-plugin-oidc.audit.delete"
|
|
109
|
+
];
|
|
110
|
+
for (const uid of contentApiScopeUids) {
|
|
111
|
+
strapi2.contentAPI.permissions.providers.action.register(uid, { uid });
|
|
112
|
+
}
|
|
102
113
|
const enforceOIDCConfig = getEnforceOIDCConfig(strapi2);
|
|
103
114
|
if (enforceOIDCConfig !== null) {
|
|
104
115
|
try {
|
|
@@ -162,7 +173,12 @@ const config = {
|
|
|
162
173
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
163
174
|
AUDIT_LOG_RETENTION_DAYS: 90,
|
|
164
175
|
OIDC_GROUP_FIELD: "groups",
|
|
165
|
-
OIDC_GROUP_ROLE_MAP: "{}"
|
|
176
|
+
OIDC_GROUP_ROLE_MAP: "{}",
|
|
177
|
+
OIDC_REQUIRE_EMAIL_VERIFIED: true,
|
|
178
|
+
OIDC_TRUSTED_IP_HEADER: "",
|
|
179
|
+
OIDC_JWKS_URI: "",
|
|
180
|
+
OIDC_ISSUER: "",
|
|
181
|
+
OIDC_FORCE_SECURE_COOKIES: false
|
|
166
182
|
},
|
|
167
183
|
validator() {
|
|
168
184
|
}
|
|
@@ -211,11 +227,21 @@ const contentTypes = {
|
|
|
211
227
|
whitelists,
|
|
212
228
|
"audit-log": auditLog$1
|
|
213
229
|
};
|
|
214
|
-
function
|
|
230
|
+
function shouldMarkSecure(strapi2, ctx) {
|
|
215
231
|
const isProduction = strapi2.config.get("environment") === "production";
|
|
232
|
+
if (!isProduction) return false;
|
|
233
|
+
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc") ?? {};
|
|
234
|
+
if (config2.OIDC_FORCE_SECURE_COOKIES === true) return true;
|
|
235
|
+
if (ctx.request.secure) return true;
|
|
236
|
+
const proxyTrusted = ctx.app?.proxy === true;
|
|
237
|
+
if (proxyTrusted && typeof ctx.get === "function" && ctx.get("x-forwarded-proto") === "https")
|
|
238
|
+
return true;
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function getExpiredCookieOptions(strapi2, ctx) {
|
|
216
242
|
return {
|
|
217
243
|
httpOnly: true,
|
|
218
|
-
secure:
|
|
244
|
+
secure: shouldMarkSecure(strapi2, ctx),
|
|
219
245
|
path: strapi2.config.get("admin.auth.cookie.path", "/admin"),
|
|
220
246
|
domain: strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain"),
|
|
221
247
|
sameSite: strapi2.config.get("admin.auth.cookie.sameSite", "lax"),
|
|
@@ -242,7 +268,9 @@ const errorCodes = {
|
|
|
242
268
|
NONCE_MISMATCH: "NONCE_MISMATCH",
|
|
243
269
|
ROLE_UPDATE_FAILED: "ROLE_UPDATE_FAILED",
|
|
244
270
|
USER_CREATION_FAILED: "USER_CREATION_FAILED",
|
|
245
|
-
WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED"
|
|
271
|
+
WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED",
|
|
272
|
+
EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED",
|
|
273
|
+
ID_TOKEN_INVALID: "ID_TOKEN_INVALID"
|
|
246
274
|
};
|
|
247
275
|
const ERROR_DETAIL_TEMPLATES = {
|
|
248
276
|
token_exchange_failed: "Token exchange failed with HTTP status {status}",
|
|
@@ -252,6 +280,8 @@ const ERROR_DETAIL_TEMPLATES = {
|
|
|
252
280
|
id_token_parse_failed: "ID token parse failed: {error}",
|
|
253
281
|
sign_in_unknown: "Unknown sign-in error: {error}",
|
|
254
282
|
invalid_email: "Invalid email address received from OIDC provider",
|
|
283
|
+
email_not_verified: "Email address has not been verified by the OIDC provider",
|
|
284
|
+
id_token_invalid: "ID token verification failed: {error}",
|
|
255
285
|
whitelist_not_present: "Email not present in whitelist",
|
|
256
286
|
session_manager_unsupported: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
257
287
|
missing_config: "Missing required config keys: {keys}"
|
|
@@ -271,6 +301,8 @@ const errorMessages = {
|
|
|
271
301
|
ID_TOKEN_PARSE_FAILED: "Failed to parse ID token",
|
|
272
302
|
NONCE_MISMATCH: "Nonce mismatch",
|
|
273
303
|
INVALID_EMAIL: "Invalid email address received from OIDC provider",
|
|
304
|
+
EMAIL_NOT_VERIFIED: "Email address has not been verified by the OIDC provider",
|
|
305
|
+
ID_TOKEN_INVALID: "ID token verification failed",
|
|
274
306
|
WHITELIST_NOT_PRESENT: "Not present in whitelist",
|
|
275
307
|
SESSION_MANAGER_UNSUPPORTED: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
276
308
|
MISSING_CONFIG: (keys) => `Missing required config keys: ${keys}`
|
|
@@ -368,6 +400,8 @@ const en = {
|
|
|
368
400
|
"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.",
|
|
369
401
|
"auditlog.action.token_exchange_failed": "The authorisation code could not be exchanged for tokens. The OIDC provider rejected the request.",
|
|
370
402
|
"auditlog.action.whitelist_rejected": "The user's email address is not on the whitelist. Access was denied.",
|
|
403
|
+
"auditlog.action.email_not_verified": "The OIDC provider did not confirm the user's email address as verified. Access was denied.",
|
|
404
|
+
"auditlog.action.id_token_invalid": "The ID token failed signature, issuer, audience, or expiry validation. Access was denied.",
|
|
371
405
|
"auth.page.authenticating.title": "Authenticating...",
|
|
372
406
|
"auth.page.authenticating.noscript.heading": "JavaScript Required",
|
|
373
407
|
"auth.page.authenticating.noscript.body": "JavaScript must be enabled for authentication to complete.",
|
|
@@ -477,24 +511,42 @@ const OIDC_ERROR_DISPATCH = {
|
|
|
477
511
|
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
478
512
|
key: "sign_in_unknown"
|
|
479
513
|
},
|
|
514
|
+
email_not_verified: {
|
|
515
|
+
action: "email_not_verified",
|
|
516
|
+
code: errorCodes.EMAIL_NOT_VERIFIED,
|
|
517
|
+
key: "email_not_verified"
|
|
518
|
+
},
|
|
519
|
+
id_token_invalid: {
|
|
520
|
+
action: "id_token_invalid",
|
|
521
|
+
code: errorCodes.ID_TOKEN_INVALID,
|
|
522
|
+
key: "id_token_invalid"
|
|
523
|
+
},
|
|
480
524
|
unknown: {
|
|
481
525
|
action: "login_failure",
|
|
482
526
|
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
483
527
|
key: "sign_in_unknown"
|
|
484
528
|
}
|
|
485
529
|
};
|
|
530
|
+
const TRUSTED_HEADER_WHITELIST = /* @__PURE__ */ new Set(["cf-connecting-ip"]);
|
|
531
|
+
function getTrustedHeaderName() {
|
|
532
|
+
const config2 = strapi.config.get("plugin::strapi-plugin-oidc") ?? {};
|
|
533
|
+
const raw = config2.OIDC_TRUSTED_IP_HEADER;
|
|
534
|
+
if (typeof raw !== "string" || !raw) return void 0;
|
|
535
|
+
const normalized = raw.trim().toLowerCase();
|
|
536
|
+
return TRUSTED_HEADER_WHITELIST.has(normalized) ? normalized : void 0;
|
|
537
|
+
}
|
|
486
538
|
function getClientIp(ctx) {
|
|
487
|
-
const
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
539
|
+
const proxyTrusted = ctx.app?.proxy === true;
|
|
540
|
+
if (proxyTrusted) {
|
|
541
|
+
const trustedHeader = getTrustedHeaderName();
|
|
542
|
+
if (trustedHeader) {
|
|
543
|
+
const value = ctx.get(trustedHeader);
|
|
544
|
+
if (value) return value.split(",")[0].trim();
|
|
545
|
+
}
|
|
546
|
+
const forwarded = ctx.request.ips;
|
|
547
|
+
if (forwarded && forwarded.length > 0) {
|
|
548
|
+
return forwarded[0];
|
|
549
|
+
}
|
|
498
550
|
}
|
|
499
551
|
return ctx.ip;
|
|
500
552
|
}
|
|
@@ -511,6 +563,43 @@ const REQUIRED_CONFIG_KEYS = [
|
|
|
511
563
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
512
564
|
];
|
|
513
565
|
const LOGOUT_USERINFO_TIMEOUT_MS = 3e3;
|
|
566
|
+
const jwksCache = /* @__PURE__ */ new Map();
|
|
567
|
+
let jwksDisabledWarned = false;
|
|
568
|
+
function getJwks(uri) {
|
|
569
|
+
let jwks = jwksCache.get(uri);
|
|
570
|
+
if (!jwks) {
|
|
571
|
+
jwks = jose.createRemoteJWKSet(new URL(uri));
|
|
572
|
+
jwksCache.set(uri, jwks);
|
|
573
|
+
}
|
|
574
|
+
return jwks;
|
|
575
|
+
}
|
|
576
|
+
async function verifyIdToken(idToken, config2) {
|
|
577
|
+
const jwksUri = config2.OIDC_JWKS_URI;
|
|
578
|
+
const issuer = config2.OIDC_ISSUER;
|
|
579
|
+
if (!jwksUri) {
|
|
580
|
+
if (!jwksDisabledWarned) {
|
|
581
|
+
jwksDisabledWarned = true;
|
|
582
|
+
strapi.log.warn(
|
|
583
|
+
"[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."
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const jwks = getJwks(jwksUri);
|
|
590
|
+
const { payload } = await jose.jwtVerify(idToken, jwks, {
|
|
591
|
+
issuer: issuer || void 0,
|
|
592
|
+
audience: config2.OIDC_CLIENT_ID
|
|
593
|
+
});
|
|
594
|
+
return payload;
|
|
595
|
+
} catch (e) {
|
|
596
|
+
if (e instanceof jose.errors.JWTClaimValidationFailed || e instanceof jose.errors.JWSSignatureVerificationFailed || e instanceof jose.errors.JWTExpired || e instanceof jose.errors.JWTInvalid || e instanceof jose.errors.JWSInvalid) {
|
|
597
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
598
|
+
throw new OidcError("id_token_invalid", msg, e);
|
|
599
|
+
}
|
|
600
|
+
throw e;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
514
603
|
function configValidation() {
|
|
515
604
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
516
605
|
const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
|
|
@@ -524,11 +613,10 @@ async function oidcSignIn(ctx) {
|
|
|
524
613
|
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge__default.default();
|
|
525
614
|
const state = node_crypto.randomBytes(32).toString("base64url");
|
|
526
615
|
const nonce = node_crypto.randomBytes(32).toString("base64url");
|
|
527
|
-
const isProduction = strapi.config.get("environment") === "production";
|
|
528
616
|
const cookieOptions = {
|
|
529
617
|
httpOnly: true,
|
|
530
618
|
maxAge: 6e5,
|
|
531
|
-
secure:
|
|
619
|
+
secure: shouldMarkSecure(strapi, ctx),
|
|
532
620
|
sameSite: "lax"
|
|
533
621
|
};
|
|
534
622
|
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
@@ -561,14 +649,16 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
561
649
|
}
|
|
562
650
|
const tokenData = await response.json();
|
|
563
651
|
if (tokenData.id_token) {
|
|
652
|
+
const verifiedPayload = await verifyIdToken(tokenData.id_token, config2);
|
|
564
653
|
try {
|
|
565
|
-
const
|
|
566
|
-
|
|
654
|
+
const idTokenPayload = verifiedPayload ?? JSON.parse(
|
|
655
|
+
Buffer.from(tokenData.id_token.split(".")[1], "base64url").toString("utf8")
|
|
656
|
+
);
|
|
567
657
|
if (idTokenPayload.nonce !== expectedNonce) {
|
|
568
658
|
throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
|
|
569
659
|
}
|
|
570
660
|
} catch (e) {
|
|
571
|
-
if (e instanceof OidcError
|
|
661
|
+
if (e instanceof OidcError) throw e;
|
|
572
662
|
throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
|
|
573
663
|
}
|
|
574
664
|
}
|
|
@@ -714,6 +804,13 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
|
|
|
714
804
|
if (!email || !isValidEmail(email)) {
|
|
715
805
|
throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
|
|
716
806
|
}
|
|
807
|
+
if (config2.OIDC_REQUIRE_EMAIL_VERIFIED !== false) {
|
|
808
|
+
const emailVerified = userResponseData.email_verified;
|
|
809
|
+
const isVerified = emailVerified === true || emailVerified === "true";
|
|
810
|
+
if (!isVerified) {
|
|
811
|
+
throw new OidcError("email_not_verified", errorMessages.EMAIL_NOT_VERIFIED);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
717
814
|
await whitelistService2.checkWhitelistForEmail(email);
|
|
718
815
|
const resolved = await resolveRoles(userResponseData, config2, roleService2);
|
|
719
816
|
const { user, userCreated, rolesUpdated } = await ensureUser(
|
|
@@ -740,7 +837,7 @@ function classifyOidcError(e, userInfo) {
|
|
|
740
837
|
const dispatch = OIDC_ERROR_DISPATCH[kind];
|
|
741
838
|
const msg = e instanceof Error ? e.message : String(e);
|
|
742
839
|
let params;
|
|
743
|
-
if (kind === "id_token_parse_failed" || kind === "unknown") {
|
|
840
|
+
if (kind === "id_token_parse_failed" || kind === "id_token_invalid" || kind === "unknown") {
|
|
744
841
|
params = { error: msg };
|
|
745
842
|
} else if (kind === "user_creation_failed" && userInfo?.email) {
|
|
746
843
|
params = { email: userInfo.email, error: msg };
|
|
@@ -835,8 +932,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
835
932
|
try {
|
|
836
933
|
const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
837
934
|
userInfo = exchangeResult.userInfo;
|
|
838
|
-
const
|
|
839
|
-
const secureFlag = isProduction && ctx.request.secure;
|
|
935
|
+
const secureFlag = shouldMarkSecure(strapi, ctx);
|
|
840
936
|
ctx.cookies.set("oidc_access_token", exchangeResult.accessToken, {
|
|
841
937
|
httpOnly: true,
|
|
842
938
|
maxAge: 3e5,
|
|
@@ -1006,16 +1102,34 @@ async function register(ctx) {
|
|
|
1006
1102
|
return;
|
|
1007
1103
|
}
|
|
1008
1104
|
const rawEmails = Array.isArray(email) ? email : email.split(",");
|
|
1009
|
-
const
|
|
1105
|
+
const normalized = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
|
|
1106
|
+
const rejectedEmails = [];
|
|
1107
|
+
const validEmails = [];
|
|
1108
|
+
for (const e of normalized) {
|
|
1109
|
+
if (isValidEmail(e)) {
|
|
1110
|
+
validEmails.push(e);
|
|
1111
|
+
} else {
|
|
1112
|
+
rejectedEmails.push(e);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (validEmails.length === 0) {
|
|
1116
|
+
ctx.status = 400;
|
|
1117
|
+
ctx.body = { error: "No valid email addresses supplied", rejectedEmails };
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1010
1120
|
const whitelistService2 = getWhitelistService();
|
|
1011
|
-
|
|
1012
|
-
|
|
1121
|
+
let acceptedCount = 0;
|
|
1122
|
+
let alreadyWhitelistedCount = 0;
|
|
1123
|
+
for (const singleEmail of validEmails) {
|
|
1013
1124
|
const alreadyWhitelisted = await whitelistService2.hasUser(singleEmail);
|
|
1014
|
-
if (
|
|
1125
|
+
if (alreadyWhitelisted) {
|
|
1126
|
+
alreadyWhitelistedCount++;
|
|
1127
|
+
} else {
|
|
1015
1128
|
await whitelistService2.registerUser(singleEmail);
|
|
1129
|
+
acceptedCount++;
|
|
1016
1130
|
}
|
|
1017
1131
|
}
|
|
1018
|
-
ctx.body = {
|
|
1132
|
+
ctx.body = { acceptedCount, alreadyWhitelistedCount, rejectedEmails };
|
|
1019
1133
|
}
|
|
1020
1134
|
async function removeEmail(ctx) {
|
|
1021
1135
|
const { email } = ctx.params;
|
|
@@ -1071,7 +1185,7 @@ async function syncUsers(ctx) {
|
|
|
1071
1185
|
await whitelistService2.registerUser(email);
|
|
1072
1186
|
}
|
|
1073
1187
|
}
|
|
1074
|
-
ctx.body = {
|
|
1188
|
+
ctx.body = {};
|
|
1075
1189
|
}
|
|
1076
1190
|
const whitelist = {
|
|
1077
1191
|
info,
|
|
@@ -1092,6 +1206,8 @@ const AUDIT_ACTIONS = [
|
|
|
1092
1206
|
"nonce_mismatch",
|
|
1093
1207
|
"token_exchange_failed",
|
|
1094
1208
|
"whitelist_rejected",
|
|
1209
|
+
"email_not_verified",
|
|
1210
|
+
"id_token_invalid",
|
|
1095
1211
|
"logout",
|
|
1096
1212
|
"session_expired",
|
|
1097
1213
|
"user_created"
|
|
@@ -1291,6 +1407,22 @@ const controllers = {
|
|
|
1291
1407
|
const rateLimitMap = /* @__PURE__ */ new Map();
|
|
1292
1408
|
const RATE_LIMIT_WINDOW = 6e4;
|
|
1293
1409
|
const MAX_REQUESTS = 1e3;
|
|
1410
|
+
const MAX_MAP_SIZE = 1e4;
|
|
1411
|
+
const PRUNE_THRESHOLD = 1e3;
|
|
1412
|
+
function pruneExpiredEntries(now) {
|
|
1413
|
+
const windowStart = now - RATE_LIMIT_WINDOW;
|
|
1414
|
+
for (const [key, stamps] of rateLimitMap) {
|
|
1415
|
+
if (stamps.length === 0 || stamps[stamps.length - 1] <= windowStart) {
|
|
1416
|
+
rateLimitMap.delete(key);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function evictOldestEntry() {
|
|
1421
|
+
const oldest = rateLimitMap.keys().next().value;
|
|
1422
|
+
if (oldest !== void 0) {
|
|
1423
|
+
rateLimitMap.delete(oldest);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1294
1426
|
function getRateLimitKey(ctx) {
|
|
1295
1427
|
const ip = getClientIp(ctx);
|
|
1296
1428
|
const ua = ctx.request.header["user-agent"] ?? "";
|
|
@@ -1301,6 +1433,9 @@ function rateLimitMiddleware(ctx, next) {
|
|
|
1301
1433
|
const key = getRateLimitKey(ctx);
|
|
1302
1434
|
const now = Date.now();
|
|
1303
1435
|
const windowStart = now - RATE_LIMIT_WINDOW;
|
|
1436
|
+
if (rateLimitMap.size > PRUNE_THRESHOLD) {
|
|
1437
|
+
pruneExpiredEntries(now);
|
|
1438
|
+
}
|
|
1304
1439
|
const requestStamps = (rateLimitMap.get(key) ?? []).filter((ts) => ts > windowStart);
|
|
1305
1440
|
if (requestStamps.length >= MAX_REQUESTS) {
|
|
1306
1441
|
ctx.status = 429;
|
|
@@ -1308,6 +1443,9 @@ function rateLimitMiddleware(ctx, next) {
|
|
|
1308
1443
|
return;
|
|
1309
1444
|
}
|
|
1310
1445
|
requestStamps.push(now);
|
|
1446
|
+
if (!rateLimitMap.has(key) && rateLimitMap.size >= MAX_MAP_SIZE) {
|
|
1447
|
+
evictOldestEntry();
|
|
1448
|
+
}
|
|
1311
1449
|
rateLimitMap.set(key, requestStamps);
|
|
1312
1450
|
return next();
|
|
1313
1451
|
}
|
|
@@ -1351,7 +1489,7 @@ const routes = {
|
|
|
1351
1489
|
config: { auth: false, middlewares: [rateLimitMiddleware] }
|
|
1352
1490
|
},
|
|
1353
1491
|
{
|
|
1354
|
-
method: "
|
|
1492
|
+
method: "POST",
|
|
1355
1493
|
path: "/logout",
|
|
1356
1494
|
handler: "oidc.logout",
|
|
1357
1495
|
config: { auth: false }
|
|
@@ -1433,53 +1571,63 @@ const routes = {
|
|
|
1433
1571
|
// API-token-authenticated routes for programmatic whitelist management.
|
|
1434
1572
|
// Accessible at /strapi-plugin-oidc/... using a Strapi API token
|
|
1435
1573
|
// (full-access or custom) in the Authorization: Bearer <token> header.
|
|
1574
|
+
// Custom tokens must be granted one or more of the semantic scopes below.
|
|
1436
1575
|
"content-api": {
|
|
1437
1576
|
type: "content-api",
|
|
1438
1577
|
routes: [
|
|
1439
1578
|
{
|
|
1440
1579
|
method: "GET",
|
|
1441
1580
|
path: "/whitelist",
|
|
1442
|
-
handler: "whitelist.info"
|
|
1581
|
+
handler: "whitelist.info",
|
|
1582
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.read"] } }
|
|
1443
1583
|
},
|
|
1444
1584
|
{
|
|
1445
1585
|
method: "POST",
|
|
1446
1586
|
path: "/whitelist",
|
|
1447
|
-
handler: "whitelist.register"
|
|
1587
|
+
handler: "whitelist.register",
|
|
1588
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.write"] } }
|
|
1448
1589
|
},
|
|
1449
1590
|
{
|
|
1450
1591
|
method: "POST",
|
|
1451
1592
|
path: "/whitelist/import",
|
|
1452
|
-
handler: "whitelist.importUsers"
|
|
1593
|
+
handler: "whitelist.importUsers",
|
|
1594
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.write"] } }
|
|
1453
1595
|
},
|
|
1454
1596
|
{
|
|
1455
1597
|
method: "DELETE",
|
|
1456
1598
|
path: "/whitelist/:email",
|
|
1457
|
-
handler: "whitelist.removeEmail"
|
|
1599
|
+
handler: "whitelist.removeEmail",
|
|
1600
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.delete"] } }
|
|
1458
1601
|
},
|
|
1459
1602
|
{
|
|
1460
1603
|
method: "DELETE",
|
|
1461
1604
|
path: "/whitelist",
|
|
1462
|
-
handler: "whitelist.deleteAll"
|
|
1605
|
+
handler: "whitelist.deleteAll",
|
|
1606
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.delete"] } }
|
|
1463
1607
|
},
|
|
1464
1608
|
{
|
|
1465
1609
|
method: "GET",
|
|
1466
1610
|
path: "/whitelist/export",
|
|
1467
|
-
handler: "whitelist.exportWhitelist"
|
|
1611
|
+
handler: "whitelist.exportWhitelist",
|
|
1612
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.whitelist.read"] } }
|
|
1468
1613
|
},
|
|
1469
1614
|
{
|
|
1470
1615
|
method: "GET",
|
|
1471
1616
|
path: "/audit-logs",
|
|
1472
|
-
handler: "auditLog.find"
|
|
1617
|
+
handler: "auditLog.find",
|
|
1618
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.audit.read"] } }
|
|
1473
1619
|
},
|
|
1474
1620
|
{
|
|
1475
1621
|
method: "GET",
|
|
1476
1622
|
path: "/audit-logs/export",
|
|
1477
|
-
handler: "auditLog.export"
|
|
1623
|
+
handler: "auditLog.export",
|
|
1624
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.audit.read"] } }
|
|
1478
1625
|
},
|
|
1479
1626
|
{
|
|
1480
1627
|
method: "DELETE",
|
|
1481
1628
|
path: "/audit-logs",
|
|
1482
|
-
handler: "auditLog.clearAll"
|
|
1629
|
+
handler: "auditLog.clearAll",
|
|
1630
|
+
config: { auth: { scope: ["plugin::strapi-plugin-oidc.audit.delete"] } }
|
|
1483
1631
|
}
|
|
1484
1632
|
]
|
|
1485
1633
|
}
|
|
@@ -1731,13 +1879,12 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1731
1879
|
type: rememberMe ? "refresh" : "session"
|
|
1732
1880
|
}
|
|
1733
1881
|
);
|
|
1734
|
-
const isProduction = strapi2.config.get("environment") === "production";
|
|
1735
1882
|
const domain = strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain");
|
|
1736
1883
|
const path = strapi2.config.get("admin.auth.cookie.path", "/admin");
|
|
1737
1884
|
const sameSite = strapi2.config.get("admin.auth.cookie.sameSite", "lax");
|
|
1738
1885
|
const cookieOptions = {
|
|
1739
1886
|
httpOnly: true,
|
|
1740
|
-
secure:
|
|
1887
|
+
secure: shouldMarkSecure(strapi2, ctx),
|
|
1741
1888
|
overwrite: true,
|
|
1742
1889
|
domain,
|
|
1743
1890
|
path,
|
|
@@ -1856,14 +2003,6 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1856
2003
|
},
|
|
1857
2004
|
async deleteAllUsers() {
|
|
1858
2005
|
await getWhitelistQuery().deleteMany({});
|
|
1859
|
-
},
|
|
1860
|
-
async countAdminUsersByEmails(emails) {
|
|
1861
|
-
if (emails.length === 0) return 0;
|
|
1862
|
-
const rows = await strapi2.query("admin::user").findMany({
|
|
1863
|
-
where: { email: { $in: emails } },
|
|
1864
|
-
select: ["id"]
|
|
1865
|
-
});
|
|
1866
|
-
return rows.length;
|
|
1867
2006
|
}
|
|
1868
2007
|
};
|
|
1869
2008
|
}
|