strapi-plugin-oidc 1.6.4 → 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-MnV7H8G6.mjs → index-CKyNupYU.mjs} +231 -103
- package/dist/admin/{index-AxBC5YLT.mjs → index-D2aMSVmR.mjs} +2 -8
- package/dist/admin/{index-DowwUs07.js → index-DUFtPEHD.js} +230 -102
- package/dist/admin/{index-EAfqxfV4.js → index-DVjS4hOr.js} +2 -8
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +238 -165
- package/dist/server/index.mjs +238 -165
- 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",
|
|
@@ -293,8 +305,6 @@ const en = {
|
|
|
293
305
|
"whitelist.toggle.enabled": "Enabled",
|
|
294
306
|
"whitelist.toggle.disabled": "Disabled",
|
|
295
307
|
"whitelist.email.placeholder": "Email address",
|
|
296
|
-
"whitelist.roles.placeholder": "Select specific role(s)",
|
|
297
|
-
"whitelist.table.roles": "Role(s)",
|
|
298
308
|
"whitelist.table.empty": "No email addresses",
|
|
299
309
|
"whitelist.delete.label": "Delete",
|
|
300
310
|
"page.title.oidc": "OIDC",
|
|
@@ -318,7 +328,6 @@ const en = {
|
|
|
318
328
|
"unsaved.description": "You have unsaved changes that will be lost if you leave. Do you want to continue?",
|
|
319
329
|
"unsaved.confirm": "Leave",
|
|
320
330
|
"unsaved.cancel": "Stay",
|
|
321
|
-
"whitelist.table.roles.default": "(Default)",
|
|
322
331
|
"auditlog.title": "Audit Logs",
|
|
323
332
|
"auditlog.export": "Download",
|
|
324
333
|
"auditlog.table.timestamp": "Timestamp",
|
|
@@ -361,6 +370,53 @@ const userFacingMessages = {
|
|
|
361
370
|
return en["user.signInError"];
|
|
362
371
|
}
|
|
363
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
|
+
};
|
|
364
420
|
const REQUIRED_CONFIG_KEYS = [
|
|
365
421
|
"OIDC_CLIENT_ID",
|
|
366
422
|
"OIDC_CLIENT_SECRET",
|
|
@@ -373,6 +429,7 @@ const REQUIRED_CONFIG_KEYS = [
|
|
|
373
429
|
"OIDC_GIVEN_NAME_FIELD",
|
|
374
430
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
375
431
|
];
|
|
432
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 3e3;
|
|
376
433
|
function configValidation() {
|
|
377
434
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
378
435
|
const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
|
|
@@ -390,7 +447,6 @@ async function oidcSignIn(ctx) {
|
|
|
390
447
|
const cookieOptions = {
|
|
391
448
|
httpOnly: true,
|
|
392
449
|
maxAge: 6e5,
|
|
393
|
-
// 10 minutes
|
|
394
450
|
secure: isProduction && ctx.request.secure,
|
|
395
451
|
sameSite: "lax"
|
|
396
452
|
};
|
|
@@ -420,7 +476,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
420
476
|
}
|
|
421
477
|
});
|
|
422
478
|
if (!response.ok) {
|
|
423
|
-
throw new
|
|
479
|
+
throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
|
|
424
480
|
}
|
|
425
481
|
const tokenData = await response.json();
|
|
426
482
|
if (tokenData.id_token) {
|
|
@@ -428,23 +484,23 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
428
484
|
const payloadB64 = tokenData.id_token.split(".")[1];
|
|
429
485
|
const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
430
486
|
if (idTokenPayload.nonce !== expectedNonce) {
|
|
431
|
-
throw new
|
|
487
|
+
throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
|
|
432
488
|
}
|
|
433
489
|
} catch (e) {
|
|
434
|
-
if (e.
|
|
435
|
-
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);
|
|
436
492
|
}
|
|
437
493
|
}
|
|
438
494
|
const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
439
495
|
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
440
496
|
});
|
|
441
497
|
if (!userResponse.ok) {
|
|
442
|
-
throw new
|
|
498
|
+
throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
|
|
443
499
|
}
|
|
444
500
|
const userInfo = await userResponse.json();
|
|
445
501
|
return { userInfo, accessToken: tokenData.access_token };
|
|
446
502
|
}
|
|
447
|
-
function
|
|
503
|
+
function collectGroupMapRoleNames(userInfo, config2) {
|
|
448
504
|
const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
|
|
449
505
|
if (!Array.isArray(rawGroups) || rawGroups.length === 0) return [];
|
|
450
506
|
const groups = rawGroups.filter((g) => typeof g === "string");
|
|
@@ -455,22 +511,15 @@ function resolveRolesFromGroups(userInfo, config2, availableRoles) {
|
|
|
455
511
|
} catch {
|
|
456
512
|
return [];
|
|
457
513
|
}
|
|
458
|
-
const
|
|
514
|
+
const roleNameSet = /* @__PURE__ */ new Set();
|
|
459
515
|
for (const group of groups) {
|
|
460
516
|
const roleNames = groupRoleMap[group];
|
|
461
517
|
if (!roleNames) continue;
|
|
462
518
|
for (const name of roleNames) {
|
|
463
|
-
|
|
464
|
-
if (match) roleIdSet.add(String(match.id));
|
|
519
|
+
roleNameSet.add(name);
|
|
465
520
|
}
|
|
466
521
|
}
|
|
467
|
-
return [...
|
|
468
|
-
}
|
|
469
|
-
async function resolveRoles(userInfo, config2, roleService2, availableRoles) {
|
|
470
|
-
const groupRoles = resolveRolesFromGroups(userInfo, config2, availableRoles);
|
|
471
|
-
if (groupRoles.length > 0) return { roles: groupRoles, fromGroupMapping: true };
|
|
472
|
-
const oidcRoles = await roleService2.oidcRoles();
|
|
473
|
-
return { roles: oidcRoles?.roles || [], fromGroupMapping: false };
|
|
522
|
+
return [...roleNameSet];
|
|
474
523
|
}
|
|
475
524
|
async function registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2) {
|
|
476
525
|
const defaultLocale = oauthService2.localeFindByHeader(
|
|
@@ -488,10 +537,7 @@ async function registerNewUser(oauthService2, email, userResponseData, config2,
|
|
|
488
537
|
}
|
|
489
538
|
function rolesChanged(current, next) {
|
|
490
539
|
if (current.size !== next.size) return true;
|
|
491
|
-
|
|
492
|
-
if (!current.has(id)) return true;
|
|
493
|
-
}
|
|
494
|
-
return false;
|
|
540
|
+
return [...next].some((id) => !current.has(id));
|
|
495
541
|
}
|
|
496
542
|
async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
497
543
|
try {
|
|
@@ -517,27 +563,51 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
|
517
563
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
518
564
|
const rawEmail = String(userResponseData.email ?? "");
|
|
519
565
|
const email = rawEmail.toLowerCase();
|
|
520
|
-
if (!email ||
|
|
521
|
-
throw new
|
|
566
|
+
if (!email || !isValidEmail(email)) {
|
|
567
|
+
throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
|
|
522
568
|
}
|
|
523
569
|
await whitelistService2.checkWhitelistForEmail(email);
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
+
}
|
|
532
597
|
let userCreated = false;
|
|
533
598
|
let rolesUpdated = false;
|
|
534
599
|
let user = await userService.findOneByEmail(email, ["roles"]);
|
|
535
600
|
if (!user) {
|
|
536
|
-
|
|
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
|
+
}
|
|
537
607
|
userCreated = true;
|
|
538
608
|
rolesUpdated = true;
|
|
539
609
|
} else if (fromGroupMapping && roles2.length > 0) {
|
|
540
|
-
const currentRoleIds = new Set(user.roles.map((r) => String(r.id)));
|
|
610
|
+
const currentRoleIds = new Set((user.roles ?? []).map((r) => String(r.id)));
|
|
541
611
|
if (rolesChanged(currentRoleIds, new Set(roles2))) {
|
|
542
612
|
await updateUserRoles(user, currentRoleIds, roles2);
|
|
543
613
|
rolesUpdated = true;
|
|
@@ -547,55 +617,30 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
|
|
|
547
617
|
oauthService2.triggerSignInSuccess(user);
|
|
548
618
|
return { activateUser: user, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
|
|
549
619
|
}
|
|
550
|
-
function classifyOidcError(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
return { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH };
|
|
560
|
-
if (msg === "Token exchange failed")
|
|
561
|
-
return { action: "token_exchange_failed", code: errorCodes.TOKEN_EXCHANGE_FAILED };
|
|
562
|
-
if (msg === "Failed to fetch user info") {
|
|
563
|
-
return {
|
|
564
|
-
action: "login_failure",
|
|
565
|
-
code: errorCodes.USERINFO_FETCH_FAILED,
|
|
566
|
-
key: "userinfo_fetch_failed"
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
if (msg === "Failed to parse ID token") {
|
|
570
|
-
return {
|
|
571
|
-
action: "login_failure",
|
|
572
|
-
code: errorCodes.ID_TOKEN_PARSE_FAILED,
|
|
573
|
-
key: "id_token_parse_failed",
|
|
574
|
-
params: { error: msg }
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
if (msg === "User creation failed" || msg.includes("createUser")) {
|
|
578
|
-
return {
|
|
579
|
-
action: "login_failure",
|
|
580
|
-
code: errorCodes.USER_CREATION_FAILED,
|
|
581
|
-
key: "user_creation_failed",
|
|
582
|
-
params: userInfo?.email ? { email: userInfo.email, error: msg } : void 0
|
|
583
|
-
};
|
|
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 };
|
|
584
629
|
}
|
|
585
630
|
return {
|
|
586
|
-
action:
|
|
587
|
-
code:
|
|
588
|
-
key:
|
|
589
|
-
params
|
|
631
|
+
action: dispatch.action,
|
|
632
|
+
code: dispatch.code,
|
|
633
|
+
key: dispatch.key,
|
|
634
|
+
params
|
|
590
635
|
};
|
|
591
636
|
}
|
|
592
637
|
async function oidcSignInCallback(ctx) {
|
|
593
638
|
const config2 = configValidation();
|
|
594
|
-
const userService =
|
|
595
|
-
const oauthService2 =
|
|
596
|
-
const roleService2 =
|
|
597
|
-
const whitelistService2 =
|
|
598
|
-
const auditLog2 =
|
|
639
|
+
const userService = getAdminUserService();
|
|
640
|
+
const oauthService2 = getOauthService();
|
|
641
|
+
const roleService2 = getRoleService();
|
|
642
|
+
const whitelistService2 = getWhitelistService();
|
|
643
|
+
const auditLog2 = getAuditLogService();
|
|
599
644
|
if (!ctx.query.code) {
|
|
600
645
|
await auditLog2.log({ action: "missing_code", ip: ctx.ip });
|
|
601
646
|
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
|
|
@@ -627,7 +672,6 @@ async function oidcSignInCallback(ctx) {
|
|
|
627
672
|
ctx.cookies.set("oidc_access_token", accessToken, {
|
|
628
673
|
httpOnly: true,
|
|
629
674
|
maxAge: 3e5,
|
|
630
|
-
// 5 minutes — matches typical provider access token lifetime
|
|
631
675
|
secure: isProduction && ctx.request.secure,
|
|
632
676
|
sameSite: "lax"
|
|
633
677
|
});
|
|
@@ -668,19 +712,18 @@ async function oidcSignInCallback(ctx) {
|
|
|
668
712
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
669
713
|
ctx.send(html);
|
|
670
714
|
} catch (e) {
|
|
671
|
-
const
|
|
672
|
-
const errorInfo = classifyOidcError(msg, userInfo);
|
|
715
|
+
const errorInfo = classifyOidcError(e, userInfo);
|
|
673
716
|
await auditLog2.log({
|
|
674
717
|
action: errorInfo.action,
|
|
675
718
|
email: userInfo?.email,
|
|
676
719
|
ip: ctx.ip,
|
|
677
720
|
detailsKey: errorInfo.action,
|
|
678
|
-
detailsParams: errorInfo.action === "login_failure" ? { message:
|
|
721
|
+
detailsParams: errorInfo.action === "login_failure" ? { message: e instanceof Error ? e.message : String(e) } : void 0
|
|
679
722
|
});
|
|
680
723
|
strapi.log.error({
|
|
681
724
|
code: errorInfo.code,
|
|
682
725
|
phase: "oidc_callback",
|
|
683
|
-
message:
|
|
726
|
+
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
684
727
|
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
685
728
|
email: userInfo?.email
|
|
686
729
|
});
|
|
@@ -689,7 +732,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
689
732
|
}
|
|
690
733
|
async function logout(ctx) {
|
|
691
734
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
692
|
-
const auditLog2 =
|
|
735
|
+
const auditLog2 = getAuditLogService();
|
|
693
736
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
694
737
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
695
738
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
@@ -699,7 +742,8 @@ async function logout(ctx) {
|
|
|
699
742
|
if (logoutUrl && isOidcSession && accessToken) {
|
|
700
743
|
try {
|
|
701
744
|
const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
702
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
745
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
746
|
+
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
703
747
|
});
|
|
704
748
|
if (response.ok) {
|
|
705
749
|
if (userEmail)
|
|
@@ -730,7 +774,7 @@ const oidc = {
|
|
|
730
774
|
logout
|
|
731
775
|
};
|
|
732
776
|
async function find$1(ctx) {
|
|
733
|
-
const roleService2 =
|
|
777
|
+
const roleService2 = getRoleService();
|
|
734
778
|
const roles2 = await roleService2.find();
|
|
735
779
|
const oidcConstants = roleService2.getOidcRoles();
|
|
736
780
|
for (const oidc2 of oidcConstants) {
|
|
@@ -744,7 +788,7 @@ async function find$1(ctx) {
|
|
|
744
788
|
async function update(ctx) {
|
|
745
789
|
try {
|
|
746
790
|
const { roles: roles2 } = ctx.request.body;
|
|
747
|
-
const roleService2 =
|
|
791
|
+
const roleService2 = getRoleService();
|
|
748
792
|
await roleService2.update(roles2);
|
|
749
793
|
ctx.send({}, 204);
|
|
750
794
|
} catch (e) {
|
|
@@ -765,8 +809,17 @@ function formatDatetimeForFilename(date) {
|
|
|
765
809
|
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
766
810
|
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
|
|
767
811
|
}
|
|
768
|
-
function
|
|
769
|
-
|
|
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");
|
|
770
823
|
}
|
|
771
824
|
async function info(ctx) {
|
|
772
825
|
const whitelistService2 = getWhitelistService();
|
|
@@ -781,8 +834,9 @@ async function info(ctx) {
|
|
|
781
834
|
};
|
|
782
835
|
}
|
|
783
836
|
async function updateSettings(ctx) {
|
|
784
|
-
const
|
|
785
|
-
|
|
837
|
+
const body = ctx.request.body;
|
|
838
|
+
const { useWhitelist } = body;
|
|
839
|
+
let { enforceOIDC } = body;
|
|
786
840
|
const whitelistService2 = getWhitelistService();
|
|
787
841
|
if (useWhitelist && enforceOIDC) {
|
|
788
842
|
const users = await whitelistService2.getUsers();
|
|
@@ -811,11 +865,9 @@ async function register(ctx) {
|
|
|
811
865
|
const rawEmails = Array.isArray(email) ? email : email.split(",");
|
|
812
866
|
const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
|
|
813
867
|
const whitelistService2 = getWhitelistService();
|
|
814
|
-
|
|
868
|
+
const matchedExistingUsersCount = await whitelistService2.countAdminUsersByEmails(emailList);
|
|
815
869
|
for (const singleEmail of emailList) {
|
|
816
|
-
const
|
|
817
|
-
if (existingUser) matchedExistingUsersCount++;
|
|
818
|
-
const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({ where: { email: singleEmail } });
|
|
870
|
+
const alreadyWhitelisted = await whitelistService2.hasUser(singleEmail);
|
|
819
871
|
if (!alreadyWhitelisted) {
|
|
820
872
|
await whitelistService2.registerUser(singleEmail);
|
|
821
873
|
}
|
|
@@ -829,13 +881,12 @@ async function removeEmail(ctx) {
|
|
|
829
881
|
ctx.body = {};
|
|
830
882
|
}
|
|
831
883
|
async function deleteAll(ctx) {
|
|
832
|
-
|
|
884
|
+
const whitelistService2 = getWhitelistService();
|
|
885
|
+
await whitelistService2.deleteAllUsers();
|
|
833
886
|
ctx.body = {};
|
|
834
887
|
}
|
|
835
888
|
async function exportWhitelist(ctx) {
|
|
836
|
-
|
|
837
|
-
ctx.set("Content-Type", "application/json");
|
|
838
|
-
ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-whitelist-${datetime}.json"`);
|
|
889
|
+
setJsonAttachmentHeaders(ctx, "strapi-oidc-whitelist");
|
|
839
890
|
const whitelistService2 = getWhitelistService();
|
|
840
891
|
const users = await whitelistService2.getUsers();
|
|
841
892
|
ctx.body = users.map((u) => ({ email: u.email }));
|
|
@@ -847,7 +898,7 @@ async function importUsers(ctx) {
|
|
|
847
898
|
ctx.body = { error: "Expected { users: [{email}] }" };
|
|
848
899
|
return;
|
|
849
900
|
}
|
|
850
|
-
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);
|
|
851
902
|
const deduped = [...new Set(normalized)];
|
|
852
903
|
const whitelistService2 = getWhitelistService();
|
|
853
904
|
const existing = await whitelistService2.getUsers();
|
|
@@ -862,7 +913,7 @@ async function importUsers(ctx) {
|
|
|
862
913
|
}
|
|
863
914
|
async function syncUsers(ctx) {
|
|
864
915
|
const { users: rawUsers } = ctx.request.body;
|
|
865
|
-
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(
|
|
916
|
+
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(isValidEmail);
|
|
866
917
|
const whitelistService2 = getWhitelistService();
|
|
867
918
|
const currentUsers = await whitelistService2.getUsers();
|
|
868
919
|
const syncEmailSet = new Set(emails);
|
|
@@ -890,44 +941,49 @@ const whitelist = {
|
|
|
890
941
|
importUsers,
|
|
891
942
|
exportWhitelist
|
|
892
943
|
};
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
async function find(ctx) {
|
|
897
|
-
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
898
|
-
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
|
|
899
|
-
ctx.body = await getAuditLogService().find({ page, pageSize });
|
|
900
|
-
}
|
|
901
|
-
async function exportLogs(ctx) {
|
|
902
|
-
const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
|
|
903
|
-
ctx.set("Content-Type", "application/json");
|
|
904
|
-
ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-audit-log-${datetime}.json"`);
|
|
905
|
-
const service = getAuditLogService();
|
|
906
|
-
const PAGE_SIZE = 1e3;
|
|
907
|
-
const allRows = [];
|
|
944
|
+
const EXPORT_PAGE_SIZE = 500;
|
|
945
|
+
async function* ndjsonRowStream(service) {
|
|
908
946
|
let page = 1;
|
|
909
947
|
while (true) {
|
|
910
|
-
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 = "";
|
|
911
951
|
for (const row of results) {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
createdAt: row.createdAt,
|
|
952
|
+
chunk += JSON.stringify({
|
|
953
|
+
datetime: row.createdAt,
|
|
915
954
|
action: row.action,
|
|
916
955
|
email: row.email ?? null,
|
|
917
956
|
ip: row.ip ?? null,
|
|
918
957
|
details: row.details
|
|
919
|
-
});
|
|
958
|
+
}) + "\n";
|
|
920
959
|
}
|
|
921
|
-
|
|
960
|
+
yield Buffer.from(chunk, "utf8");
|
|
961
|
+
if (results.length < EXPORT_PAGE_SIZE) return;
|
|
922
962
|
page++;
|
|
923
963
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
})
|
|
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);
|
|
931
987
|
}
|
|
932
988
|
async function clearAll(ctx) {
|
|
933
989
|
await getAuditLogService().clearAll();
|
|
@@ -1162,7 +1218,7 @@ function renderHtmlTemplate(title, content) {
|
|
|
1162
1218
|
--icon-color: #d02b20;
|
|
1163
1219
|
--success-bg: #eafbe7;
|
|
1164
1220
|
--success-color: #328048;
|
|
1165
|
-
--shadow: 0 1px
|
|
1221
|
+
--shadow: 0 1px 4 rgba(33, 33, 52, 0.1);
|
|
1166
1222
|
}
|
|
1167
1223
|
@media (prefers-color-scheme: dark) {
|
|
1168
1224
|
:root {
|
|
@@ -1177,7 +1233,7 @@ function renderHtmlTemplate(title, content) {
|
|
|
1177
1233
|
--icon-color: #f23628;
|
|
1178
1234
|
--success-bg: #1c3523;
|
|
1179
1235
|
--success-color: #55ca76;
|
|
1180
|
-
--shadow: 0 1px
|
|
1236
|
+
--shadow: 0 1px 4 rgba(0, 0, 0, 0.5);
|
|
1181
1237
|
}
|
|
1182
1238
|
}
|
|
1183
1239
|
body {
|
|
@@ -1262,14 +1318,11 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1262
1318
|
return {
|
|
1263
1319
|
async createUser(email, lastname, firstname, locale, roles2 = []) {
|
|
1264
1320
|
const userService = strapi2.service("admin::user");
|
|
1265
|
-
|
|
1266
|
-
const dbUser = await userService.findOneByEmail(email.toLocaleLowerCase());
|
|
1267
|
-
if (dbUser) return dbUser;
|
|
1268
|
-
}
|
|
1321
|
+
const normalizedEmail = email.toLowerCase();
|
|
1269
1322
|
const createdUser = await userService.create({
|
|
1270
1323
|
firstname: firstname || "unset",
|
|
1271
1324
|
lastname: lastname || "",
|
|
1272
|
-
email:
|
|
1325
|
+
email: normalizedEmail,
|
|
1273
1326
|
roles: roles2,
|
|
1274
1327
|
preferedLanguage: locale
|
|
1275
1328
|
});
|
|
@@ -1300,35 +1353,35 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1300
1353
|
},
|
|
1301
1354
|
async triggerWebHook(user) {
|
|
1302
1355
|
let ENTRY_CREATE;
|
|
1303
|
-
const webhookStore = strapi2.serviceMap
|
|
1304
|
-
const eventHub = strapi2.serviceMap
|
|
1356
|
+
const webhookStore = strapi2.serviceMap?.get("webhookStore");
|
|
1357
|
+
const eventHub = strapi2.serviceMap?.get("eventHub");
|
|
1305
1358
|
if (webhookStore) {
|
|
1306
1359
|
ENTRY_CREATE = webhookStore.allowedEvents.get("ENTRY_CREATE");
|
|
1307
1360
|
}
|
|
1308
1361
|
const modelDef = strapi2.getModel("admin::user");
|
|
1309
1362
|
const sanitizedEntity = await strapiUtils__default.default.sanitize.sanitizers.defaultSanitizeOutput(
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
},
|
|
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
|
|
1314
1366
|
user
|
|
1315
1367
|
);
|
|
1316
|
-
eventHub
|
|
1368
|
+
eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
|
|
1317
1369
|
model: modelDef.modelName,
|
|
1318
1370
|
entry: sanitizedEntity
|
|
1319
1371
|
});
|
|
1320
1372
|
},
|
|
1321
1373
|
triggerSignInSuccess(user) {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
eventHub.
|
|
1325
|
-
|
|
1374
|
+
const userCopy = { ...user };
|
|
1375
|
+
delete userCopy.password;
|
|
1376
|
+
const eventHub = strapi2.serviceMap?.get("eventHub");
|
|
1377
|
+
eventHub?.emit("admin.auth.success", {
|
|
1378
|
+
user: userCopy,
|
|
1326
1379
|
provider: "strapi-plugin-oidc"
|
|
1327
1380
|
});
|
|
1328
1381
|
},
|
|
1329
1382
|
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
1330
1383
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1331
|
-
const isRememberMe = !!config2
|
|
1384
|
+
const isRememberMe = !!config2?.REMEMBER_ME;
|
|
1332
1385
|
const content = `
|
|
1333
1386
|
<noscript>
|
|
1334
1387
|
<div class="card">
|
|
@@ -1379,12 +1432,15 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1379
1432
|
const userId = String(user.id);
|
|
1380
1433
|
const deviceId = node_crypto.randomUUID();
|
|
1381
1434
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1382
|
-
const rememberMe = !!config2
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
+
);
|
|
1388
1444
|
const isProduction = strapi2.config.get("environment") === "production";
|
|
1389
1445
|
const domain = strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain");
|
|
1390
1446
|
const path = strapi2.config.get("admin.auth.cookie.path", "/admin");
|
|
@@ -1401,7 +1457,6 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1401
1457
|
const idleLifespanSec = strapi2.config.get(
|
|
1402
1458
|
"admin.auth.sessions.idleRefreshTokenLifespan",
|
|
1403
1459
|
1209600
|
|
1404
|
-
// 14 days — Strapi default
|
|
1405
1460
|
);
|
|
1406
1461
|
const idleMs = idleLifespanSec * 1e3;
|
|
1407
1462
|
const absoluteMs = new Date(absoluteExpiresAt).getTime() - Date.now();
|
|
@@ -1411,7 +1466,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1411
1466
|
}
|
|
1412
1467
|
ctx.cookies.set("strapi_admin_refresh", refreshToken, cookieOptions);
|
|
1413
1468
|
ctx.cookies.set("oidc_authenticated", "1", { ...cookieOptions, path: "/" });
|
|
1414
|
-
const accessResult = await
|
|
1469
|
+
const accessResult = await smAdmin.generateAccessToken(refreshToken);
|
|
1415
1470
|
if ("error" in accessResult) {
|
|
1416
1471
|
throw new Error(accessResult.error);
|
|
1417
1472
|
}
|
|
@@ -1502,8 +1557,23 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1502
1557
|
const result = await getWhitelistQuery().findOne({
|
|
1503
1558
|
where: { email }
|
|
1504
1559
|
});
|
|
1505
|
-
if (!result) throw new
|
|
1560
|
+
if (!result) throw new OidcError("whitelist_rejected", errorMessages.WHITELIST_NOT_PRESENT);
|
|
1506
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;
|
|
1507
1577
|
}
|
|
1508
1578
|
};
|
|
1509
1579
|
}
|
|
@@ -1538,7 +1608,10 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1538
1608
|
});
|
|
1539
1609
|
}
|
|
1540
1610
|
},
|
|
1541
|
-
async find({
|
|
1611
|
+
async find({
|
|
1612
|
+
page = 1,
|
|
1613
|
+
pageSize = 25
|
|
1614
|
+
} = {}) {
|
|
1542
1615
|
const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").findPage({
|
|
1543
1616
|
sort: { createdAt: "desc" },
|
|
1544
1617
|
page,
|