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.
@@ -1,5 +1,6 @@
1
1
  import { randomUUID, randomBytes, createHash } from "node:crypto";
2
2
  import pkceChallenge from "pkce-challenge";
3
+ import { Readable } from "node:stream";
3
4
  import strapiUtils from "@strapi/utils";
4
5
  import generator from "generate-password";
5
6
  function register$1() {
@@ -30,6 +31,12 @@ function getRetentionDays() {
30
31
  function isAuditLogEnabled() {
31
32
  return getRetentionDays() !== 0;
32
33
  }
34
+ const PLUGIN_NAME = "strapi-plugin-oidc";
35
+ const getOauthService = () => strapi.plugin(PLUGIN_NAME).service("oauth");
36
+ const getRoleService = () => strapi.plugin(PLUGIN_NAME).service("role");
37
+ const getWhitelistService = () => strapi.plugin(PLUGIN_NAME).service("whitelist");
38
+ const getAuditLogService = () => strapi.plugin(PLUGIN_NAME).service("auditLog");
39
+ const getAdminUserService = () => strapi.service("admin::user");
33
40
  const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
34
41
  async function bootstrap({ strapi: strapi2 }) {
35
42
  const adminUrl = strapi2.config.get("admin.url", "/admin");
@@ -41,7 +48,7 @@ async function bootstrap({ strapi: strapi2 }) {
41
48
  const isTokenRefresh = path === tokenRefreshPath;
42
49
  if (isAuthRoute && isPost || isTokenRefresh) {
43
50
  try {
44
- const whitelistService2 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
51
+ const whitelistService2 = getWhitelistService();
45
52
  const settings = await whitelistService2.getSettings();
46
53
  const enforceOIDC = resolveEnforceOIDC(strapi2, settings?.enforceOIDC);
47
54
  if (enforceOIDC && isAuthRoute && isPost) {
@@ -89,7 +96,7 @@ async function bootstrap({ strapi: strapi2 }) {
89
96
  const enforceOIDCConfig = getEnforceOIDCConfig(strapi2);
90
97
  if (enforceOIDCConfig !== null) {
91
98
  try {
92
- const whitelistService2 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
99
+ const whitelistService2 = getWhitelistService();
93
100
  const settings = await whitelistService2.getSettings();
94
101
  if (settings.enforceOIDC !== enforceOIDCConfig) {
95
102
  await whitelistService2.setSettings({ ...settings, enforceOIDC: enforceOIDCConfig });
@@ -119,7 +126,7 @@ async function bootstrap({ strapi: strapi2 }) {
119
126
  task: async () => {
120
127
  try {
121
128
  const retentionDays = getRetentionDays();
122
- await strapi2.plugin("strapi-plugin-oidc").service("auditLog").cleanup(retentionDays);
129
+ await getAuditLogService().cleanup(retentionDays);
123
130
  } catch (err) {
124
131
  strapi2.log.warn("[strapi-plugin-oidc] Audit log cleanup failed:", err.message);
125
132
  }
@@ -213,9 +220,14 @@ function getExpiredCookieOptions(strapi2, ctx) {
213
220
  function clearAuthCookies(strapi2, ctx) {
214
221
  const options2 = getExpiredCookieOptions(strapi2, ctx);
215
222
  ctx.cookies.set("strapi_admin_refresh", "", options2);
216
- ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
217
- ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
218
- ctx.cookies.set("oidc_user_email", "", { ...options2, path: "/" });
223
+ const rootPathOptions = { ...options2, path: "/" };
224
+ for (const name of ["oidc_authenticated", "oidc_access_token", "oidc_user_email"]) {
225
+ ctx.cookies.set(name, "", rootPathOptions);
226
+ }
227
+ }
228
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
229
+ function isValidEmail(email) {
230
+ return EMAIL_REGEX.test(email);
219
231
  }
220
232
  const errorCodes = {
221
233
  TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
@@ -287,8 +299,6 @@ const en = {
287
299
  "whitelist.toggle.enabled": "Enabled",
288
300
  "whitelist.toggle.disabled": "Disabled",
289
301
  "whitelist.email.placeholder": "Email address",
290
- "whitelist.roles.placeholder": "Select specific role(s)",
291
- "whitelist.table.roles": "Role(s)",
292
302
  "whitelist.table.empty": "No email addresses",
293
303
  "whitelist.delete.label": "Delete",
294
304
  "page.title.oidc": "OIDC",
@@ -312,7 +322,6 @@ const en = {
312
322
  "unsaved.description": "You have unsaved changes that will be lost if you leave. Do you want to continue?",
313
323
  "unsaved.confirm": "Leave",
314
324
  "unsaved.cancel": "Stay",
315
- "whitelist.table.roles.default": "(Default)",
316
325
  "auditlog.title": "Audit Logs",
317
326
  "auditlog.export": "Download",
318
327
  "auditlog.table.timestamp": "Timestamp",
@@ -355,6 +364,53 @@ const userFacingMessages = {
355
364
  return en["user.signInError"];
356
365
  }
357
366
  };
367
+ class OidcError extends Error {
368
+ kind;
369
+ cause;
370
+ constructor(kind, message, cause) {
371
+ super(message);
372
+ this.name = "OidcError";
373
+ this.kind = kind;
374
+ this.cause = cause;
375
+ }
376
+ }
377
+ const OIDC_ERROR_DISPATCH = {
378
+ nonce_mismatch: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH },
379
+ token_exchange_failed: {
380
+ action: "token_exchange_failed",
381
+ code: errorCodes.TOKEN_EXCHANGE_FAILED
382
+ },
383
+ id_token_parse_failed: {
384
+ action: "login_failure",
385
+ code: errorCodes.ID_TOKEN_PARSE_FAILED,
386
+ key: "id_token_parse_failed"
387
+ },
388
+ userinfo_fetch_failed: {
389
+ action: "login_failure",
390
+ code: errorCodes.USERINFO_FETCH_FAILED,
391
+ key: "userinfo_fetch_failed"
392
+ },
393
+ user_creation_failed: {
394
+ action: "login_failure",
395
+ code: errorCodes.USER_CREATION_FAILED,
396
+ key: "user_creation_failed"
397
+ },
398
+ whitelist_rejected: {
399
+ action: "whitelist_rejected",
400
+ code: errorCodes.WHITELIST_CHECK_FAILED,
401
+ key: "whitelist_rejected"
402
+ },
403
+ invalid_email: {
404
+ action: "login_failure",
405
+ code: errorCodes.TOKEN_EXCHANGE_FAILED,
406
+ key: "sign_in_unknown"
407
+ },
408
+ unknown: {
409
+ action: "login_failure",
410
+ code: errorCodes.TOKEN_EXCHANGE_FAILED,
411
+ key: "sign_in_unknown"
412
+ }
413
+ };
358
414
  const REQUIRED_CONFIG_KEYS = [
359
415
  "OIDC_CLIENT_ID",
360
416
  "OIDC_CLIENT_SECRET",
@@ -367,6 +423,7 @@ const REQUIRED_CONFIG_KEYS = [
367
423
  "OIDC_GIVEN_NAME_FIELD",
368
424
  "OIDC_AUTHORIZATION_ENDPOINT"
369
425
  ];
426
+ const LOGOUT_USERINFO_TIMEOUT_MS = 3e3;
370
427
  function configValidation() {
371
428
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
372
429
  const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
@@ -384,7 +441,6 @@ async function oidcSignIn(ctx) {
384
441
  const cookieOptions = {
385
442
  httpOnly: true,
386
443
  maxAge: 6e5,
387
- // 10 minutes
388
444
  secure: isProduction && ctx.request.secure,
389
445
  sameSite: "lax"
390
446
  };
@@ -414,7 +470,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
414
470
  }
415
471
  });
416
472
  if (!response.ok) {
417
- throw new Error(errorMessages.TOKEN_EXCHANGE_FAILED);
473
+ throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
418
474
  }
419
475
  const tokenData = await response.json();
420
476
  if (tokenData.id_token) {
@@ -422,23 +478,23 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
422
478
  const payloadB64 = tokenData.id_token.split(".")[1];
423
479
  const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
424
480
  if (idTokenPayload.nonce !== expectedNonce) {
425
- throw new Error(errorMessages.NONCE_MISMATCH);
481
+ throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
426
482
  }
427
483
  } catch (e) {
428
- if (e.message === "Nonce mismatch") throw e;
429
- throw new Error(errorMessages.ID_TOKEN_PARSE_FAILED);
484
+ if (e instanceof OidcError && e.kind === "nonce_mismatch") throw e;
485
+ throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
430
486
  }
431
487
  }
432
488
  const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
433
489
  headers: { Authorization: `Bearer ${tokenData.access_token}` }
434
490
  });
435
491
  if (!userResponse.ok) {
436
- throw new Error(errorMessages.USERINFO_FETCH_FAILED);
492
+ throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
437
493
  }
438
494
  const userInfo = await userResponse.json();
439
495
  return { userInfo, accessToken: tokenData.access_token };
440
496
  }
441
- function resolveRolesFromGroups(userInfo, config2, availableRoles) {
497
+ function collectGroupMapRoleNames(userInfo, config2) {
442
498
  const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
443
499
  if (!Array.isArray(rawGroups) || rawGroups.length === 0) return [];
444
500
  const groups = rawGroups.filter((g) => typeof g === "string");
@@ -449,22 +505,15 @@ function resolveRolesFromGroups(userInfo, config2, availableRoles) {
449
505
  } catch {
450
506
  return [];
451
507
  }
452
- const roleIdSet = /* @__PURE__ */ new Set();
508
+ const roleNameSet = /* @__PURE__ */ new Set();
453
509
  for (const group of groups) {
454
510
  const roleNames = groupRoleMap[group];
455
511
  if (!roleNames) continue;
456
512
  for (const name of roleNames) {
457
- const match = availableRoles.find((r) => r.name === name);
458
- if (match) roleIdSet.add(String(match.id));
513
+ roleNameSet.add(name);
459
514
  }
460
515
  }
461
- return [...roleIdSet];
462
- }
463
- async function resolveRoles(userInfo, config2, roleService2, availableRoles) {
464
- const groupRoles = resolveRolesFromGroups(userInfo, config2, availableRoles);
465
- if (groupRoles.length > 0) return { roles: groupRoles, fromGroupMapping: true };
466
- const oidcRoles = await roleService2.oidcRoles();
467
- return { roles: oidcRoles?.roles || [], fromGroupMapping: false };
516
+ return [...roleNameSet];
468
517
  }
469
518
  async function registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2) {
470
519
  const defaultLocale = oauthService2.localeFindByHeader(
@@ -482,10 +531,7 @@ async function registerNewUser(oauthService2, email, userResponseData, config2,
482
531
  }
483
532
  function rolesChanged(current, next) {
484
533
  if (current.size !== next.size) return true;
485
- for (const id of next) {
486
- if (!current.has(id)) return true;
487
- }
488
- return false;
534
+ return [...next].some((id) => !current.has(id));
489
535
  }
490
536
  async function updateUserRoles(user, currentRoleIds, newRoleIds) {
491
537
  try {
@@ -511,27 +557,51 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
511
557
  async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
512
558
  const rawEmail = String(userResponseData.email ?? "");
513
559
  const email = rawEmail.toLowerCase();
514
- if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
515
- throw new Error(errorMessages.INVALID_EMAIL);
560
+ if (!email || !isValidEmail(email)) {
561
+ throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
516
562
  }
517
563
  await whitelistService2.checkWhitelistForEmail(email);
518
- const allRoles = await strapi.db.query("admin::role").findMany();
519
- const { roles: roles2, fromGroupMapping } = await resolveRoles(
520
- userResponseData,
521
- config2,
522
- roleService2,
523
- allRoles
524
- );
525
- const resolvedRoleNames = allRoles.filter((r) => roles2.includes(String(r.id))).map((r) => r.name);
564
+ const candidateNames = collectGroupMapRoleNames(userResponseData, config2);
565
+ let roles2 = [];
566
+ let fromGroupMapping = false;
567
+ let resolvedRoleNames = [];
568
+ if (candidateNames.length > 0) {
569
+ const matchedRoles = await strapi.db.query("admin::role").findMany({
570
+ where: { name: { $in: candidateNames } },
571
+ select: ["id", "name"]
572
+ });
573
+ const nameToId = new Map(matchedRoles.map((r) => [r.name, String(r.id)]));
574
+ for (const name of candidateNames) {
575
+ const id = nameToId.get(name);
576
+ if (id) roles2.push(id);
577
+ }
578
+ resolvedRoleNames = matchedRoles.map((r) => r.name);
579
+ fromGroupMapping = true;
580
+ } else {
581
+ const oidcRolesResult = await roleService2.oidcRoles();
582
+ roles2 = oidcRolesResult?.roles || [];
583
+ if (roles2.length > 0) {
584
+ const oidcRoleRecords = await strapi.db.query("admin::role").findMany({
585
+ where: { id: { $in: roles2.map(Number) } },
586
+ select: ["id", "name"]
587
+ });
588
+ resolvedRoleNames = oidcRoleRecords.map((r) => r.name);
589
+ }
590
+ }
526
591
  let userCreated = false;
527
592
  let rolesUpdated = false;
528
593
  let user = await userService.findOneByEmail(email, ["roles"]);
529
594
  if (!user) {
530
- user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
595
+ try {
596
+ user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
597
+ } catch (e) {
598
+ const msg = e instanceof Error ? e.message : String(e);
599
+ throw new OidcError("user_creation_failed", msg, e);
600
+ }
531
601
  userCreated = true;
532
602
  rolesUpdated = true;
533
603
  } else if (fromGroupMapping && roles2.length > 0) {
534
- const currentRoleIds = new Set(user.roles.map((r) => String(r.id)));
604
+ const currentRoleIds = new Set((user.roles ?? []).map((r) => String(r.id)));
535
605
  if (rolesChanged(currentRoleIds, new Set(roles2))) {
536
606
  await updateUserRoles(user, currentRoleIds, roles2);
537
607
  rolesUpdated = true;
@@ -541,55 +611,30 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
541
611
  oauthService2.triggerSignInSuccess(user);
542
612
  return { activateUser: user, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
543
613
  }
544
- function classifyOidcError(msg, userInfo) {
545
- if (msg.includes("whitelist")) {
546
- return {
547
- action: "whitelist_rejected",
548
- code: errorCodes.WHITELIST_CHECK_FAILED,
549
- key: "whitelist_rejected"
550
- };
551
- }
552
- if (msg === "Nonce mismatch")
553
- return { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH };
554
- if (msg === "Token exchange failed")
555
- return { action: "token_exchange_failed", code: errorCodes.TOKEN_EXCHANGE_FAILED };
556
- if (msg === "Failed to fetch user info") {
557
- return {
558
- action: "login_failure",
559
- code: errorCodes.USERINFO_FETCH_FAILED,
560
- key: "userinfo_fetch_failed"
561
- };
562
- }
563
- if (msg === "Failed to parse ID token") {
564
- return {
565
- action: "login_failure",
566
- code: errorCodes.ID_TOKEN_PARSE_FAILED,
567
- key: "id_token_parse_failed",
568
- params: { error: msg }
569
- };
570
- }
571
- if (msg === "User creation failed" || msg.includes("createUser")) {
572
- return {
573
- action: "login_failure",
574
- code: errorCodes.USER_CREATION_FAILED,
575
- key: "user_creation_failed",
576
- params: userInfo?.email ? { email: userInfo.email, error: msg } : void 0
577
- };
614
+ function classifyOidcError(e, userInfo) {
615
+ const kind = e instanceof OidcError ? e.kind : "unknown";
616
+ const dispatch = OIDC_ERROR_DISPATCH[kind];
617
+ const msg = e instanceof Error ? e.message : String(e);
618
+ let params;
619
+ if (kind === "id_token_parse_failed" || kind === "unknown") {
620
+ params = { error: msg };
621
+ } else if (kind === "user_creation_failed" && userInfo?.email) {
622
+ params = { email: userInfo.email, error: msg };
578
623
  }
579
624
  return {
580
- action: "login_failure",
581
- code: errorCodes.TOKEN_EXCHANGE_FAILED,
582
- key: "sign_in_unknown",
583
- params: { error: msg || "unknown" }
625
+ action: dispatch.action,
626
+ code: dispatch.code,
627
+ key: dispatch.key,
628
+ params
584
629
  };
585
630
  }
586
631
  async function oidcSignInCallback(ctx) {
587
632
  const config2 = configValidation();
588
- const userService = strapi.service("admin::user");
589
- const oauthService2 = strapi.plugin("strapi-plugin-oidc").service("oauth");
590
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
591
- const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
592
- const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
633
+ const userService = getAdminUserService();
634
+ const oauthService2 = getOauthService();
635
+ const roleService2 = getRoleService();
636
+ const whitelistService2 = getWhitelistService();
637
+ const auditLog2 = getAuditLogService();
593
638
  if (!ctx.query.code) {
594
639
  await auditLog2.log({ action: "missing_code", ip: ctx.ip });
595
640
  return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
@@ -621,7 +666,6 @@ async function oidcSignInCallback(ctx) {
621
666
  ctx.cookies.set("oidc_access_token", accessToken, {
622
667
  httpOnly: true,
623
668
  maxAge: 3e5,
624
- // 5 minutes — matches typical provider access token lifetime
625
669
  secure: isProduction && ctx.request.secure,
626
670
  sameSite: "lax"
627
671
  });
@@ -662,19 +706,18 @@ async function oidcSignInCallback(ctx) {
662
706
  ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
663
707
  ctx.send(html);
664
708
  } catch (e) {
665
- const msg = e.message ?? "";
666
- const errorInfo = classifyOidcError(msg, userInfo);
709
+ const errorInfo = classifyOidcError(e, userInfo);
667
710
  await auditLog2.log({
668
711
  action: errorInfo.action,
669
712
  email: userInfo?.email,
670
713
  ip: ctx.ip,
671
714
  detailsKey: errorInfo.action,
672
- detailsParams: errorInfo.action === "login_failure" ? { message: msg } : void 0
715
+ detailsParams: errorInfo.action === "login_failure" ? { message: e instanceof Error ? e.message : String(e) } : void 0
673
716
  });
674
717
  strapi.log.error({
675
718
  code: errorInfo.code,
676
719
  phase: "oidc_callback",
677
- message: msg || "Unknown sign-in error",
720
+ message: e instanceof Error ? e.message : "Unknown sign-in error",
678
721
  detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
679
722
  email: userInfo?.email
680
723
  });
@@ -683,7 +726,7 @@ async function oidcSignInCallback(ctx) {
683
726
  }
684
727
  async function logout(ctx) {
685
728
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
686
- const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
729
+ const auditLog2 = getAuditLogService();
687
730
  const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
688
731
  const adminPanelUrl = strapi.config.get("admin.url", "/admin");
689
732
  const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
@@ -693,7 +736,8 @@ async function logout(ctx) {
693
736
  if (logoutUrl && isOidcSession && accessToken) {
694
737
  try {
695
738
  const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
696
- headers: { Authorization: `Bearer ${accessToken}` }
739
+ headers: { Authorization: `Bearer ${accessToken}` },
740
+ signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
697
741
  });
698
742
  if (response.ok) {
699
743
  if (userEmail)
@@ -724,7 +768,7 @@ const oidc = {
724
768
  logout
725
769
  };
726
770
  async function find$1(ctx) {
727
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
771
+ const roleService2 = getRoleService();
728
772
  const roles2 = await roleService2.find();
729
773
  const oidcConstants = roleService2.getOidcRoles();
730
774
  for (const oidc2 of oidcConstants) {
@@ -738,7 +782,7 @@ async function find$1(ctx) {
738
782
  async function update(ctx) {
739
783
  try {
740
784
  const { roles: roles2 } = ctx.request.body;
741
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
785
+ const roleService2 = getRoleService();
742
786
  await roleService2.update(roles2);
743
787
  ctx.send({}, 204);
744
788
  } catch (e) {
@@ -759,8 +803,17 @@ function formatDatetimeForFilename(date) {
759
803
  const seconds = String(date.getSeconds()).padStart(2, "0");
760
804
  return `${year}${month}${day}_${hours}${minutes}${seconds}`;
761
805
  }
762
- function getWhitelistService() {
763
- return strapi.plugin("strapi-plugin-oidc").service("whitelist");
806
+ function setJsonAttachmentHeaders(ctx, basename) {
807
+ const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
808
+ ctx.set("Content-Type", "application/json");
809
+ ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.json"`);
810
+ }
811
+ function setNdjsonAttachmentHeaders(ctx, basename) {
812
+ const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
813
+ ctx.set("Content-Type", "application/x-ndjson; charset=utf-8");
814
+ ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.ndjson"`);
815
+ ctx.set("Cache-Control", "no-store");
816
+ ctx.set("X-Content-Type-Options", "nosniff");
764
817
  }
765
818
  async function info(ctx) {
766
819
  const whitelistService2 = getWhitelistService();
@@ -775,8 +828,9 @@ async function info(ctx) {
775
828
  };
776
829
  }
777
830
  async function updateSettings(ctx) {
778
- const { useWhitelist } = ctx.request.body;
779
- let { enforceOIDC } = ctx.request.body;
831
+ const body = ctx.request.body;
832
+ const { useWhitelist } = body;
833
+ let { enforceOIDC } = body;
780
834
  const whitelistService2 = getWhitelistService();
781
835
  if (useWhitelist && enforceOIDC) {
782
836
  const users = await whitelistService2.getUsers();
@@ -805,11 +859,9 @@ async function register(ctx) {
805
859
  const rawEmails = Array.isArray(email) ? email : email.split(",");
806
860
  const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
807
861
  const whitelistService2 = getWhitelistService();
808
- let matchedExistingUsersCount = 0;
862
+ const matchedExistingUsersCount = await whitelistService2.countAdminUsersByEmails(emailList);
809
863
  for (const singleEmail of emailList) {
810
- const existingUser = await strapi.query("admin::user").findOne({ where: { email: singleEmail } });
811
- if (existingUser) matchedExistingUsersCount++;
812
- const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({ where: { email: singleEmail } });
864
+ const alreadyWhitelisted = await whitelistService2.hasUser(singleEmail);
813
865
  if (!alreadyWhitelisted) {
814
866
  await whitelistService2.registerUser(singleEmail);
815
867
  }
@@ -823,13 +875,12 @@ async function removeEmail(ctx) {
823
875
  ctx.body = {};
824
876
  }
825
877
  async function deleteAll(ctx) {
826
- await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
878
+ const whitelistService2 = getWhitelistService();
879
+ await whitelistService2.deleteAllUsers();
827
880
  ctx.body = {};
828
881
  }
829
882
  async function exportWhitelist(ctx) {
830
- const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
831
- ctx.set("Content-Type", "application/json");
832
- ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-whitelist-${datetime}.json"`);
883
+ setJsonAttachmentHeaders(ctx, "strapi-oidc-whitelist");
833
884
  const whitelistService2 = getWhitelistService();
834
885
  const users = await whitelistService2.getUsers();
835
886
  ctx.body = users.map((u) => ({ email: u.email }));
@@ -841,7 +892,7 @@ async function importUsers(ctx) {
841
892
  ctx.body = { error: "Expected { users: [{email}] }" };
842
893
  return;
843
894
  }
844
- const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter((email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email));
895
+ const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter(isValidEmail);
845
896
  const deduped = [...new Set(normalized)];
846
897
  const whitelistService2 = getWhitelistService();
847
898
  const existing = await whitelistService2.getUsers();
@@ -856,7 +907,7 @@ async function importUsers(ctx) {
856
907
  }
857
908
  async function syncUsers(ctx) {
858
909
  const { users: rawUsers } = ctx.request.body;
859
- const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter((e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e));
910
+ const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(isValidEmail);
860
911
  const whitelistService2 = getWhitelistService();
861
912
  const currentUsers = await whitelistService2.getUsers();
862
913
  const syncEmailSet = new Set(emails);
@@ -884,44 +935,49 @@ const whitelist = {
884
935
  importUsers,
885
936
  exportWhitelist
886
937
  };
887
- function getAuditLogService() {
888
- return strapi.plugin("strapi-plugin-oidc").service("auditLog");
889
- }
890
- async function find(ctx) {
891
- const page = Math.max(1, Number(ctx.query.page) || 1);
892
- const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
893
- ctx.body = await getAuditLogService().find({ page, pageSize });
894
- }
895
- async function exportLogs(ctx) {
896
- const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
897
- ctx.set("Content-Type", "application/json");
898
- ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-audit-log-${datetime}.json"`);
899
- const service = getAuditLogService();
900
- const PAGE_SIZE = 1e3;
901
- const allRows = [];
938
+ const EXPORT_PAGE_SIZE = 500;
939
+ async function* ndjsonRowStream(service) {
902
940
  let page = 1;
903
941
  while (true) {
904
- const { results } = await service.find({ page, pageSize: PAGE_SIZE });
942
+ const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE });
943
+ if (results.length === 0) return;
944
+ let chunk = "";
905
945
  for (const row of results) {
906
- allRows.push({
907
- id: row.id,
908
- createdAt: row.createdAt,
946
+ chunk += JSON.stringify({
947
+ datetime: row.createdAt,
909
948
  action: row.action,
910
949
  email: row.email ?? null,
911
950
  ip: row.ip ?? null,
912
951
  details: row.details
913
- });
952
+ }) + "\n";
914
953
  }
915
- if (results.length < PAGE_SIZE) break;
954
+ yield Buffer.from(chunk, "utf8");
955
+ if (results.length < EXPORT_PAGE_SIZE) return;
916
956
  page++;
917
957
  }
918
- ctx.body = allRows.map((row) => ({
919
- datetime: row.createdAt,
920
- action: row.action,
921
- email: row.email,
922
- ip: row.ip,
923
- details: row.details
924
- }));
958
+ }
959
+ function errorAwareNdjsonStream(service) {
960
+ const gen = ndjsonRowStream(service);
961
+ const readable = Readable.from(gen);
962
+ readable.on("error", (err) => {
963
+ strapi$1.log.error({ phase: "audit_log_export", err }, "NDJSON export stream failed");
964
+ });
965
+ return readable;
966
+ }
967
+ let strapi$1;
968
+ function find(ctx) {
969
+ strapi$1 = ctx.strapi;
970
+ const page = Math.max(1, Number(ctx.query.page) || 1);
971
+ const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
972
+ return getAuditLogService().find({ page, pageSize }).then((result) => {
973
+ ctx.body = result;
974
+ });
975
+ }
976
+ async function exportLogs(ctx) {
977
+ strapi$1 = ctx.strapi;
978
+ setNdjsonAttachmentHeaders(ctx, "strapi-oidc-audit-log");
979
+ const service = getAuditLogService();
980
+ ctx.body = errorAwareNdjsonStream(service);
925
981
  }
926
982
  async function clearAll(ctx) {
927
983
  await getAuditLogService().clearAll();
@@ -1156,7 +1212,7 @@ function renderHtmlTemplate(title, content) {
1156
1212
  --icon-color: #d02b20;
1157
1213
  --success-bg: #eafbe7;
1158
1214
  --success-color: #328048;
1159
- --shadow: 0 1px 4px rgba(33, 33, 52, 0.1);
1215
+ --shadow: 0 1px 4 rgba(33, 33, 52, 0.1);
1160
1216
  }
1161
1217
  @media (prefers-color-scheme: dark) {
1162
1218
  :root {
@@ -1171,7 +1227,7 @@ function renderHtmlTemplate(title, content) {
1171
1227
  --icon-color: #f23628;
1172
1228
  --success-bg: #1c3523;
1173
1229
  --success-color: #55ca76;
1174
- --shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
1230
+ --shadow: 0 1px 4 rgba(0, 0, 0, 0.5);
1175
1231
  }
1176
1232
  }
1177
1233
  body {
@@ -1256,14 +1312,11 @@ function oauthService({ strapi: strapi2 }) {
1256
1312
  return {
1257
1313
  async createUser(email, lastname, firstname, locale, roles2 = []) {
1258
1314
  const userService = strapi2.service("admin::user");
1259
- if (/[A-Z]/.test(email)) {
1260
- const dbUser = await userService.findOneByEmail(email.toLocaleLowerCase());
1261
- if (dbUser) return dbUser;
1262
- }
1315
+ const normalizedEmail = email.toLowerCase();
1263
1316
  const createdUser = await userService.create({
1264
1317
  firstname: firstname || "unset",
1265
1318
  lastname: lastname || "",
1266
- email: email.toLocaleLowerCase(),
1319
+ email: normalizedEmail,
1267
1320
  roles: roles2,
1268
1321
  preferedLanguage: locale
1269
1322
  });
@@ -1294,35 +1347,35 @@ function oauthService({ strapi: strapi2 }) {
1294
1347
  },
1295
1348
  async triggerWebHook(user) {
1296
1349
  let ENTRY_CREATE;
1297
- const webhookStore = strapi2.serviceMap.get("webhookStore");
1298
- const eventHub = strapi2.serviceMap.get("eventHub");
1350
+ const webhookStore = strapi2.serviceMap?.get("webhookStore");
1351
+ const eventHub = strapi2.serviceMap?.get("eventHub");
1299
1352
  if (webhookStore) {
1300
1353
  ENTRY_CREATE = webhookStore.allowedEvents.get("ENTRY_CREATE");
1301
1354
  }
1302
1355
  const modelDef = strapi2.getModel("admin::user");
1303
1356
  const sanitizedEntity = await strapiUtils.sanitize.sanitizers.defaultSanitizeOutput(
1304
- {
1305
- schema: modelDef,
1306
- getModel: (uid2) => strapi2.getModel(uid2)
1307
- },
1357
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1358
+ { schema: modelDef, getModel: (uid2) => strapi2.getModel(uid2) },
1359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1308
1360
  user
1309
1361
  );
1310
- eventHub.emit(ENTRY_CREATE, {
1362
+ eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
1311
1363
  model: modelDef.modelName,
1312
1364
  entry: sanitizedEntity
1313
1365
  });
1314
1366
  },
1315
1367
  triggerSignInSuccess(user) {
1316
- delete user.password;
1317
- const eventHub = strapi2.serviceMap.get("eventHub");
1318
- eventHub.emit("admin.auth.success", {
1319
- user,
1368
+ const userCopy = { ...user };
1369
+ delete userCopy.password;
1370
+ const eventHub = strapi2.serviceMap?.get("eventHub");
1371
+ eventHub?.emit("admin.auth.success", {
1372
+ user: userCopy,
1320
1373
  provider: "strapi-plugin-oidc"
1321
1374
  });
1322
1375
  },
1323
1376
  renderSignUpSuccess(jwtToken, user, nonce) {
1324
1377
  const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
1325
- const isRememberMe = !!config2["REMEMBER_ME"];
1378
+ const isRememberMe = !!config2?.REMEMBER_ME;
1326
1379
  const content = `
1327
1380
  <noscript>
1328
1381
  <div class="card">
@@ -1373,12 +1426,15 @@ function oauthService({ strapi: strapi2 }) {
1373
1426
  const userId = String(user.id);
1374
1427
  const deviceId = randomUUID();
1375
1428
  const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
1376
- const rememberMe = !!config2["REMEMBER_ME"];
1377
- const { token: refreshToken, absoluteExpiresAt } = await sessionManager(
1378
- "admin"
1379
- ).generateRefreshToken(userId, deviceId, {
1380
- type: rememberMe ? "refresh" : "session"
1381
- });
1429
+ const rememberMe = !!config2?.REMEMBER_ME;
1430
+ const smAdmin = sessionManager("admin");
1431
+ const { token: refreshToken, absoluteExpiresAt } = await smAdmin.generateRefreshToken(
1432
+ userId,
1433
+ deviceId,
1434
+ {
1435
+ type: rememberMe ? "refresh" : "session"
1436
+ }
1437
+ );
1382
1438
  const isProduction = strapi2.config.get("environment") === "production";
1383
1439
  const domain = strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain");
1384
1440
  const path = strapi2.config.get("admin.auth.cookie.path", "/admin");
@@ -1395,7 +1451,6 @@ function oauthService({ strapi: strapi2 }) {
1395
1451
  const idleLifespanSec = strapi2.config.get(
1396
1452
  "admin.auth.sessions.idleRefreshTokenLifespan",
1397
1453
  1209600
1398
- // 14 days — Strapi default
1399
1454
  );
1400
1455
  const idleMs = idleLifespanSec * 1e3;
1401
1456
  const absoluteMs = new Date(absoluteExpiresAt).getTime() - Date.now();
@@ -1405,7 +1460,7 @@ function oauthService({ strapi: strapi2 }) {
1405
1460
  }
1406
1461
  ctx.cookies.set("strapi_admin_refresh", refreshToken, cookieOptions);
1407
1462
  ctx.cookies.set("oidc_authenticated", "1", { ...cookieOptions, path: "/" });
1408
- const accessResult = await sessionManager("admin").generateAccessToken(refreshToken);
1463
+ const accessResult = await smAdmin.generateAccessToken(refreshToken);
1409
1464
  if ("error" in accessResult) {
1410
1465
  throw new Error(accessResult.error);
1411
1466
  }
@@ -1496,8 +1551,23 @@ function whitelistService({ strapi: strapi2 }) {
1496
1551
  const result = await getWhitelistQuery().findOne({
1497
1552
  where: { email }
1498
1553
  });
1499
- if (!result) throw new Error(errorMessages.WHITELIST_NOT_PRESENT);
1554
+ if (!result) throw new OidcError("whitelist_rejected", errorMessages.WHITELIST_NOT_PRESENT);
1500
1555
  return result;
1556
+ },
1557
+ async hasUser(email) {
1558
+ const row = await getWhitelistQuery().findOne({ where: { email }, select: ["id"] });
1559
+ return !!row;
1560
+ },
1561
+ async deleteAllUsers() {
1562
+ await getWhitelistQuery().deleteMany({});
1563
+ },
1564
+ async countAdminUsersByEmails(emails) {
1565
+ if (emails.length === 0) return 0;
1566
+ const rows = await strapi2.query("admin::user").findMany({
1567
+ where: { email: { $in: emails } },
1568
+ select: ["id"]
1569
+ });
1570
+ return rows.length;
1501
1571
  }
1502
1572
  };
1503
1573
  }
@@ -1532,7 +1602,10 @@ function auditLogService({ strapi: strapi2 }) {
1532
1602
  });
1533
1603
  }
1534
1604
  },
1535
- async find({ page = 1, pageSize = 25 } = {}) {
1605
+ async find({
1606
+ page = 1,
1607
+ pageSize = 25
1608
+ } = {}) {
1536
1609
  const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").findPage({
1537
1610
  sort: { createdAt: "desc" },
1538
1611
  page,