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.
@@ -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 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
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 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
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 strapi2.plugin("strapi-plugin-oidc").service("auditLog").cleanup(retentionDays);
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
- ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
223
- ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
224
- ctx.cookies.set("oidc_user_email", "", { ...options2, path: "/" });
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 Error(errorMessages.TOKEN_EXCHANGE_FAILED);
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 Error(errorMessages.NONCE_MISMATCH);
487
+ throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
432
488
  }
433
489
  } catch (e) {
434
- if (e.message === "Nonce mismatch") throw e;
435
- throw new Error(errorMessages.ID_TOKEN_PARSE_FAILED);
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 Error(errorMessages.USERINFO_FETCH_FAILED);
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 resolveRolesFromGroups(userInfo, config2, availableRoles) {
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 roleIdSet = /* @__PURE__ */ new Set();
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
- const match = availableRoles.find((r) => r.name === name);
464
- if (match) roleIdSet.add(String(match.id));
519
+ roleNameSet.add(name);
465
520
  }
466
521
  }
467
- return [...roleIdSet];
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
- for (const id of next) {
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 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
521
- throw new Error(errorMessages.INVALID_EMAIL);
566
+ if (!email || !isValidEmail(email)) {
567
+ throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
522
568
  }
523
569
  await whitelistService2.checkWhitelistForEmail(email);
524
- const allRoles = await strapi.db.query("admin::role").findMany();
525
- const { roles: roles2, fromGroupMapping } = await resolveRoles(
526
- userResponseData,
527
- config2,
528
- roleService2,
529
- allRoles
530
- );
531
- const resolvedRoleNames = allRoles.filter((r) => roles2.includes(String(r.id))).map((r) => r.name);
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
- user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
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(msg, userInfo) {
551
- if (msg.includes("whitelist")) {
552
- return {
553
- action: "whitelist_rejected",
554
- code: errorCodes.WHITELIST_CHECK_FAILED,
555
- key: "whitelist_rejected"
556
- };
557
- }
558
- if (msg === "Nonce mismatch")
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: "login_failure",
587
- code: errorCodes.TOKEN_EXCHANGE_FAILED,
588
- key: "sign_in_unknown",
589
- params: { error: msg || "unknown" }
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 = strapi.service("admin::user");
595
- const oauthService2 = strapi.plugin("strapi-plugin-oidc").service("oauth");
596
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
597
- const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
598
- const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
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 msg = e.message ?? "";
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: msg } : void 0
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: msg || "Unknown sign-in error",
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 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
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 = strapi.plugin("strapi-plugin-oidc").service("role");
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 = strapi.plugin("strapi-plugin-oidc").service("role");
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 getWhitelistService() {
769
- return strapi.plugin("strapi-plugin-oidc").service("whitelist");
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 { useWhitelist } = ctx.request.body;
785
- let { enforceOIDC } = ctx.request.body;
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
- let matchedExistingUsersCount = 0;
868
+ const matchedExistingUsersCount = await whitelistService2.countAdminUsersByEmails(emailList);
815
869
  for (const singleEmail of emailList) {
816
- const existingUser = await strapi.query("admin::user").findOne({ where: { email: singleEmail } });
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
- await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
884
+ const whitelistService2 = getWhitelistService();
885
+ await whitelistService2.deleteAllUsers();
833
886
  ctx.body = {};
834
887
  }
835
888
  async function exportWhitelist(ctx) {
836
- const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
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((email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email));
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((e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e));
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
- function getAuditLogService() {
894
- return strapi.plugin("strapi-plugin-oidc").service("auditLog");
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: PAGE_SIZE });
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
- allRows.push({
913
- id: row.id,
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
- if (results.length < PAGE_SIZE) break;
960
+ yield Buffer.from(chunk, "utf8");
961
+ if (results.length < EXPORT_PAGE_SIZE) return;
922
962
  page++;
923
963
  }
924
- ctx.body = allRows.map((row) => ({
925
- datetime: row.createdAt,
926
- action: row.action,
927
- email: row.email,
928
- ip: row.ip,
929
- details: row.details
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 4px rgba(33, 33, 52, 0.1);
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 4px rgba(0, 0, 0, 0.5);
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
- if (/[A-Z]/.test(email)) {
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: email.toLocaleLowerCase(),
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.get("webhookStore");
1304
- const eventHub = strapi2.serviceMap.get("eventHub");
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
- schema: modelDef,
1312
- getModel: (uid2) => strapi2.getModel(uid2)
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.emit(ENTRY_CREATE, {
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
- delete user.password;
1323
- const eventHub = strapi2.serviceMap.get("eventHub");
1324
- eventHub.emit("admin.auth.success", {
1325
- user,
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["REMEMBER_ME"];
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["REMEMBER_ME"];
1383
- const { token: refreshToken, absoluteExpiresAt } = await sessionManager(
1384
- "admin"
1385
- ).generateRefreshToken(userId, deviceId, {
1386
- type: rememberMe ? "refresh" : "session"
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 sessionManager("admin").generateAccessToken(refreshToken);
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 Error(errorMessages.WHITELIST_NOT_PRESENT);
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({ page = 1, pageSize = 25 } = {}) {
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,