strapi-plugin-oidc 1.6.5 → 1.6.6
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 +7 -7
- package/dist/admin/{index-HQ2uuypE.mjs → index-CKyNupYU.mjs} +231 -103
- package/dist/admin/{index-DgUClS5s.mjs → index-D2aMSVmR.mjs} +2 -5
- package/dist/admin/{index-pWwCtdNu.js → index-DUFtPEHD.js} +230 -102
- package/dist/admin/{index-C2BnnDzh.js → index-DVjS4hOr.js} +2 -5
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +238 -162
- package/dist/server/index.mjs +238 -162
- package/package.json +1 -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 node_stream = require("node:stream");
|
|
5
6
|
const strapiUtils = require("@strapi/utils");
|
|
6
7
|
const generator = require("generate-password");
|
|
7
8
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
@@ -36,6 +37,12 @@ function getRetentionDays() {
|
|
|
36
37
|
function isAuditLogEnabled() {
|
|
37
38
|
return getRetentionDays() !== 0;
|
|
38
39
|
}
|
|
40
|
+
const PLUGIN_NAME = "strapi-plugin-oidc";
|
|
41
|
+
const getOauthService = () => strapi.plugin(PLUGIN_NAME).service("oauth");
|
|
42
|
+
const getRoleService = () => strapi.plugin(PLUGIN_NAME).service("role");
|
|
43
|
+
const getWhitelistService = () => strapi.plugin(PLUGIN_NAME).service("whitelist");
|
|
44
|
+
const getAuditLogService = () => strapi.plugin(PLUGIN_NAME).service("auditLog");
|
|
45
|
+
const getAdminUserService = () => strapi.service("admin::user");
|
|
39
46
|
const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
|
|
40
47
|
async function bootstrap({ strapi: strapi2 }) {
|
|
41
48
|
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
@@ -47,7 +54,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
47
54
|
const isTokenRefresh = path === tokenRefreshPath;
|
|
48
55
|
if (isAuthRoute && isPost || isTokenRefresh) {
|
|
49
56
|
try {
|
|
50
|
-
const whitelistService2 =
|
|
57
|
+
const whitelistService2 = getWhitelistService();
|
|
51
58
|
const settings = await whitelistService2.getSettings();
|
|
52
59
|
const enforceOIDC = resolveEnforceOIDC(strapi2, settings?.enforceOIDC);
|
|
53
60
|
if (enforceOIDC && isAuthRoute && isPost) {
|
|
@@ -95,7 +102,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
95
102
|
const enforceOIDCConfig = getEnforceOIDCConfig(strapi2);
|
|
96
103
|
if (enforceOIDCConfig !== null) {
|
|
97
104
|
try {
|
|
98
|
-
const whitelistService2 =
|
|
105
|
+
const whitelistService2 = getWhitelistService();
|
|
99
106
|
const settings = await whitelistService2.getSettings();
|
|
100
107
|
if (settings.enforceOIDC !== enforceOIDCConfig) {
|
|
101
108
|
await whitelistService2.setSettings({ ...settings, enforceOIDC: enforceOIDCConfig });
|
|
@@ -125,7 +132,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
125
132
|
task: async () => {
|
|
126
133
|
try {
|
|
127
134
|
const retentionDays = getRetentionDays();
|
|
128
|
-
await
|
|
135
|
+
await getAuditLogService().cleanup(retentionDays);
|
|
129
136
|
} catch (err) {
|
|
130
137
|
strapi2.log.warn("[strapi-plugin-oidc] Audit log cleanup failed:", err.message);
|
|
131
138
|
}
|
|
@@ -219,9 +226,14 @@ function getExpiredCookieOptions(strapi2, ctx) {
|
|
|
219
226
|
function clearAuthCookies(strapi2, ctx) {
|
|
220
227
|
const options2 = getExpiredCookieOptions(strapi2, ctx);
|
|
221
228
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
229
|
+
const rootPathOptions = { ...options2, path: "/" };
|
|
230
|
+
for (const name of ["oidc_authenticated", "oidc_access_token", "oidc_user_email"]) {
|
|
231
|
+
ctx.cookies.set(name, "", rootPathOptions);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
235
|
+
function isValidEmail(email) {
|
|
236
|
+
return EMAIL_REGEX.test(email);
|
|
225
237
|
}
|
|
226
238
|
const errorCodes = {
|
|
227
239
|
TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
|
|
@@ -358,6 +370,53 @@ const userFacingMessages = {
|
|
|
358
370
|
return en["user.signInError"];
|
|
359
371
|
}
|
|
360
372
|
};
|
|
373
|
+
class OidcError extends Error {
|
|
374
|
+
kind;
|
|
375
|
+
cause;
|
|
376
|
+
constructor(kind, message, cause) {
|
|
377
|
+
super(message);
|
|
378
|
+
this.name = "OidcError";
|
|
379
|
+
this.kind = kind;
|
|
380
|
+
this.cause = cause;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const OIDC_ERROR_DISPATCH = {
|
|
384
|
+
nonce_mismatch: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH },
|
|
385
|
+
token_exchange_failed: {
|
|
386
|
+
action: "token_exchange_failed",
|
|
387
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED
|
|
388
|
+
},
|
|
389
|
+
id_token_parse_failed: {
|
|
390
|
+
action: "login_failure",
|
|
391
|
+
code: errorCodes.ID_TOKEN_PARSE_FAILED,
|
|
392
|
+
key: "id_token_parse_failed"
|
|
393
|
+
},
|
|
394
|
+
userinfo_fetch_failed: {
|
|
395
|
+
action: "login_failure",
|
|
396
|
+
code: errorCodes.USERINFO_FETCH_FAILED,
|
|
397
|
+
key: "userinfo_fetch_failed"
|
|
398
|
+
},
|
|
399
|
+
user_creation_failed: {
|
|
400
|
+
action: "login_failure",
|
|
401
|
+
code: errorCodes.USER_CREATION_FAILED,
|
|
402
|
+
key: "user_creation_failed"
|
|
403
|
+
},
|
|
404
|
+
whitelist_rejected: {
|
|
405
|
+
action: "whitelist_rejected",
|
|
406
|
+
code: errorCodes.WHITELIST_CHECK_FAILED,
|
|
407
|
+
key: "whitelist_rejected"
|
|
408
|
+
},
|
|
409
|
+
invalid_email: {
|
|
410
|
+
action: "login_failure",
|
|
411
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
412
|
+
key: "sign_in_unknown"
|
|
413
|
+
},
|
|
414
|
+
unknown: {
|
|
415
|
+
action: "login_failure",
|
|
416
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
417
|
+
key: "sign_in_unknown"
|
|
418
|
+
}
|
|
419
|
+
};
|
|
361
420
|
const REQUIRED_CONFIG_KEYS = [
|
|
362
421
|
"OIDC_CLIENT_ID",
|
|
363
422
|
"OIDC_CLIENT_SECRET",
|
|
@@ -370,6 +429,7 @@ const REQUIRED_CONFIG_KEYS = [
|
|
|
370
429
|
"OIDC_GIVEN_NAME_FIELD",
|
|
371
430
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
372
431
|
];
|
|
432
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 3e3;
|
|
373
433
|
function configValidation() {
|
|
374
434
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
375
435
|
const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
|
|
@@ -387,7 +447,6 @@ async function oidcSignIn(ctx) {
|
|
|
387
447
|
const cookieOptions = {
|
|
388
448
|
httpOnly: true,
|
|
389
449
|
maxAge: 6e5,
|
|
390
|
-
// 10 minutes
|
|
391
450
|
secure: isProduction && ctx.request.secure,
|
|
392
451
|
sameSite: "lax"
|
|
393
452
|
};
|
|
@@ -417,7 +476,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
417
476
|
}
|
|
418
477
|
});
|
|
419
478
|
if (!response.ok) {
|
|
420
|
-
throw new
|
|
479
|
+
throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
|
|
421
480
|
}
|
|
422
481
|
const tokenData = await response.json();
|
|
423
482
|
if (tokenData.id_token) {
|
|
@@ -425,23 +484,23 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
425
484
|
const payloadB64 = tokenData.id_token.split(".")[1];
|
|
426
485
|
const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
427
486
|
if (idTokenPayload.nonce !== expectedNonce) {
|
|
428
|
-
throw new
|
|
487
|
+
throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
|
|
429
488
|
}
|
|
430
489
|
} catch (e) {
|
|
431
|
-
if (e.
|
|
432
|
-
throw new
|
|
490
|
+
if (e instanceof OidcError && e.kind === "nonce_mismatch") throw e;
|
|
491
|
+
throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
|
|
433
492
|
}
|
|
434
493
|
}
|
|
435
494
|
const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
436
495
|
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
437
496
|
});
|
|
438
497
|
if (!userResponse.ok) {
|
|
439
|
-
throw new
|
|
498
|
+
throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
|
|
440
499
|
}
|
|
441
500
|
const userInfo = await userResponse.json();
|
|
442
501
|
return { userInfo, accessToken: tokenData.access_token };
|
|
443
502
|
}
|
|
444
|
-
function
|
|
503
|
+
function collectGroupMapRoleNames(userInfo, config2) {
|
|
445
504
|
const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
|
|
446
505
|
if (!Array.isArray(rawGroups) || rawGroups.length === 0) return [];
|
|
447
506
|
const groups = rawGroups.filter((g) => typeof g === "string");
|
|
@@ -452,22 +511,15 @@ function resolveRolesFromGroups(userInfo, config2, availableRoles) {
|
|
|
452
511
|
} catch {
|
|
453
512
|
return [];
|
|
454
513
|
}
|
|
455
|
-
const
|
|
514
|
+
const roleNameSet = /* @__PURE__ */ new Set();
|
|
456
515
|
for (const group of groups) {
|
|
457
516
|
const roleNames = groupRoleMap[group];
|
|
458
517
|
if (!roleNames) continue;
|
|
459
518
|
for (const name of roleNames) {
|
|
460
|
-
|
|
461
|
-
if (match) roleIdSet.add(String(match.id));
|
|
519
|
+
roleNameSet.add(name);
|
|
462
520
|
}
|
|
463
521
|
}
|
|
464
|
-
return [...
|
|
465
|
-
}
|
|
466
|
-
async function resolveRoles(userInfo, config2, roleService2, availableRoles) {
|
|
467
|
-
const groupRoles = resolveRolesFromGroups(userInfo, config2, availableRoles);
|
|
468
|
-
if (groupRoles.length > 0) return { roles: groupRoles, fromGroupMapping: true };
|
|
469
|
-
const oidcRoles = await roleService2.oidcRoles();
|
|
470
|
-
return { roles: oidcRoles?.roles || [], fromGroupMapping: false };
|
|
522
|
+
return [...roleNameSet];
|
|
471
523
|
}
|
|
472
524
|
async function registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2) {
|
|
473
525
|
const defaultLocale = oauthService2.localeFindByHeader(
|
|
@@ -485,10 +537,7 @@ async function registerNewUser(oauthService2, email, userResponseData, config2,
|
|
|
485
537
|
}
|
|
486
538
|
function rolesChanged(current, next) {
|
|
487
539
|
if (current.size !== next.size) return true;
|
|
488
|
-
|
|
489
|
-
if (!current.has(id)) return true;
|
|
490
|
-
}
|
|
491
|
-
return false;
|
|
540
|
+
return [...next].some((id) => !current.has(id));
|
|
492
541
|
}
|
|
493
542
|
async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
494
543
|
try {
|
|
@@ -514,27 +563,51 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
|
514
563
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
515
564
|
const rawEmail = String(userResponseData.email ?? "");
|
|
516
565
|
const email = rawEmail.toLowerCase();
|
|
517
|
-
if (!email ||
|
|
518
|
-
throw new
|
|
566
|
+
if (!email || !isValidEmail(email)) {
|
|
567
|
+
throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
|
|
519
568
|
}
|
|
520
569
|
await whitelistService2.checkWhitelistForEmail(email);
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
570
|
+
const candidateNames = collectGroupMapRoleNames(userResponseData, config2);
|
|
571
|
+
let roles2 = [];
|
|
572
|
+
let fromGroupMapping = false;
|
|
573
|
+
let resolvedRoleNames = [];
|
|
574
|
+
if (candidateNames.length > 0) {
|
|
575
|
+
const matchedRoles = await strapi.db.query("admin::role").findMany({
|
|
576
|
+
where: { name: { $in: candidateNames } },
|
|
577
|
+
select: ["id", "name"]
|
|
578
|
+
});
|
|
579
|
+
const nameToId = new Map(matchedRoles.map((r) => [r.name, String(r.id)]));
|
|
580
|
+
for (const name of candidateNames) {
|
|
581
|
+
const id = nameToId.get(name);
|
|
582
|
+
if (id) roles2.push(id);
|
|
583
|
+
}
|
|
584
|
+
resolvedRoleNames = matchedRoles.map((r) => r.name);
|
|
585
|
+
fromGroupMapping = true;
|
|
586
|
+
} else {
|
|
587
|
+
const oidcRolesResult = await roleService2.oidcRoles();
|
|
588
|
+
roles2 = oidcRolesResult?.roles || [];
|
|
589
|
+
if (roles2.length > 0) {
|
|
590
|
+
const oidcRoleRecords = await strapi.db.query("admin::role").findMany({
|
|
591
|
+
where: { id: { $in: roles2.map(Number) } },
|
|
592
|
+
select: ["id", "name"]
|
|
593
|
+
});
|
|
594
|
+
resolvedRoleNames = oidcRoleRecords.map((r) => r.name);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
529
597
|
let userCreated = false;
|
|
530
598
|
let rolesUpdated = false;
|
|
531
599
|
let user = await userService.findOneByEmail(email, ["roles"]);
|
|
532
600
|
if (!user) {
|
|
533
|
-
|
|
601
|
+
try {
|
|
602
|
+
user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
|
|
603
|
+
} catch (e) {
|
|
604
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
605
|
+
throw new OidcError("user_creation_failed", msg, e);
|
|
606
|
+
}
|
|
534
607
|
userCreated = true;
|
|
535
608
|
rolesUpdated = true;
|
|
536
609
|
} else if (fromGroupMapping && roles2.length > 0) {
|
|
537
|
-
const currentRoleIds = new Set(user.roles.map((r) => String(r.id)));
|
|
610
|
+
const currentRoleIds = new Set((user.roles ?? []).map((r) => String(r.id)));
|
|
538
611
|
if (rolesChanged(currentRoleIds, new Set(roles2))) {
|
|
539
612
|
await updateUserRoles(user, currentRoleIds, roles2);
|
|
540
613
|
rolesUpdated = true;
|
|
@@ -544,55 +617,30 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
|
|
|
544
617
|
oauthService2.triggerSignInSuccess(user);
|
|
545
618
|
return { activateUser: user, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
|
|
546
619
|
}
|
|
547
|
-
function classifyOidcError(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
return { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH };
|
|
557
|
-
if (msg === "Token exchange failed")
|
|
558
|
-
return { action: "token_exchange_failed", code: errorCodes.TOKEN_EXCHANGE_FAILED };
|
|
559
|
-
if (msg === "Failed to fetch user info") {
|
|
560
|
-
return {
|
|
561
|
-
action: "login_failure",
|
|
562
|
-
code: errorCodes.USERINFO_FETCH_FAILED,
|
|
563
|
-
key: "userinfo_fetch_failed"
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
if (msg === "Failed to parse ID token") {
|
|
567
|
-
return {
|
|
568
|
-
action: "login_failure",
|
|
569
|
-
code: errorCodes.ID_TOKEN_PARSE_FAILED,
|
|
570
|
-
key: "id_token_parse_failed",
|
|
571
|
-
params: { error: msg }
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
if (msg === "User creation failed" || msg.includes("createUser")) {
|
|
575
|
-
return {
|
|
576
|
-
action: "login_failure",
|
|
577
|
-
code: errorCodes.USER_CREATION_FAILED,
|
|
578
|
-
key: "user_creation_failed",
|
|
579
|
-
params: userInfo?.email ? { email: userInfo.email, error: msg } : void 0
|
|
580
|
-
};
|
|
620
|
+
function classifyOidcError(e, userInfo) {
|
|
621
|
+
const kind = e instanceof OidcError ? e.kind : "unknown";
|
|
622
|
+
const dispatch = OIDC_ERROR_DISPATCH[kind];
|
|
623
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
624
|
+
let params;
|
|
625
|
+
if (kind === "id_token_parse_failed" || kind === "unknown") {
|
|
626
|
+
params = { error: msg };
|
|
627
|
+
} else if (kind === "user_creation_failed" && userInfo?.email) {
|
|
628
|
+
params = { email: userInfo.email, error: msg };
|
|
581
629
|
}
|
|
582
630
|
return {
|
|
583
|
-
action:
|
|
584
|
-
code:
|
|
585
|
-
key:
|
|
586
|
-
params
|
|
631
|
+
action: dispatch.action,
|
|
632
|
+
code: dispatch.code,
|
|
633
|
+
key: dispatch.key,
|
|
634
|
+
params
|
|
587
635
|
};
|
|
588
636
|
}
|
|
589
637
|
async function oidcSignInCallback(ctx) {
|
|
590
638
|
const config2 = configValidation();
|
|
591
|
-
const userService =
|
|
592
|
-
const oauthService2 =
|
|
593
|
-
const roleService2 =
|
|
594
|
-
const whitelistService2 =
|
|
595
|
-
const auditLog2 =
|
|
639
|
+
const userService = getAdminUserService();
|
|
640
|
+
const oauthService2 = getOauthService();
|
|
641
|
+
const roleService2 = getRoleService();
|
|
642
|
+
const whitelistService2 = getWhitelistService();
|
|
643
|
+
const auditLog2 = getAuditLogService();
|
|
596
644
|
if (!ctx.query.code) {
|
|
597
645
|
await auditLog2.log({ action: "missing_code", ip: ctx.ip });
|
|
598
646
|
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
|
|
@@ -624,7 +672,6 @@ async function oidcSignInCallback(ctx) {
|
|
|
624
672
|
ctx.cookies.set("oidc_access_token", accessToken, {
|
|
625
673
|
httpOnly: true,
|
|
626
674
|
maxAge: 3e5,
|
|
627
|
-
// 5 minutes — matches typical provider access token lifetime
|
|
628
675
|
secure: isProduction && ctx.request.secure,
|
|
629
676
|
sameSite: "lax"
|
|
630
677
|
});
|
|
@@ -665,19 +712,18 @@ async function oidcSignInCallback(ctx) {
|
|
|
665
712
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
666
713
|
ctx.send(html);
|
|
667
714
|
} catch (e) {
|
|
668
|
-
const
|
|
669
|
-
const errorInfo = classifyOidcError(msg, userInfo);
|
|
715
|
+
const errorInfo = classifyOidcError(e, userInfo);
|
|
670
716
|
await auditLog2.log({
|
|
671
717
|
action: errorInfo.action,
|
|
672
718
|
email: userInfo?.email,
|
|
673
719
|
ip: ctx.ip,
|
|
674
720
|
detailsKey: errorInfo.action,
|
|
675
|
-
detailsParams: errorInfo.action === "login_failure" ? { message:
|
|
721
|
+
detailsParams: errorInfo.action === "login_failure" ? { message: e instanceof Error ? e.message : String(e) } : void 0
|
|
676
722
|
});
|
|
677
723
|
strapi.log.error({
|
|
678
724
|
code: errorInfo.code,
|
|
679
725
|
phase: "oidc_callback",
|
|
680
|
-
message:
|
|
726
|
+
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
681
727
|
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
682
728
|
email: userInfo?.email
|
|
683
729
|
});
|
|
@@ -686,7 +732,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
686
732
|
}
|
|
687
733
|
async function logout(ctx) {
|
|
688
734
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
689
|
-
const auditLog2 =
|
|
735
|
+
const auditLog2 = getAuditLogService();
|
|
690
736
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
691
737
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
692
738
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
@@ -696,7 +742,8 @@ async function logout(ctx) {
|
|
|
696
742
|
if (logoutUrl && isOidcSession && accessToken) {
|
|
697
743
|
try {
|
|
698
744
|
const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
699
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
745
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
746
|
+
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
700
747
|
});
|
|
701
748
|
if (response.ok) {
|
|
702
749
|
if (userEmail)
|
|
@@ -727,7 +774,7 @@ const oidc = {
|
|
|
727
774
|
logout
|
|
728
775
|
};
|
|
729
776
|
async function find$1(ctx) {
|
|
730
|
-
const roleService2 =
|
|
777
|
+
const roleService2 = getRoleService();
|
|
731
778
|
const roles2 = await roleService2.find();
|
|
732
779
|
const oidcConstants = roleService2.getOidcRoles();
|
|
733
780
|
for (const oidc2 of oidcConstants) {
|
|
@@ -741,7 +788,7 @@ async function find$1(ctx) {
|
|
|
741
788
|
async function update(ctx) {
|
|
742
789
|
try {
|
|
743
790
|
const { roles: roles2 } = ctx.request.body;
|
|
744
|
-
const roleService2 =
|
|
791
|
+
const roleService2 = getRoleService();
|
|
745
792
|
await roleService2.update(roles2);
|
|
746
793
|
ctx.send({}, 204);
|
|
747
794
|
} catch (e) {
|
|
@@ -762,8 +809,17 @@ function formatDatetimeForFilename(date) {
|
|
|
762
809
|
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
763
810
|
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
|
|
764
811
|
}
|
|
765
|
-
function
|
|
766
|
-
|
|
812
|
+
function setJsonAttachmentHeaders(ctx, basename) {
|
|
813
|
+
const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
|
|
814
|
+
ctx.set("Content-Type", "application/json");
|
|
815
|
+
ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.json"`);
|
|
816
|
+
}
|
|
817
|
+
function setNdjsonAttachmentHeaders(ctx, basename) {
|
|
818
|
+
const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
|
|
819
|
+
ctx.set("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
820
|
+
ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.ndjson"`);
|
|
821
|
+
ctx.set("Cache-Control", "no-store");
|
|
822
|
+
ctx.set("X-Content-Type-Options", "nosniff");
|
|
767
823
|
}
|
|
768
824
|
async function info(ctx) {
|
|
769
825
|
const whitelistService2 = getWhitelistService();
|
|
@@ -778,8 +834,9 @@ async function info(ctx) {
|
|
|
778
834
|
};
|
|
779
835
|
}
|
|
780
836
|
async function updateSettings(ctx) {
|
|
781
|
-
const
|
|
782
|
-
|
|
837
|
+
const body = ctx.request.body;
|
|
838
|
+
const { useWhitelist } = body;
|
|
839
|
+
let { enforceOIDC } = body;
|
|
783
840
|
const whitelistService2 = getWhitelistService();
|
|
784
841
|
if (useWhitelist && enforceOIDC) {
|
|
785
842
|
const users = await whitelistService2.getUsers();
|
|
@@ -808,11 +865,9 @@ async function register(ctx) {
|
|
|
808
865
|
const rawEmails = Array.isArray(email) ? email : email.split(",");
|
|
809
866
|
const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
|
|
810
867
|
const whitelistService2 = getWhitelistService();
|
|
811
|
-
|
|
868
|
+
const matchedExistingUsersCount = await whitelistService2.countAdminUsersByEmails(emailList);
|
|
812
869
|
for (const singleEmail of emailList) {
|
|
813
|
-
const
|
|
814
|
-
if (existingUser) matchedExistingUsersCount++;
|
|
815
|
-
const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({ where: { email: singleEmail } });
|
|
870
|
+
const alreadyWhitelisted = await whitelistService2.hasUser(singleEmail);
|
|
816
871
|
if (!alreadyWhitelisted) {
|
|
817
872
|
await whitelistService2.registerUser(singleEmail);
|
|
818
873
|
}
|
|
@@ -826,13 +881,12 @@ async function removeEmail(ctx) {
|
|
|
826
881
|
ctx.body = {};
|
|
827
882
|
}
|
|
828
883
|
async function deleteAll(ctx) {
|
|
829
|
-
|
|
884
|
+
const whitelistService2 = getWhitelistService();
|
|
885
|
+
await whitelistService2.deleteAllUsers();
|
|
830
886
|
ctx.body = {};
|
|
831
887
|
}
|
|
832
888
|
async function exportWhitelist(ctx) {
|
|
833
|
-
|
|
834
|
-
ctx.set("Content-Type", "application/json");
|
|
835
|
-
ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-whitelist-${datetime}.json"`);
|
|
889
|
+
setJsonAttachmentHeaders(ctx, "strapi-oidc-whitelist");
|
|
836
890
|
const whitelistService2 = getWhitelistService();
|
|
837
891
|
const users = await whitelistService2.getUsers();
|
|
838
892
|
ctx.body = users.map((u) => ({ email: u.email }));
|
|
@@ -844,7 +898,7 @@ async function importUsers(ctx) {
|
|
|
844
898
|
ctx.body = { error: "Expected { users: [{email}] }" };
|
|
845
899
|
return;
|
|
846
900
|
}
|
|
847
|
-
const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter(
|
|
901
|
+
const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter(isValidEmail);
|
|
848
902
|
const deduped = [...new Set(normalized)];
|
|
849
903
|
const whitelistService2 = getWhitelistService();
|
|
850
904
|
const existing = await whitelistService2.getUsers();
|
|
@@ -859,7 +913,7 @@ async function importUsers(ctx) {
|
|
|
859
913
|
}
|
|
860
914
|
async function syncUsers(ctx) {
|
|
861
915
|
const { users: rawUsers } = ctx.request.body;
|
|
862
|
-
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(
|
|
916
|
+
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(isValidEmail);
|
|
863
917
|
const whitelistService2 = getWhitelistService();
|
|
864
918
|
const currentUsers = await whitelistService2.getUsers();
|
|
865
919
|
const syncEmailSet = new Set(emails);
|
|
@@ -887,44 +941,49 @@ const whitelist = {
|
|
|
887
941
|
importUsers,
|
|
888
942
|
exportWhitelist
|
|
889
943
|
};
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
async function find(ctx) {
|
|
894
|
-
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
895
|
-
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
|
|
896
|
-
ctx.body = await getAuditLogService().find({ page, pageSize });
|
|
897
|
-
}
|
|
898
|
-
async function exportLogs(ctx) {
|
|
899
|
-
const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
|
|
900
|
-
ctx.set("Content-Type", "application/json");
|
|
901
|
-
ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-audit-log-${datetime}.json"`);
|
|
902
|
-
const service = getAuditLogService();
|
|
903
|
-
const PAGE_SIZE = 1e3;
|
|
904
|
-
const allRows = [];
|
|
944
|
+
const EXPORT_PAGE_SIZE = 500;
|
|
945
|
+
async function* ndjsonRowStream(service) {
|
|
905
946
|
let page = 1;
|
|
906
947
|
while (true) {
|
|
907
|
-
const { results } = await service.find({ page, pageSize:
|
|
948
|
+
const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE });
|
|
949
|
+
if (results.length === 0) return;
|
|
950
|
+
let chunk = "";
|
|
908
951
|
for (const row of results) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
createdAt: row.createdAt,
|
|
952
|
+
chunk += JSON.stringify({
|
|
953
|
+
datetime: row.createdAt,
|
|
912
954
|
action: row.action,
|
|
913
955
|
email: row.email ?? null,
|
|
914
956
|
ip: row.ip ?? null,
|
|
915
957
|
details: row.details
|
|
916
|
-
});
|
|
958
|
+
}) + "\n";
|
|
917
959
|
}
|
|
918
|
-
|
|
960
|
+
yield Buffer.from(chunk, "utf8");
|
|
961
|
+
if (results.length < EXPORT_PAGE_SIZE) return;
|
|
919
962
|
page++;
|
|
920
963
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
})
|
|
964
|
+
}
|
|
965
|
+
function errorAwareNdjsonStream(service) {
|
|
966
|
+
const gen = ndjsonRowStream(service);
|
|
967
|
+
const readable = node_stream.Readable.from(gen);
|
|
968
|
+
readable.on("error", (err) => {
|
|
969
|
+
strapi$1.log.error({ phase: "audit_log_export", err }, "NDJSON export stream failed");
|
|
970
|
+
});
|
|
971
|
+
return readable;
|
|
972
|
+
}
|
|
973
|
+
let strapi$1;
|
|
974
|
+
function find(ctx) {
|
|
975
|
+
strapi$1 = ctx.strapi;
|
|
976
|
+
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
977
|
+
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
|
|
978
|
+
return getAuditLogService().find({ page, pageSize }).then((result) => {
|
|
979
|
+
ctx.body = result;
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
async function exportLogs(ctx) {
|
|
983
|
+
strapi$1 = ctx.strapi;
|
|
984
|
+
setNdjsonAttachmentHeaders(ctx, "strapi-oidc-audit-log");
|
|
985
|
+
const service = getAuditLogService();
|
|
986
|
+
ctx.body = errorAwareNdjsonStream(service);
|
|
928
987
|
}
|
|
929
988
|
async function clearAll(ctx) {
|
|
930
989
|
await getAuditLogService().clearAll();
|
|
@@ -1159,7 +1218,7 @@ function renderHtmlTemplate(title, content) {
|
|
|
1159
1218
|
--icon-color: #d02b20;
|
|
1160
1219
|
--success-bg: #eafbe7;
|
|
1161
1220
|
--success-color: #328048;
|
|
1162
|
-
--shadow: 0 1px
|
|
1221
|
+
--shadow: 0 1px 4 rgba(33, 33, 52, 0.1);
|
|
1163
1222
|
}
|
|
1164
1223
|
@media (prefers-color-scheme: dark) {
|
|
1165
1224
|
:root {
|
|
@@ -1174,7 +1233,7 @@ function renderHtmlTemplate(title, content) {
|
|
|
1174
1233
|
--icon-color: #f23628;
|
|
1175
1234
|
--success-bg: #1c3523;
|
|
1176
1235
|
--success-color: #55ca76;
|
|
1177
|
-
--shadow: 0 1px
|
|
1236
|
+
--shadow: 0 1px 4 rgba(0, 0, 0, 0.5);
|
|
1178
1237
|
}
|
|
1179
1238
|
}
|
|
1180
1239
|
body {
|
|
@@ -1259,14 +1318,11 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1259
1318
|
return {
|
|
1260
1319
|
async createUser(email, lastname, firstname, locale, roles2 = []) {
|
|
1261
1320
|
const userService = strapi2.service("admin::user");
|
|
1262
|
-
|
|
1263
|
-
const dbUser = await userService.findOneByEmail(email.toLocaleLowerCase());
|
|
1264
|
-
if (dbUser) return dbUser;
|
|
1265
|
-
}
|
|
1321
|
+
const normalizedEmail = email.toLowerCase();
|
|
1266
1322
|
const createdUser = await userService.create({
|
|
1267
1323
|
firstname: firstname || "unset",
|
|
1268
1324
|
lastname: lastname || "",
|
|
1269
|
-
email:
|
|
1325
|
+
email: normalizedEmail,
|
|
1270
1326
|
roles: roles2,
|
|
1271
1327
|
preferedLanguage: locale
|
|
1272
1328
|
});
|
|
@@ -1297,35 +1353,35 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1297
1353
|
},
|
|
1298
1354
|
async triggerWebHook(user) {
|
|
1299
1355
|
let ENTRY_CREATE;
|
|
1300
|
-
const webhookStore = strapi2.serviceMap
|
|
1301
|
-
const eventHub = strapi2.serviceMap
|
|
1356
|
+
const webhookStore = strapi2.serviceMap?.get("webhookStore");
|
|
1357
|
+
const eventHub = strapi2.serviceMap?.get("eventHub");
|
|
1302
1358
|
if (webhookStore) {
|
|
1303
1359
|
ENTRY_CREATE = webhookStore.allowedEvents.get("ENTRY_CREATE");
|
|
1304
1360
|
}
|
|
1305
1361
|
const modelDef = strapi2.getModel("admin::user");
|
|
1306
1362
|
const sanitizedEntity = await strapiUtils__default.default.sanitize.sanitizers.defaultSanitizeOutput(
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
},
|
|
1363
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1364
|
+
{ schema: modelDef, getModel: (uid2) => strapi2.getModel(uid2) },
|
|
1365
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1311
1366
|
user
|
|
1312
1367
|
);
|
|
1313
|
-
eventHub
|
|
1368
|
+
eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
|
|
1314
1369
|
model: modelDef.modelName,
|
|
1315
1370
|
entry: sanitizedEntity
|
|
1316
1371
|
});
|
|
1317
1372
|
},
|
|
1318
1373
|
triggerSignInSuccess(user) {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
eventHub.
|
|
1322
|
-
|
|
1374
|
+
const userCopy = { ...user };
|
|
1375
|
+
delete userCopy.password;
|
|
1376
|
+
const eventHub = strapi2.serviceMap?.get("eventHub");
|
|
1377
|
+
eventHub?.emit("admin.auth.success", {
|
|
1378
|
+
user: userCopy,
|
|
1323
1379
|
provider: "strapi-plugin-oidc"
|
|
1324
1380
|
});
|
|
1325
1381
|
},
|
|
1326
1382
|
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
1327
1383
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1328
|
-
const isRememberMe = !!config2
|
|
1384
|
+
const isRememberMe = !!config2?.REMEMBER_ME;
|
|
1329
1385
|
const content = `
|
|
1330
1386
|
<noscript>
|
|
1331
1387
|
<div class="card">
|
|
@@ -1376,12 +1432,15 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1376
1432
|
const userId = String(user.id);
|
|
1377
1433
|
const deviceId = node_crypto.randomUUID();
|
|
1378
1434
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1379
|
-
const rememberMe = !!config2
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1435
|
+
const rememberMe = !!config2?.REMEMBER_ME;
|
|
1436
|
+
const smAdmin = sessionManager("admin");
|
|
1437
|
+
const { token: refreshToken, absoluteExpiresAt } = await smAdmin.generateRefreshToken(
|
|
1438
|
+
userId,
|
|
1439
|
+
deviceId,
|
|
1440
|
+
{
|
|
1441
|
+
type: rememberMe ? "refresh" : "session"
|
|
1442
|
+
}
|
|
1443
|
+
);
|
|
1385
1444
|
const isProduction = strapi2.config.get("environment") === "production";
|
|
1386
1445
|
const domain = strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain");
|
|
1387
1446
|
const path = strapi2.config.get("admin.auth.cookie.path", "/admin");
|
|
@@ -1398,7 +1457,6 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1398
1457
|
const idleLifespanSec = strapi2.config.get(
|
|
1399
1458
|
"admin.auth.sessions.idleRefreshTokenLifespan",
|
|
1400
1459
|
1209600
|
|
1401
|
-
// 14 days — Strapi default
|
|
1402
1460
|
);
|
|
1403
1461
|
const idleMs = idleLifespanSec * 1e3;
|
|
1404
1462
|
const absoluteMs = new Date(absoluteExpiresAt).getTime() - Date.now();
|
|
@@ -1408,7 +1466,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1408
1466
|
}
|
|
1409
1467
|
ctx.cookies.set("strapi_admin_refresh", refreshToken, cookieOptions);
|
|
1410
1468
|
ctx.cookies.set("oidc_authenticated", "1", { ...cookieOptions, path: "/" });
|
|
1411
|
-
const accessResult = await
|
|
1469
|
+
const accessResult = await smAdmin.generateAccessToken(refreshToken);
|
|
1412
1470
|
if ("error" in accessResult) {
|
|
1413
1471
|
throw new Error(accessResult.error);
|
|
1414
1472
|
}
|
|
@@ -1499,8 +1557,23 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1499
1557
|
const result = await getWhitelistQuery().findOne({
|
|
1500
1558
|
where: { email }
|
|
1501
1559
|
});
|
|
1502
|
-
if (!result) throw new
|
|
1560
|
+
if (!result) throw new OidcError("whitelist_rejected", errorMessages.WHITELIST_NOT_PRESENT);
|
|
1503
1561
|
return result;
|
|
1562
|
+
},
|
|
1563
|
+
async hasUser(email) {
|
|
1564
|
+
const row = await getWhitelistQuery().findOne({ where: { email }, select: ["id"] });
|
|
1565
|
+
return !!row;
|
|
1566
|
+
},
|
|
1567
|
+
async deleteAllUsers() {
|
|
1568
|
+
await getWhitelistQuery().deleteMany({});
|
|
1569
|
+
},
|
|
1570
|
+
async countAdminUsersByEmails(emails) {
|
|
1571
|
+
if (emails.length === 0) return 0;
|
|
1572
|
+
const rows = await strapi2.query("admin::user").findMany({
|
|
1573
|
+
where: { email: { $in: emails } },
|
|
1574
|
+
select: ["id"]
|
|
1575
|
+
});
|
|
1576
|
+
return rows.length;
|
|
1504
1577
|
}
|
|
1505
1578
|
};
|
|
1506
1579
|
}
|
|
@@ -1535,7 +1608,10 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1535
1608
|
});
|
|
1536
1609
|
}
|
|
1537
1610
|
},
|
|
1538
|
-
async find({
|
|
1611
|
+
async find({
|
|
1612
|
+
page = 1,
|
|
1613
|
+
pageSize = 25
|
|
1614
|
+
} = {}) {
|
|
1539
1615
|
const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").findPage({
|
|
1540
1616
|
sort: { createdAt: "desc" },
|
|
1541
1617
|
page,
|