strapi-plugin-oidc 1.6.5 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -15
- package/dist/admin/index-C2KZ4QxC.js +4381 -0
- package/dist/admin/{index-C2BnnDzh.js → index-DB7zjuHj.js} +23 -6
- package/dist/admin/{index-DgUClS5s.mjs → index-D_ZKgByO.mjs} +23 -6
- package/dist/admin/index-UvfJxIgI.mjs +4379 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +686 -272
- package/dist/server/index.mjs +686 -272
- package/package.json +3 -2
- package/dist/admin/index-HQ2uuypE.mjs +0 -841
- package/dist/admin/index-pWwCtdNu.js +0 -843
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",
|
|
@@ -272,6 +284,7 @@ const en = {
|
|
|
272
284
|
"page.save.error": "Update failed.",
|
|
273
285
|
"page.add": "Add",
|
|
274
286
|
"page.cancel": "Cancel",
|
|
287
|
+
"common.remove": "Remove {label}",
|
|
275
288
|
"page.ok": "OK",
|
|
276
289
|
"roles.title": "Default Role(s)",
|
|
277
290
|
"roles.placeholder": "Select default role(s)",
|
|
@@ -303,7 +316,7 @@ const en = {
|
|
|
303
316
|
"enforce.config.info": "Enforcement is controlled by the OIDC_ENFORCE config variable and cannot be changed here.",
|
|
304
317
|
"login.settings.title": "Login Settings",
|
|
305
318
|
"login.sso": "Login via SSO",
|
|
306
|
-
"
|
|
319
|
+
"pagination.total": "{count, plural, one {# entry} other {# entries}}",
|
|
307
320
|
"whitelist.import": "Import",
|
|
308
321
|
"whitelist.export": "Export",
|
|
309
322
|
"whitelist.delete.all.label": "Delete All",
|
|
@@ -331,6 +344,20 @@ const en = {
|
|
|
331
344
|
"auditlog.clear.success": "Audit logs cleared",
|
|
332
345
|
"auditlog.clear.error": "Failed to clear audit logs",
|
|
333
346
|
"auditlog.export.error": "Failed to export audit logs",
|
|
347
|
+
"auditlog.filters": "Filters",
|
|
348
|
+
"auditlog.filters.action": "Action",
|
|
349
|
+
"auditlog.filters.email": "Email",
|
|
350
|
+
"auditlog.filters.ip": "IP address",
|
|
351
|
+
"auditlog.filters.createdAt": "Date",
|
|
352
|
+
"auditlog.filters.clear": "Clear filters",
|
|
353
|
+
"auditlog.filters.empty": "No entries match the current filters",
|
|
354
|
+
"auditlog.calendar.prevMonth": "Previous month",
|
|
355
|
+
"auditlog.calendar.nextMonth": "Next month",
|
|
356
|
+
"auditlog.calendar.state.today": "today",
|
|
357
|
+
"auditlog.calendar.state.selected": "selected",
|
|
358
|
+
"auditlog.calendar.state.alreadyAdded": "already added",
|
|
359
|
+
"auditlog.calendar.state.future": "unavailable, future date",
|
|
360
|
+
"auditlog.calendar.dayWithState": "{date}, {state}",
|
|
334
361
|
"auditlog.action.login_success": "User successfully authenticated via OIDC and was granted access.",
|
|
335
362
|
"auditlog.action.user_created": "A new Strapi admin account was created for this user on their first OIDC login.",
|
|
336
363
|
"auditlog.action.logout": "User logged out and their OIDC session was ended.",
|
|
@@ -341,21 +368,119 @@ const en = {
|
|
|
341
368
|
"auditlog.action.nonce_mismatch": "The nonce in the ID token did not match the one generated at login. This may indicate a token replay attack.",
|
|
342
369
|
"auditlog.action.token_exchange_failed": "The authorisation code could not be exchanged for tokens. The OIDC provider rejected the request.",
|
|
343
370
|
"auditlog.action.whitelist_rejected": "The user's email address is not on the whitelist. Access was denied.",
|
|
371
|
+
"auth.page.authenticating.title": "Authenticating...",
|
|
372
|
+
"auth.page.authenticating.noscript.heading": "JavaScript Required",
|
|
373
|
+
"auth.page.authenticating.noscript.body": "JavaScript must be enabled for authentication to complete.",
|
|
374
|
+
"auth.page.error.title": "Authentication Failed",
|
|
375
|
+
"auth.page.error.returnToLogin": "Return to Login",
|
|
344
376
|
"user.missing_code": "Authorisation code was not received from the OIDC provider.",
|
|
345
377
|
"user.invalid_state": "State parameter mismatch. Please restart the login flow.",
|
|
346
378
|
"user.signInError": "Authentication failed. Please try again.",
|
|
347
379
|
"settings.section": "OIDC",
|
|
348
380
|
"settings.configuration": "Configuration"
|
|
349
381
|
};
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
382
|
+
const __vite_glob_0_0 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
383
|
+
__proto__: null,
|
|
384
|
+
default: en
|
|
385
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
386
|
+
const modules = /* @__PURE__ */ Object.assign({ "../translations/locales/en.json": __vite_glob_0_0 });
|
|
387
|
+
const locales = Object.fromEntries(
|
|
388
|
+
Object.entries(modules).map(([path, mod]) => {
|
|
389
|
+
const code = path.match(/\/([^/]+)\.json$/)?.[1];
|
|
390
|
+
return [code ?? "", mod.default];
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
Object.keys(locales).filter(Boolean);
|
|
394
|
+
const DEFAULT_LOCALE = "en";
|
|
395
|
+
function parseAcceptLanguage(header) {
|
|
396
|
+
return header.split(",").map((part) => {
|
|
397
|
+
const [tag, ...params] = part.trim().split(";");
|
|
398
|
+
const qParam = params.find((p) => p.trim().startsWith("q="));
|
|
399
|
+
const q = qParam ? parseFloat(qParam.trim().slice(2)) : 1;
|
|
400
|
+
return { tag: tag.toLowerCase(), q: Number.isFinite(q) ? q : 1 };
|
|
401
|
+
}).filter((entry) => entry.tag).sort((a, b) => b.q - a.q);
|
|
402
|
+
}
|
|
403
|
+
function negotiateLocale(acceptLanguage) {
|
|
404
|
+
if (!acceptLanguage) return DEFAULT_LOCALE;
|
|
405
|
+
for (const { tag } of parseAcceptLanguage(acceptLanguage)) {
|
|
406
|
+
if (locales[tag]) return tag;
|
|
407
|
+
const base = tag.split("-")[0];
|
|
408
|
+
if (locales[base]) return base;
|
|
409
|
+
}
|
|
410
|
+
return DEFAULT_LOCALE;
|
|
411
|
+
}
|
|
412
|
+
function t(locale, key, fallback) {
|
|
413
|
+
return locales[locale]?.[key] ?? locales[DEFAULT_LOCALE]?.[key] ?? fallback ?? key;
|
|
414
|
+
}
|
|
415
|
+
const userFacingMessages = (locale) => ({
|
|
416
|
+
missing_code: t(
|
|
417
|
+
locale,
|
|
418
|
+
"user.missing_code",
|
|
419
|
+
"Authorisation code was not received from the OIDC provider."
|
|
420
|
+
),
|
|
421
|
+
invalid_state: t(
|
|
422
|
+
locale,
|
|
423
|
+
"user.invalid_state",
|
|
424
|
+
"State parameter mismatch. Please restart the login flow."
|
|
425
|
+
),
|
|
426
|
+
signInError: t(locale, "user.signInError", "Authentication failed. Please try again.")
|
|
427
|
+
});
|
|
428
|
+
const authPageMessages = (locale) => ({
|
|
429
|
+
authenticatingTitle: t(locale, "auth.page.authenticating.title", "Authenticating..."),
|
|
430
|
+
noscriptHeading: t(locale, "auth.page.authenticating.noscript.heading", "JavaScript Required"),
|
|
431
|
+
noscriptBody: t(
|
|
432
|
+
locale,
|
|
433
|
+
"auth.page.authenticating.noscript.body",
|
|
434
|
+
"JavaScript must be enabled for authentication to complete."
|
|
435
|
+
),
|
|
436
|
+
errorTitle: t(locale, "auth.page.error.title", "Authentication Failed"),
|
|
437
|
+
returnToLogin: t(locale, "auth.page.error.returnToLogin", "Return to Login")
|
|
438
|
+
});
|
|
439
|
+
class OidcError extends Error {
|
|
440
|
+
kind;
|
|
441
|
+
cause;
|
|
442
|
+
constructor(kind, message, cause) {
|
|
443
|
+
super(message);
|
|
444
|
+
this.name = "OidcError";
|
|
445
|
+
this.kind = kind;
|
|
446
|
+
this.cause = cause;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const OIDC_ERROR_DISPATCH = {
|
|
450
|
+
nonce_mismatch: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH },
|
|
451
|
+
token_exchange_failed: {
|
|
452
|
+
action: "token_exchange_failed",
|
|
453
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED
|
|
454
|
+
},
|
|
455
|
+
id_token_parse_failed: {
|
|
456
|
+
action: "login_failure",
|
|
457
|
+
code: errorCodes.ID_TOKEN_PARSE_FAILED,
|
|
458
|
+
key: "id_token_parse_failed"
|
|
353
459
|
},
|
|
354
|
-
|
|
355
|
-
|
|
460
|
+
userinfo_fetch_failed: {
|
|
461
|
+
action: "login_failure",
|
|
462
|
+
code: errorCodes.USERINFO_FETCH_FAILED,
|
|
463
|
+
key: "userinfo_fetch_failed"
|
|
356
464
|
},
|
|
357
|
-
|
|
358
|
-
|
|
465
|
+
user_creation_failed: {
|
|
466
|
+
action: "login_failure",
|
|
467
|
+
code: errorCodes.USER_CREATION_FAILED,
|
|
468
|
+
key: "user_creation_failed"
|
|
469
|
+
},
|
|
470
|
+
whitelist_rejected: {
|
|
471
|
+
action: "whitelist_rejected",
|
|
472
|
+
code: errorCodes.WHITELIST_CHECK_FAILED,
|
|
473
|
+
key: "whitelist_rejected"
|
|
474
|
+
},
|
|
475
|
+
invalid_email: {
|
|
476
|
+
action: "login_failure",
|
|
477
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
478
|
+
key: "sign_in_unknown"
|
|
479
|
+
},
|
|
480
|
+
unknown: {
|
|
481
|
+
action: "login_failure",
|
|
482
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
483
|
+
key: "sign_in_unknown"
|
|
359
484
|
}
|
|
360
485
|
};
|
|
361
486
|
const REQUIRED_CONFIG_KEYS = [
|
|
@@ -370,6 +495,7 @@ const REQUIRED_CONFIG_KEYS = [
|
|
|
370
495
|
"OIDC_GIVEN_NAME_FIELD",
|
|
371
496
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
372
497
|
];
|
|
498
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 3e3;
|
|
373
499
|
function configValidation() {
|
|
374
500
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
375
501
|
const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
|
|
@@ -387,7 +513,6 @@ async function oidcSignIn(ctx) {
|
|
|
387
513
|
const cookieOptions = {
|
|
388
514
|
httpOnly: true,
|
|
389
515
|
maxAge: 6e5,
|
|
390
|
-
// 10 minutes
|
|
391
516
|
secure: isProduction && ctx.request.secure,
|
|
392
517
|
sameSite: "lax"
|
|
393
518
|
};
|
|
@@ -417,7 +542,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
417
542
|
}
|
|
418
543
|
});
|
|
419
544
|
if (!response.ok) {
|
|
420
|
-
throw new
|
|
545
|
+
throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
|
|
421
546
|
}
|
|
422
547
|
const tokenData = await response.json();
|
|
423
548
|
if (tokenData.id_token) {
|
|
@@ -425,23 +550,23 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
425
550
|
const payloadB64 = tokenData.id_token.split(".")[1];
|
|
426
551
|
const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
427
552
|
if (idTokenPayload.nonce !== expectedNonce) {
|
|
428
|
-
throw new
|
|
553
|
+
throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
|
|
429
554
|
}
|
|
430
555
|
} catch (e) {
|
|
431
|
-
if (e.
|
|
432
|
-
throw new
|
|
556
|
+
if (e instanceof OidcError && e.kind === "nonce_mismatch") throw e;
|
|
557
|
+
throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
|
|
433
558
|
}
|
|
434
559
|
}
|
|
435
560
|
const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
436
561
|
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
437
562
|
});
|
|
438
563
|
if (!userResponse.ok) {
|
|
439
|
-
throw new
|
|
564
|
+
throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
|
|
440
565
|
}
|
|
441
566
|
const userInfo = await userResponse.json();
|
|
442
567
|
return { userInfo, accessToken: tokenData.access_token };
|
|
443
568
|
}
|
|
444
|
-
function
|
|
569
|
+
function collectGroupMapRoleNames(userInfo, config2) {
|
|
445
570
|
const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
|
|
446
571
|
if (!Array.isArray(rawGroups) || rawGroups.length === 0) return [];
|
|
447
572
|
const groups = rawGroups.filter((g) => typeof g === "string");
|
|
@@ -452,22 +577,15 @@ function resolveRolesFromGroups(userInfo, config2, availableRoles) {
|
|
|
452
577
|
} catch {
|
|
453
578
|
return [];
|
|
454
579
|
}
|
|
455
|
-
const
|
|
580
|
+
const roleNameSet = /* @__PURE__ */ new Set();
|
|
456
581
|
for (const group of groups) {
|
|
457
582
|
const roleNames = groupRoleMap[group];
|
|
458
583
|
if (!roleNames) continue;
|
|
459
584
|
for (const name of roleNames) {
|
|
460
|
-
|
|
461
|
-
if (match) roleIdSet.add(String(match.id));
|
|
585
|
+
roleNameSet.add(name);
|
|
462
586
|
}
|
|
463
587
|
}
|
|
464
|
-
return [...
|
|
465
|
-
}
|
|
466
|
-
async function resolveRoles(userInfo, config2, roleService2, availableRoles) {
|
|
467
|
-
const groupRoles = resolveRolesFromGroups(userInfo, config2, availableRoles);
|
|
468
|
-
if (groupRoles.length > 0) return { roles: groupRoles, fromGroupMapping: true };
|
|
469
|
-
const oidcRoles = await roleService2.oidcRoles();
|
|
470
|
-
return { roles: oidcRoles?.roles || [], fromGroupMapping: false };
|
|
588
|
+
return [...roleNameSet];
|
|
471
589
|
}
|
|
472
590
|
async function registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2) {
|
|
473
591
|
const defaultLocale = oauthService2.localeFindByHeader(
|
|
@@ -485,10 +603,7 @@ async function registerNewUser(oauthService2, email, userResponseData, config2,
|
|
|
485
603
|
}
|
|
486
604
|
function rolesChanged(current, next) {
|
|
487
605
|
if (current.size !== next.size) return true;
|
|
488
|
-
|
|
489
|
-
if (!current.has(id)) return true;
|
|
490
|
-
}
|
|
491
|
-
return false;
|
|
606
|
+
return [...next].some((id) => !current.has(id));
|
|
492
607
|
}
|
|
493
608
|
async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
494
609
|
try {
|
|
@@ -511,101 +626,187 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
|
511
626
|
throw updateErr;
|
|
512
627
|
}
|
|
513
628
|
}
|
|
629
|
+
async function resolveRolesFromGroups(candidateNames) {
|
|
630
|
+
const matchedRoles = await strapi.db.query("admin::role").findMany({
|
|
631
|
+
where: { name: { $in: candidateNames } },
|
|
632
|
+
select: ["id", "name"]
|
|
633
|
+
});
|
|
634
|
+
const nameToId = new Map(matchedRoles.map((r) => [r.name, String(r.id)]));
|
|
635
|
+
const roles2 = [];
|
|
636
|
+
for (const name of candidateNames) {
|
|
637
|
+
const id = nameToId.get(name);
|
|
638
|
+
if (id) roles2.push(id);
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
roles: roles2,
|
|
642
|
+
fromGroupMapping: true,
|
|
643
|
+
resolvedRoleNames: matchedRoles.map((r) => r.name)
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function resolveRolesFromDefaults(roleService2) {
|
|
647
|
+
const oidcRolesResult = await roleService2.oidcRoles();
|
|
648
|
+
const roles2 = oidcRolesResult?.roles || [];
|
|
649
|
+
if (roles2.length === 0) {
|
|
650
|
+
return { roles: roles2, fromGroupMapping: false, resolvedRoleNames: [] };
|
|
651
|
+
}
|
|
652
|
+
const records = await strapi.db.query("admin::role").findMany({
|
|
653
|
+
where: { id: { $in: roles2.map(Number) } },
|
|
654
|
+
select: ["id", "name"]
|
|
655
|
+
});
|
|
656
|
+
return {
|
|
657
|
+
roles: roles2,
|
|
658
|
+
fromGroupMapping: false,
|
|
659
|
+
resolvedRoleNames: records.map((r) => r.name)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async function resolveRoles(userResponseData, config2, roleService2) {
|
|
663
|
+
const candidateNames = collectGroupMapRoleNames(userResponseData, config2);
|
|
664
|
+
if (candidateNames.length > 0) {
|
|
665
|
+
return resolveRolesFromGroups(candidateNames);
|
|
666
|
+
}
|
|
667
|
+
return resolveRolesFromDefaults(roleService2);
|
|
668
|
+
}
|
|
669
|
+
async function ensureUser(userService, oauthService2, email, userResponseData, config2, ctx, resolved) {
|
|
670
|
+
const existing = await userService.findOneByEmail(email, ["roles"]);
|
|
671
|
+
if (!existing) {
|
|
672
|
+
try {
|
|
673
|
+
const user = await registerNewUser(
|
|
674
|
+
oauthService2,
|
|
675
|
+
email,
|
|
676
|
+
userResponseData,
|
|
677
|
+
config2,
|
|
678
|
+
ctx,
|
|
679
|
+
resolved.roles
|
|
680
|
+
);
|
|
681
|
+
return { user, userCreated: true, rolesUpdated: true };
|
|
682
|
+
} catch (e) {
|
|
683
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
684
|
+
throw new OidcError("user_creation_failed", msg, e);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (!resolved.fromGroupMapping || resolved.roles.length === 0) {
|
|
688
|
+
return { user: existing, userCreated: false, rolesUpdated: false };
|
|
689
|
+
}
|
|
690
|
+
const currentRoleIds = new Set((existing.roles ?? []).map((r) => String(r.id)));
|
|
691
|
+
if (!rolesChanged(currentRoleIds, new Set(resolved.roles))) {
|
|
692
|
+
return { user: existing, userCreated: false, rolesUpdated: false };
|
|
693
|
+
}
|
|
694
|
+
await updateUserRoles(existing, currentRoleIds, resolved.roles);
|
|
695
|
+
return { user: existing, userCreated: false, rolesUpdated: true };
|
|
696
|
+
}
|
|
514
697
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
throw new Error(errorMessages.INVALID_EMAIL);
|
|
698
|
+
const email = String(userResponseData.email ?? "").toLowerCase();
|
|
699
|
+
if (!email || !isValidEmail(email)) {
|
|
700
|
+
throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
|
|
519
701
|
}
|
|
520
702
|
await whitelistService2.checkWhitelistForEmail(email);
|
|
521
|
-
const
|
|
522
|
-
const {
|
|
703
|
+
const resolved = await resolveRoles(userResponseData, config2, roleService2);
|
|
704
|
+
const { user, userCreated, rolesUpdated } = await ensureUser(
|
|
705
|
+
userService,
|
|
706
|
+
oauthService2,
|
|
707
|
+
email,
|
|
523
708
|
userResponseData,
|
|
524
709
|
config2,
|
|
525
|
-
|
|
526
|
-
|
|
710
|
+
ctx,
|
|
711
|
+
resolved
|
|
527
712
|
);
|
|
528
|
-
const resolvedRoleNames = allRoles.filter((r) => roles2.includes(String(r.id))).map((r) => r.name);
|
|
529
|
-
let userCreated = false;
|
|
530
|
-
let rolesUpdated = false;
|
|
531
|
-
let user = await userService.findOneByEmail(email, ["roles"]);
|
|
532
|
-
if (!user) {
|
|
533
|
-
user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
|
|
534
|
-
userCreated = true;
|
|
535
|
-
rolesUpdated = true;
|
|
536
|
-
} else if (fromGroupMapping && roles2.length > 0) {
|
|
537
|
-
const currentRoleIds = new Set(user.roles.map((r) => String(r.id)));
|
|
538
|
-
if (rolesChanged(currentRoleIds, new Set(roles2))) {
|
|
539
|
-
await updateUserRoles(user, currentRoleIds, roles2);
|
|
540
|
-
rolesUpdated = true;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
713
|
const jwtToken = await oauthService2.generateToken(user, ctx);
|
|
544
714
|
oauthService2.triggerSignInSuccess(user);
|
|
545
|
-
return { activateUser: user, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
|
|
546
|
-
}
|
|
547
|
-
function classifyOidcError(msg, userInfo) {
|
|
548
|
-
if (msg.includes("whitelist")) {
|
|
549
|
-
return {
|
|
550
|
-
action: "whitelist_rejected",
|
|
551
|
-
code: errorCodes.WHITELIST_CHECK_FAILED,
|
|
552
|
-
key: "whitelist_rejected"
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
if (msg === "Nonce mismatch")
|
|
556
|
-
return { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH };
|
|
557
|
-
if (msg === "Token exchange failed")
|
|
558
|
-
return { action: "token_exchange_failed", code: errorCodes.TOKEN_EXCHANGE_FAILED };
|
|
559
|
-
if (msg === "Failed to fetch user info") {
|
|
560
|
-
return {
|
|
561
|
-
action: "login_failure",
|
|
562
|
-
code: errorCodes.USERINFO_FETCH_FAILED,
|
|
563
|
-
key: "userinfo_fetch_failed"
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
if (msg === "Failed to parse ID token") {
|
|
567
|
-
return {
|
|
568
|
-
action: "login_failure",
|
|
569
|
-
code: errorCodes.ID_TOKEN_PARSE_FAILED,
|
|
570
|
-
key: "id_token_parse_failed",
|
|
571
|
-
params: { error: msg }
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
if (msg === "User creation failed" || msg.includes("createUser")) {
|
|
575
|
-
return {
|
|
576
|
-
action: "login_failure",
|
|
577
|
-
code: errorCodes.USER_CREATION_FAILED,
|
|
578
|
-
key: "user_creation_failed",
|
|
579
|
-
params: userInfo?.email ? { email: userInfo.email, error: msg } : void 0
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
715
|
return {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
716
|
+
activateUser: user,
|
|
717
|
+
jwtToken,
|
|
718
|
+
userCreated,
|
|
719
|
+
rolesUpdated,
|
|
720
|
+
resolvedRoleNames: resolved.resolvedRoleNames
|
|
587
721
|
};
|
|
588
722
|
}
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
const
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (
|
|
597
|
-
|
|
598
|
-
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
|
|
723
|
+
function classifyOidcError(e, userInfo) {
|
|
724
|
+
const kind = e instanceof OidcError ? e.kind : "unknown";
|
|
725
|
+
const dispatch = OIDC_ERROR_DISPATCH[kind];
|
|
726
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
727
|
+
let params;
|
|
728
|
+
if (kind === "id_token_parse_failed" || kind === "unknown") {
|
|
729
|
+
params = { error: msg };
|
|
730
|
+
} else if (kind === "user_creation_failed" && userInfo?.email) {
|
|
731
|
+
params = { email: userInfo.email, error: msg };
|
|
599
732
|
}
|
|
733
|
+
return {
|
|
734
|
+
action: dispatch.action,
|
|
735
|
+
code: dispatch.code,
|
|
736
|
+
key: dispatch.key,
|
|
737
|
+
params
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function readAndClearPkceCookies(ctx) {
|
|
600
741
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
601
742
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
602
743
|
const oidcNonce = ctx.cookies.get("oidc_nonce");
|
|
603
744
|
ctx.cookies.set("oidc_state", null);
|
|
604
745
|
ctx.cookies.set("oidc_code_verifier", null);
|
|
605
746
|
ctx.cookies.set("oidc_nonce", null);
|
|
747
|
+
return { oidcState, codeVerifier, oidcNonce };
|
|
748
|
+
}
|
|
749
|
+
async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated, resolvedRoleNames) {
|
|
750
|
+
const roles2 = resolvedRoleNames.join(", ");
|
|
751
|
+
const entries = [
|
|
752
|
+
auditLog2.log({
|
|
753
|
+
action: "login_success",
|
|
754
|
+
email: user.email,
|
|
755
|
+
ip: ctx.ip,
|
|
756
|
+
detailsKey: rolesUpdated ? "roles_updated" : void 0,
|
|
757
|
+
detailsParams: rolesUpdated ? { roles: roles2 } : void 0
|
|
758
|
+
})
|
|
759
|
+
];
|
|
760
|
+
if (userCreated) {
|
|
761
|
+
entries.push(
|
|
762
|
+
auditLog2.log({
|
|
763
|
+
action: "user_created",
|
|
764
|
+
email: user.email,
|
|
765
|
+
ip: ctx.ip,
|
|
766
|
+
detailsKey: "user_created",
|
|
767
|
+
detailsParams: { roles: roles2 }
|
|
768
|
+
})
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
await Promise.all(entries);
|
|
772
|
+
}
|
|
773
|
+
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
774
|
+
const errorInfo = classifyOidcError(e, userInfo);
|
|
775
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
776
|
+
await auditLog2.log({
|
|
777
|
+
action: errorInfo.action,
|
|
778
|
+
email: userInfo?.email,
|
|
779
|
+
ip: ctx.ip,
|
|
780
|
+
detailsKey: errorInfo.action,
|
|
781
|
+
detailsParams: errorInfo.action === "login_failure" ? { message } : void 0
|
|
782
|
+
});
|
|
783
|
+
strapi.log.error({
|
|
784
|
+
code: errorInfo.code,
|
|
785
|
+
phase: "oidc_callback",
|
|
786
|
+
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
787
|
+
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
788
|
+
email: userInfo?.email
|
|
789
|
+
});
|
|
790
|
+
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
791
|
+
ctx.send(oauthService2.renderSignUpError(userFacingMessages(locale).signInError, locale));
|
|
792
|
+
}
|
|
793
|
+
async function oidcSignInCallback(ctx) {
|
|
794
|
+
const config2 = configValidation();
|
|
795
|
+
const oauthService2 = getOauthService();
|
|
796
|
+
const auditLog2 = getAuditLogService();
|
|
797
|
+
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
798
|
+
if (!ctx.query.code) {
|
|
799
|
+
await auditLog2.log({ action: "missing_code", ip: ctx.ip });
|
|
800
|
+
return ctx.send(
|
|
801
|
+
oauthService2.renderSignUpError(userFacingMessages(locale).missing_code, locale)
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
const { oidcState, codeVerifier, oidcNonce } = readAndClearPkceCookies(ctx);
|
|
606
805
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
607
806
|
await auditLog2.log({ action: "state_mismatch", ip: ctx.ip });
|
|
608
|
-
return ctx.send(
|
|
807
|
+
return ctx.send(
|
|
808
|
+
oauthService2.renderSignUpError(userFacingMessages(locale).invalid_state, locale)
|
|
809
|
+
);
|
|
609
810
|
}
|
|
610
811
|
const params = new URLSearchParams({
|
|
611
812
|
code: ctx.query.code,
|
|
@@ -619,107 +820,81 @@ async function oidcSignInCallback(ctx) {
|
|
|
619
820
|
try {
|
|
620
821
|
const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
621
822
|
userInfo = exchangeResult.userInfo;
|
|
622
|
-
const accessToken = exchangeResult.accessToken;
|
|
623
823
|
const isProduction = strapi.config.get("environment") === "production";
|
|
624
|
-
ctx.
|
|
824
|
+
const secureFlag = isProduction && ctx.request.secure;
|
|
825
|
+
ctx.cookies.set("oidc_access_token", exchangeResult.accessToken, {
|
|
625
826
|
httpOnly: true,
|
|
626
827
|
maxAge: 3e5,
|
|
627
|
-
|
|
628
|
-
secure: isProduction && ctx.request.secure,
|
|
828
|
+
secure: secureFlag,
|
|
629
829
|
sameSite: "lax"
|
|
630
830
|
});
|
|
631
831
|
const { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames } = await handleUserAuthentication(
|
|
632
|
-
|
|
832
|
+
getAdminUserService(),
|
|
633
833
|
oauthService2,
|
|
634
|
-
|
|
635
|
-
|
|
834
|
+
getRoleService(),
|
|
835
|
+
getWhitelistService(),
|
|
636
836
|
userInfo,
|
|
637
837
|
config2,
|
|
638
838
|
ctx
|
|
639
839
|
);
|
|
640
|
-
|
|
840
|
+
ctx.cookies.set("oidc_user_email", activateUser.email, {
|
|
641
841
|
httpOnly: true,
|
|
642
842
|
path: "/",
|
|
643
|
-
secure:
|
|
843
|
+
secure: secureFlag,
|
|
644
844
|
sameSite: "lax"
|
|
645
|
-
};
|
|
646
|
-
ctx.cookies.set("oidc_user_email", activateUser.email, identityCookieOptions);
|
|
647
|
-
if (userCreated) {
|
|
648
|
-
await auditLog2.log({
|
|
649
|
-
action: "user_created",
|
|
650
|
-
email: activateUser.email,
|
|
651
|
-
ip: ctx.ip,
|
|
652
|
-
detailsKey: "user_created",
|
|
653
|
-
detailsParams: { roles: resolvedRoleNames.join(", ") }
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
await auditLog2.log({
|
|
657
|
-
action: "login_success",
|
|
658
|
-
email: activateUser.email,
|
|
659
|
-
ip: ctx.ip,
|
|
660
|
-
detailsKey: rolesUpdated ? "roles_updated" : void 0,
|
|
661
|
-
detailsParams: rolesUpdated ? { roles: resolvedRoleNames.join(", ") } : void 0
|
|
662
845
|
});
|
|
846
|
+
await logSuccessfulAuth(
|
|
847
|
+
auditLog2,
|
|
848
|
+
ctx,
|
|
849
|
+
activateUser,
|
|
850
|
+
userCreated,
|
|
851
|
+
rolesUpdated,
|
|
852
|
+
resolvedRoleNames
|
|
853
|
+
);
|
|
663
854
|
const nonce = node_crypto.randomUUID();
|
|
664
|
-
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
665
855
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
666
|
-
ctx.send(
|
|
856
|
+
ctx.send(oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce, locale));
|
|
667
857
|
} catch (e) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
});
|
|
677
|
-
strapi.log.error({
|
|
678
|
-
code: errorInfo.code,
|
|
679
|
-
phase: "oidc_callback",
|
|
680
|
-
message: msg || "Unknown sign-in error",
|
|
681
|
-
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
682
|
-
email: userInfo?.email
|
|
858
|
+
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function isProviderSessionActive(userinfoEndpoint, accessToken) {
|
|
862
|
+
try {
|
|
863
|
+
const response = await fetch(userinfoEndpoint, {
|
|
864
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
865
|
+
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
683
866
|
});
|
|
684
|
-
|
|
867
|
+
return response.ok;
|
|
868
|
+
} catch {
|
|
869
|
+
return false;
|
|
685
870
|
}
|
|
686
871
|
}
|
|
687
872
|
async function logout(ctx) {
|
|
688
873
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
689
|
-
const auditLog2 =
|
|
874
|
+
const auditLog2 = getAuditLogService();
|
|
690
875
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
691
876
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
877
|
+
const loginUrl = `${adminPanelUrl}/auth/login`;
|
|
692
878
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
693
879
|
const accessToken = ctx.cookies.get("oidc_access_token");
|
|
694
880
|
const userEmail = ctx.cookies.get("oidc_user_email") ?? void 0;
|
|
695
881
|
clearAuthCookies(strapi, ctx);
|
|
696
|
-
if (
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
882
|
+
if (!isOidcSession) {
|
|
883
|
+
return ctx.redirect(loginUrl);
|
|
884
|
+
}
|
|
885
|
+
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: ctx.ip }) : Promise.resolve();
|
|
886
|
+
if (logoutUrl && accessToken) {
|
|
887
|
+
const active = await isProviderSessionActive(config2.OIDC_USERINFO_ENDPOINT, accessToken);
|
|
888
|
+
if (active) {
|
|
889
|
+
logAudit("logout").catch(() => {
|
|
700
890
|
});
|
|
701
|
-
|
|
702
|
-
if (userEmail)
|
|
703
|
-
auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip }).catch(() => {
|
|
704
|
-
});
|
|
705
|
-
return ctx.redirect(logoutUrl);
|
|
706
|
-
}
|
|
707
|
-
if (userEmail)
|
|
708
|
-
await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
|
|
709
|
-
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
710
|
-
} catch {
|
|
711
|
-
if (userEmail)
|
|
712
|
-
await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
|
|
713
|
-
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
891
|
+
return ctx.redirect(logoutUrl);
|
|
714
892
|
}
|
|
893
|
+
await logAudit("session_expired");
|
|
894
|
+
return ctx.redirect(loginUrl);
|
|
715
895
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
if (logoutUrl && isOidcSession) {
|
|
720
|
-
return ctx.redirect(logoutUrl);
|
|
721
|
-
}
|
|
722
|
-
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
896
|
+
await logAudit("logout");
|
|
897
|
+
ctx.redirect(logoutUrl || loginUrl);
|
|
723
898
|
}
|
|
724
899
|
const oidc = {
|
|
725
900
|
oidcSignIn,
|
|
@@ -727,7 +902,7 @@ const oidc = {
|
|
|
727
902
|
logout
|
|
728
903
|
};
|
|
729
904
|
async function find$1(ctx) {
|
|
730
|
-
const roleService2 =
|
|
905
|
+
const roleService2 = getRoleService();
|
|
731
906
|
const roles2 = await roleService2.find();
|
|
732
907
|
const oidcConstants = roleService2.getOidcRoles();
|
|
733
908
|
for (const oidc2 of oidcConstants) {
|
|
@@ -741,7 +916,7 @@ async function find$1(ctx) {
|
|
|
741
916
|
async function update(ctx) {
|
|
742
917
|
try {
|
|
743
918
|
const { roles: roles2 } = ctx.request.body;
|
|
744
|
-
const roleService2 =
|
|
919
|
+
const roleService2 = getRoleService();
|
|
745
920
|
await roleService2.update(roles2);
|
|
746
921
|
ctx.send({}, 204);
|
|
747
922
|
} catch (e) {
|
|
@@ -762,8 +937,17 @@ function formatDatetimeForFilename(date) {
|
|
|
762
937
|
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
763
938
|
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
|
|
764
939
|
}
|
|
765
|
-
function
|
|
766
|
-
|
|
940
|
+
function setJsonAttachmentHeaders(ctx, basename) {
|
|
941
|
+
const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
|
|
942
|
+
ctx.set("Content-Type", "application/json");
|
|
943
|
+
ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.json"`);
|
|
944
|
+
}
|
|
945
|
+
function setNdjsonAttachmentHeaders(ctx, basename) {
|
|
946
|
+
const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
|
|
947
|
+
ctx.set("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
948
|
+
ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.ndjson"`);
|
|
949
|
+
ctx.set("Cache-Control", "no-store");
|
|
950
|
+
ctx.set("X-Content-Type-Options", "nosniff");
|
|
767
951
|
}
|
|
768
952
|
async function info(ctx) {
|
|
769
953
|
const whitelistService2 = getWhitelistService();
|
|
@@ -778,8 +962,9 @@ async function info(ctx) {
|
|
|
778
962
|
};
|
|
779
963
|
}
|
|
780
964
|
async function updateSettings(ctx) {
|
|
781
|
-
const
|
|
782
|
-
|
|
965
|
+
const body = ctx.request.body;
|
|
966
|
+
const { useWhitelist } = body;
|
|
967
|
+
let { enforceOIDC } = body;
|
|
783
968
|
const whitelistService2 = getWhitelistService();
|
|
784
969
|
if (useWhitelist && enforceOIDC) {
|
|
785
970
|
const users = await whitelistService2.getUsers();
|
|
@@ -808,11 +993,9 @@ async function register(ctx) {
|
|
|
808
993
|
const rawEmails = Array.isArray(email) ? email : email.split(",");
|
|
809
994
|
const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
|
|
810
995
|
const whitelistService2 = getWhitelistService();
|
|
811
|
-
|
|
996
|
+
const matchedExistingUsersCount = await whitelistService2.countAdminUsersByEmails(emailList);
|
|
812
997
|
for (const singleEmail of emailList) {
|
|
813
|
-
const
|
|
814
|
-
if (existingUser) matchedExistingUsersCount++;
|
|
815
|
-
const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({ where: { email: singleEmail } });
|
|
998
|
+
const alreadyWhitelisted = await whitelistService2.hasUser(singleEmail);
|
|
816
999
|
if (!alreadyWhitelisted) {
|
|
817
1000
|
await whitelistService2.registerUser(singleEmail);
|
|
818
1001
|
}
|
|
@@ -826,13 +1009,12 @@ async function removeEmail(ctx) {
|
|
|
826
1009
|
ctx.body = {};
|
|
827
1010
|
}
|
|
828
1011
|
async function deleteAll(ctx) {
|
|
829
|
-
|
|
1012
|
+
const whitelistService2 = getWhitelistService();
|
|
1013
|
+
await whitelistService2.deleteAllUsers();
|
|
830
1014
|
ctx.body = {};
|
|
831
1015
|
}
|
|
832
1016
|
async function exportWhitelist(ctx) {
|
|
833
|
-
|
|
834
|
-
ctx.set("Content-Type", "application/json");
|
|
835
|
-
ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-whitelist-${datetime}.json"`);
|
|
1017
|
+
setJsonAttachmentHeaders(ctx, "strapi-oidc-whitelist");
|
|
836
1018
|
const whitelistService2 = getWhitelistService();
|
|
837
1019
|
const users = await whitelistService2.getUsers();
|
|
838
1020
|
ctx.body = users.map((u) => ({ email: u.email }));
|
|
@@ -844,7 +1026,7 @@ async function importUsers(ctx) {
|
|
|
844
1026
|
ctx.body = { error: "Expected { users: [{email}] }" };
|
|
845
1027
|
return;
|
|
846
1028
|
}
|
|
847
|
-
const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter(
|
|
1029
|
+
const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter(isValidEmail);
|
|
848
1030
|
const deduped = [...new Set(normalized)];
|
|
849
1031
|
const whitelistService2 = getWhitelistService();
|
|
850
1032
|
const existing = await whitelistService2.getUsers();
|
|
@@ -859,7 +1041,7 @@ async function importUsers(ctx) {
|
|
|
859
1041
|
}
|
|
860
1042
|
async function syncUsers(ctx) {
|
|
861
1043
|
const { users: rawUsers } = ctx.request.body;
|
|
862
|
-
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(
|
|
1044
|
+
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(isValidEmail);
|
|
863
1045
|
const whitelistService2 = getWhitelistService();
|
|
864
1046
|
const currentUsers = await whitelistService2.getUsers();
|
|
865
1047
|
const syncEmailSet = new Set(emails);
|
|
@@ -887,44 +1069,194 @@ const whitelist = {
|
|
|
887
1069
|
importUsers,
|
|
888
1070
|
exportWhitelist
|
|
889
1071
|
};
|
|
890
|
-
|
|
891
|
-
|
|
1072
|
+
const AUDIT_ACTIONS = [
|
|
1073
|
+
"login_success",
|
|
1074
|
+
"login_failure",
|
|
1075
|
+
"missing_code",
|
|
1076
|
+
"state_mismatch",
|
|
1077
|
+
"nonce_mismatch",
|
|
1078
|
+
"token_exchange_failed",
|
|
1079
|
+
"whitelist_rejected",
|
|
1080
|
+
"logout",
|
|
1081
|
+
"session_expired",
|
|
1082
|
+
"user_created"
|
|
1083
|
+
];
|
|
1084
|
+
const ISO_UTC_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1085
|
+
function isIsoUtcDatetime(value) {
|
|
1086
|
+
return typeof value === "string" && ISO_UTC_DATETIME.test(value);
|
|
892
1087
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1088
|
+
const ALLOWED_FIELDS = /* @__PURE__ */ new Set(["action", "email", "ip", "createdAt"]);
|
|
1089
|
+
const STRING_OPERATORS = /* @__PURE__ */ new Set([
|
|
1090
|
+
"$eq",
|
|
1091
|
+
"$contains",
|
|
1092
|
+
"$endsWith",
|
|
1093
|
+
"$null",
|
|
1094
|
+
"$notNull"
|
|
1095
|
+
]);
|
|
1096
|
+
const DATE_OPERATORS = /* @__PURE__ */ new Set(["$gte", "$lt", "$lte", "$between", "$in"]);
|
|
1097
|
+
const ENUM_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$in"]);
|
|
1098
|
+
function isPlainObject(value) {
|
|
1099
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1100
|
+
const proto = Object.getPrototypeOf(value);
|
|
1101
|
+
return proto === Object.prototype || proto === null;
|
|
897
1102
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1103
|
+
function isStringOperator(op) {
|
|
1104
|
+
return STRING_OPERATORS.has(op);
|
|
1105
|
+
}
|
|
1106
|
+
function isDateOperator(op) {
|
|
1107
|
+
return DATE_OPERATORS.has(op);
|
|
1108
|
+
}
|
|
1109
|
+
function isEnumOperator(op) {
|
|
1110
|
+
return ENUM_OPERATORS.has(op);
|
|
1111
|
+
}
|
|
1112
|
+
function isAuditAction(value) {
|
|
1113
|
+
return AUDIT_ACTIONS.includes(value);
|
|
1114
|
+
}
|
|
1115
|
+
class ValidationError extends Error {
|
|
1116
|
+
constructor(message) {
|
|
1117
|
+
super(message);
|
|
1118
|
+
this.name = "ValidationError";
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function requireType(field, op, value, check, expected) {
|
|
1122
|
+
if (!check) {
|
|
1123
|
+
throw new ValidationError(`Operator "${op}" for field "${field}" requires ${expected}`);
|
|
1124
|
+
}
|
|
1125
|
+
return value;
|
|
1126
|
+
}
|
|
1127
|
+
function parseActionOperator(op, opValue) {
|
|
1128
|
+
if (!isEnumOperator(op)) {
|
|
1129
|
+
throw new ValidationError(`Unknown operator "${op}" for field "action"`);
|
|
1130
|
+
}
|
|
1131
|
+
if (op === "$in") {
|
|
1132
|
+
requireType("action", op, opValue, Array.isArray(opValue), "an array value");
|
|
1133
|
+
for (const v of opValue) {
|
|
1134
|
+
if (!isAuditAction(v)) {
|
|
1135
|
+
throw new ValidationError(
|
|
1136
|
+
`Invalid action value "${v}" — must be one of: ${AUDIT_ACTIONS.join(", ")}`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return opValue;
|
|
1141
|
+
}
|
|
1142
|
+
if (!isAuditAction(opValue)) {
|
|
1143
|
+
throw new ValidationError(
|
|
1144
|
+
`Invalid action value "${opValue}" — must be one of: ${AUDIT_ACTIONS.join(", ")}`
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
return opValue;
|
|
1148
|
+
}
|
|
1149
|
+
function parseCreatedAtOperator(op, opValue) {
|
|
1150
|
+
if (!isDateOperator(op)) {
|
|
1151
|
+
throw new ValidationError(`Unknown operator "${op}" for field "createdAt"`);
|
|
1152
|
+
}
|
|
1153
|
+
const expected = 'an ISO-8601 UTC datetime string (e.g. "2024-01-15T00:00:00.000Z")';
|
|
1154
|
+
if (op === "$between") {
|
|
1155
|
+
const isTuple = Array.isArray(opValue) && opValue.length === 2;
|
|
1156
|
+
requireType("createdAt", op, opValue, isTuple, "a tuple [start, end]");
|
|
1157
|
+
const [a, b] = opValue;
|
|
1158
|
+
requireType("createdAt", op, opValue, isIsoUtcDatetime(a) && isIsoUtcDatetime(b), expected);
|
|
1159
|
+
return opValue;
|
|
1160
|
+
}
|
|
1161
|
+
if (op === "$in") {
|
|
1162
|
+
requireType("createdAt", op, opValue, Array.isArray(opValue), "an array value");
|
|
1163
|
+
for (const v of opValue) {
|
|
1164
|
+
requireType("createdAt", op, v, isIsoUtcDatetime(v), expected);
|
|
1165
|
+
}
|
|
1166
|
+
return opValue;
|
|
1167
|
+
}
|
|
1168
|
+
return requireType("createdAt", op, opValue, isIsoUtcDatetime(opValue), expected);
|
|
1169
|
+
}
|
|
1170
|
+
function parseStringFieldOperator(field, op, opValue) {
|
|
1171
|
+
if (!isStringOperator(op)) {
|
|
1172
|
+
throw new ValidationError(`Unknown operator "${op}" for field "${field}"`);
|
|
1173
|
+
}
|
|
1174
|
+
if (op === "$null" || op === "$notNull") {
|
|
1175
|
+
return requireType(field, op, opValue, typeof opValue === "boolean", "a boolean value");
|
|
1176
|
+
}
|
|
1177
|
+
return requireType(field, op, opValue, typeof opValue === "string", "a string value");
|
|
1178
|
+
}
|
|
1179
|
+
function parseFieldOperators(field, fieldValue) {
|
|
1180
|
+
if (!isPlainObject(fieldValue)) {
|
|
1181
|
+
throw new ValidationError(
|
|
1182
|
+
`Filter field "${field}" must be an object of operators, got ${typeof fieldValue}`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
const parsed = {};
|
|
1186
|
+
for (const [op, opValue] of Object.entries(fieldValue)) {
|
|
1187
|
+
if (field === "action") parsed[op] = parseActionOperator(op, opValue);
|
|
1188
|
+
else if (field === "createdAt") parsed[op] = parseCreatedAtOperator(op, opValue);
|
|
1189
|
+
else parsed[op] = parseStringFieldOperator(field, op, opValue);
|
|
1190
|
+
}
|
|
1191
|
+
return Object.keys(parsed).length > 0 ? parsed : null;
|
|
1192
|
+
}
|
|
1193
|
+
function parseAuditLogFilters(query) {
|
|
1194
|
+
if (!isPlainObject(query)) return {};
|
|
1195
|
+
const result = {};
|
|
1196
|
+
const filters = query.filters;
|
|
1197
|
+
if (filters === void 0) return result;
|
|
1198
|
+
if (!isPlainObject(filters)) {
|
|
1199
|
+
throw new ValidationError(`"filters" must be an object, got ${typeof filters}`);
|
|
1200
|
+
}
|
|
1201
|
+
for (const [field, fieldValue] of Object.entries(filters)) {
|
|
1202
|
+
if (!ALLOWED_FIELDS.has(field)) {
|
|
1203
|
+
throw new ValidationError(`Unknown filter field: "${field}"`);
|
|
1204
|
+
}
|
|
1205
|
+
const parsed = parseFieldOperators(field, fieldValue);
|
|
1206
|
+
if (parsed) result[field] = parsed;
|
|
1207
|
+
}
|
|
1208
|
+
return result;
|
|
1209
|
+
}
|
|
1210
|
+
const EXPORT_PAGE_SIZE = 500;
|
|
1211
|
+
async function* ndjsonRowStream(service, filters) {
|
|
905
1212
|
let page = 1;
|
|
906
1213
|
while (true) {
|
|
907
|
-
const { results } = await service.find({ page, pageSize:
|
|
1214
|
+
const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE, filters });
|
|
1215
|
+
if (results.length === 0) return;
|
|
1216
|
+
let chunk = "";
|
|
908
1217
|
for (const row of results) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
createdAt: row.createdAt,
|
|
1218
|
+
chunk += JSON.stringify({
|
|
1219
|
+
datetime: row.createdAt,
|
|
912
1220
|
action: row.action,
|
|
913
1221
|
email: row.email ?? null,
|
|
914
1222
|
ip: row.ip ?? null,
|
|
915
1223
|
details: row.details
|
|
916
|
-
});
|
|
1224
|
+
}) + "\n";
|
|
917
1225
|
}
|
|
918
|
-
|
|
1226
|
+
yield Buffer.from(chunk, "utf8");
|
|
1227
|
+
if (results.length < EXPORT_PAGE_SIZE) return;
|
|
919
1228
|
page++;
|
|
920
1229
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
})
|
|
1230
|
+
}
|
|
1231
|
+
function errorAwareNdjsonStream(strapi2, service, filters) {
|
|
1232
|
+
const gen = ndjsonRowStream(service, filters);
|
|
1233
|
+
const readable = node_stream.Readable.from(gen);
|
|
1234
|
+
readable.on("error", (err) => {
|
|
1235
|
+
strapi2.log.error({ phase: "audit_log_export", err }, "NDJSON export stream failed");
|
|
1236
|
+
});
|
|
1237
|
+
return readable;
|
|
1238
|
+
}
|
|
1239
|
+
function parseFiltersOr400(ctx) {
|
|
1240
|
+
try {
|
|
1241
|
+
return parseAuditLogFilters(ctx.query);
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
ctx.status = 400;
|
|
1244
|
+
ctx.body = { message: err instanceof ValidationError ? err.message : "Invalid filters" };
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async function find(ctx) {
|
|
1249
|
+
const filters = parseFiltersOr400(ctx);
|
|
1250
|
+
if (!filters) return;
|
|
1251
|
+
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
1252
|
+
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
|
|
1253
|
+
ctx.body = await getAuditLogService().find({ page, pageSize, filters });
|
|
1254
|
+
}
|
|
1255
|
+
async function exportLogs(ctx) {
|
|
1256
|
+
const filters = parseFiltersOr400(ctx);
|
|
1257
|
+
if (!filters) return;
|
|
1258
|
+
setNdjsonAttachmentHeaders(ctx, "strapi-oidc-audit-log");
|
|
1259
|
+
ctx.body = errorAwareNdjsonStream(ctx.strapi, getAuditLogService(), filters);
|
|
928
1260
|
}
|
|
929
1261
|
async function clearAll(ctx) {
|
|
930
1262
|
await getAuditLogService().clearAll();
|
|
@@ -1138,10 +1470,10 @@ const routes = {
|
|
|
1138
1470
|
}
|
|
1139
1471
|
};
|
|
1140
1472
|
const policies = {};
|
|
1141
|
-
function renderHtmlTemplate(title, content) {
|
|
1473
|
+
function renderHtmlTemplate(title, content, locale = "en") {
|
|
1142
1474
|
return `
|
|
1143
1475
|
<!doctype html>
|
|
1144
|
-
<html lang="
|
|
1476
|
+
<html lang="${locale}">
|
|
1145
1477
|
<head>
|
|
1146
1478
|
<meta charset="utf-8">
|
|
1147
1479
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -1159,7 +1491,7 @@ function renderHtmlTemplate(title, content) {
|
|
|
1159
1491
|
--icon-color: #d02b20;
|
|
1160
1492
|
--success-bg: #eafbe7;
|
|
1161
1493
|
--success-color: #328048;
|
|
1162
|
-
--shadow: 0 1px
|
|
1494
|
+
--shadow: 0 1px 4 rgba(33, 33, 52, 0.1);
|
|
1163
1495
|
}
|
|
1164
1496
|
@media (prefers-color-scheme: dark) {
|
|
1165
1497
|
:root {
|
|
@@ -1174,7 +1506,7 @@ function renderHtmlTemplate(title, content) {
|
|
|
1174
1506
|
--icon-color: #f23628;
|
|
1175
1507
|
--success-bg: #1c3523;
|
|
1176
1508
|
--success-color: #55ca76;
|
|
1177
|
-
--shadow: 0 1px
|
|
1509
|
+
--shadow: 0 1px 4 rgba(0, 0, 0, 0.5);
|
|
1178
1510
|
}
|
|
1179
1511
|
}
|
|
1180
1512
|
body {
|
|
@@ -1259,14 +1591,11 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1259
1591
|
return {
|
|
1260
1592
|
async createUser(email, lastname, firstname, locale, roles2 = []) {
|
|
1261
1593
|
const userService = strapi2.service("admin::user");
|
|
1262
|
-
|
|
1263
|
-
const dbUser = await userService.findOneByEmail(email.toLocaleLowerCase());
|
|
1264
|
-
if (dbUser) return dbUser;
|
|
1265
|
-
}
|
|
1594
|
+
const normalizedEmail = email.toLowerCase();
|
|
1266
1595
|
const createdUser = await userService.create({
|
|
1267
1596
|
firstname: firstname || "unset",
|
|
1268
1597
|
lastname: lastname || "",
|
|
1269
|
-
email:
|
|
1598
|
+
email: normalizedEmail,
|
|
1270
1599
|
roles: roles2,
|
|
1271
1600
|
preferedLanguage: locale
|
|
1272
1601
|
});
|
|
@@ -1297,35 +1626,36 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1297
1626
|
},
|
|
1298
1627
|
async triggerWebHook(user) {
|
|
1299
1628
|
let ENTRY_CREATE;
|
|
1300
|
-
const webhookStore = strapi2.serviceMap
|
|
1301
|
-
const eventHub = strapi2.serviceMap
|
|
1629
|
+
const webhookStore = strapi2.serviceMap?.get("webhookStore");
|
|
1630
|
+
const eventHub = strapi2.serviceMap?.get("eventHub");
|
|
1302
1631
|
if (webhookStore) {
|
|
1303
1632
|
ENTRY_CREATE = webhookStore.allowedEvents.get("ENTRY_CREATE");
|
|
1304
1633
|
}
|
|
1305
1634
|
const modelDef = strapi2.getModel("admin::user");
|
|
1306
1635
|
const sanitizedEntity = await strapiUtils__default.default.sanitize.sanitizers.defaultSanitizeOutput(
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
},
|
|
1636
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1637
|
+
{ schema: modelDef, getModel: (uid2) => strapi2.getModel(uid2) },
|
|
1638
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1311
1639
|
user
|
|
1312
1640
|
);
|
|
1313
|
-
eventHub
|
|
1641
|
+
eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
|
|
1314
1642
|
model: modelDef.modelName,
|
|
1315
1643
|
entry: sanitizedEntity
|
|
1316
1644
|
});
|
|
1317
1645
|
},
|
|
1318
1646
|
triggerSignInSuccess(user) {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
eventHub.
|
|
1322
|
-
|
|
1647
|
+
const userCopy = { ...user };
|
|
1648
|
+
delete userCopy.password;
|
|
1649
|
+
const eventHub = strapi2.serviceMap?.get("eventHub");
|
|
1650
|
+
eventHub?.emit("admin.auth.success", {
|
|
1651
|
+
user: userCopy,
|
|
1323
1652
|
provider: "strapi-plugin-oidc"
|
|
1324
1653
|
});
|
|
1325
1654
|
},
|
|
1326
|
-
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
1655
|
+
renderSignUpSuccess(jwtToken, user, nonce, locale = "en") {
|
|
1327
1656
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1328
|
-
const isRememberMe = !!config2
|
|
1657
|
+
const isRememberMe = !!config2?.REMEMBER_ME;
|
|
1658
|
+
const messages = authPageMessages(locale);
|
|
1329
1659
|
const content = `
|
|
1330
1660
|
<noscript>
|
|
1331
1661
|
<div class="card">
|
|
@@ -1334,8 +1664,8 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1334
1664
|
<path d="M20 6 9 17l-5-5"/>
|
|
1335
1665
|
</svg>
|
|
1336
1666
|
</div>
|
|
1337
|
-
<h1
|
|
1338
|
-
<p
|
|
1667
|
+
<h1>${messages.noscriptHeading}</h1>
|
|
1668
|
+
<p>${messages.noscriptBody}</p>
|
|
1339
1669
|
</div>
|
|
1340
1670
|
</noscript>
|
|
1341
1671
|
<script nonce="${nonce}">
|
|
@@ -1349,9 +1679,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1349
1679
|
location.href = '${strapi2.config.admin.url}'
|
|
1350
1680
|
})
|
|
1351
1681
|
<\/script>`;
|
|
1352
|
-
return renderHtmlTemplate(
|
|
1682
|
+
return renderHtmlTemplate(messages.authenticatingTitle, content, locale);
|
|
1353
1683
|
},
|
|
1354
|
-
renderSignUpError(message) {
|
|
1684
|
+
renderSignUpError(message, locale = "en") {
|
|
1685
|
+
const messages = authPageMessages(locale);
|
|
1355
1686
|
const safeMessage = String(message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1356
1687
|
const content = `
|
|
1357
1688
|
<div class="card">
|
|
@@ -1362,11 +1693,11 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1362
1693
|
<path d="M12 17h.01"/>
|
|
1363
1694
|
</svg>
|
|
1364
1695
|
</div>
|
|
1365
|
-
<h1
|
|
1696
|
+
<h1>${messages.errorTitle}</h1>
|
|
1366
1697
|
<p>${safeMessage}</p>
|
|
1367
|
-
<a href="${strapi2.config.admin.url}" class="btn"
|
|
1698
|
+
<a href="${strapi2.config.admin.url}" class="btn">${messages.returnToLogin}</a>
|
|
1368
1699
|
</div>`;
|
|
1369
|
-
return renderHtmlTemplate(
|
|
1700
|
+
return renderHtmlTemplate(messages.errorTitle, content, locale);
|
|
1370
1701
|
},
|
|
1371
1702
|
async generateToken(user, ctx) {
|
|
1372
1703
|
const sessionManager = strapi2.sessionManager;
|
|
@@ -1376,12 +1707,15 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1376
1707
|
const userId = String(user.id);
|
|
1377
1708
|
const deviceId = node_crypto.randomUUID();
|
|
1378
1709
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1379
|
-
const rememberMe = !!config2
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1710
|
+
const rememberMe = !!config2?.REMEMBER_ME;
|
|
1711
|
+
const smAdmin = sessionManager("admin");
|
|
1712
|
+
const { token: refreshToken, absoluteExpiresAt } = await smAdmin.generateRefreshToken(
|
|
1713
|
+
userId,
|
|
1714
|
+
deviceId,
|
|
1715
|
+
{
|
|
1716
|
+
type: rememberMe ? "refresh" : "session"
|
|
1717
|
+
}
|
|
1718
|
+
);
|
|
1385
1719
|
const isProduction = strapi2.config.get("environment") === "production";
|
|
1386
1720
|
const domain = strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain");
|
|
1387
1721
|
const path = strapi2.config.get("admin.auth.cookie.path", "/admin");
|
|
@@ -1398,7 +1732,6 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1398
1732
|
const idleLifespanSec = strapi2.config.get(
|
|
1399
1733
|
"admin.auth.sessions.idleRefreshTokenLifespan",
|
|
1400
1734
|
1209600
|
|
1401
|
-
// 14 days — Strapi default
|
|
1402
1735
|
);
|
|
1403
1736
|
const idleMs = idleLifespanSec * 1e3;
|
|
1404
1737
|
const absoluteMs = new Date(absoluteExpiresAt).getTime() - Date.now();
|
|
@@ -1408,7 +1741,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1408
1741
|
}
|
|
1409
1742
|
ctx.cookies.set("strapi_admin_refresh", refreshToken, cookieOptions);
|
|
1410
1743
|
ctx.cookies.set("oidc_authenticated", "1", { ...cookieOptions, path: "/" });
|
|
1411
|
-
const accessResult = await
|
|
1744
|
+
const accessResult = await smAdmin.generateAccessToken(refreshToken);
|
|
1412
1745
|
if ("error" in accessResult) {
|
|
1413
1746
|
throw new Error(accessResult.error);
|
|
1414
1747
|
}
|
|
@@ -1499,8 +1832,23 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1499
1832
|
const result = await getWhitelistQuery().findOne({
|
|
1500
1833
|
where: { email }
|
|
1501
1834
|
});
|
|
1502
|
-
if (!result) throw new
|
|
1835
|
+
if (!result) throw new OidcError("whitelist_rejected", errorMessages.WHITELIST_NOT_PRESENT);
|
|
1503
1836
|
return result;
|
|
1837
|
+
},
|
|
1838
|
+
async hasUser(email) {
|
|
1839
|
+
const row = await getWhitelistQuery().findOne({ where: { email }, select: ["id"] });
|
|
1840
|
+
return !!row;
|
|
1841
|
+
},
|
|
1842
|
+
async deleteAllUsers() {
|
|
1843
|
+
await getWhitelistQuery().deleteMany({});
|
|
1844
|
+
},
|
|
1845
|
+
async countAdminUsersByEmails(emails) {
|
|
1846
|
+
if (emails.length === 0) return 0;
|
|
1847
|
+
const rows = await strapi2.query("admin::user").findMany({
|
|
1848
|
+
where: { email: { $in: emails } },
|
|
1849
|
+
select: ["id"]
|
|
1850
|
+
});
|
|
1851
|
+
return rows.length;
|
|
1504
1852
|
}
|
|
1505
1853
|
};
|
|
1506
1854
|
}
|
|
@@ -1513,6 +1861,58 @@ function translateDetails(key, params) {
|
|
|
1513
1861
|
if (!translation) return null;
|
|
1514
1862
|
return interpolate(translation, params);
|
|
1515
1863
|
}
|
|
1864
|
+
const STRING_OP_MAP = {
|
|
1865
|
+
$eq: (v) => v,
|
|
1866
|
+
$contains: (v) => ({ $containsi: v }),
|
|
1867
|
+
$endsWith: (v) => ({ $endsWith: v }),
|
|
1868
|
+
$null: (v) => v === true ? null : void 0,
|
|
1869
|
+
$notNull: (v) => v === true ? { $notNull: true } : void 0
|
|
1870
|
+
};
|
|
1871
|
+
const DATE_OP_MAP = {
|
|
1872
|
+
$gte: (v) => ({ $gte: v }),
|
|
1873
|
+
$lt: (v) => ({ $lt: v }),
|
|
1874
|
+
$lte: (v) => ({ $lte: v }),
|
|
1875
|
+
$between: (v) => ({ $between: v })
|
|
1876
|
+
// $in is handled separately: each ISO day-start is expanded to a [day, day+1) range.
|
|
1877
|
+
};
|
|
1878
|
+
const DAY_MS = 864e5;
|
|
1879
|
+
function nextDayIso(iso) {
|
|
1880
|
+
return new Date(new Date(iso).getTime() + DAY_MS).toISOString();
|
|
1881
|
+
}
|
|
1882
|
+
function expandCreatedAtInToDayRanges(days) {
|
|
1883
|
+
const ranges = days.map((d) => ({ createdAt: { $gte: d, $lt: nextDayIso(d) } }));
|
|
1884
|
+
return ranges.length === 1 ? ranges[0] : { $or: ranges };
|
|
1885
|
+
}
|
|
1886
|
+
const ACTION_OP_MAP = {
|
|
1887
|
+
$eq: (v) => v,
|
|
1888
|
+
$in: (v) => ({ $in: v })
|
|
1889
|
+
};
|
|
1890
|
+
function mapFieldFilter(conditions, field, filter, opMap) {
|
|
1891
|
+
for (const [op, value] of Object.entries(filter)) {
|
|
1892
|
+
const transform = opMap[op];
|
|
1893
|
+
if (!transform) continue;
|
|
1894
|
+
const result = transform(value);
|
|
1895
|
+
if (result !== void 0) conditions.push({ [field]: result });
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
function buildWhereClause(filters) {
|
|
1899
|
+
const conditions = [];
|
|
1900
|
+
if (filters.action) mapFieldFilter(conditions, "action", filters.action, ACTION_OP_MAP);
|
|
1901
|
+
if (filters.email) mapFieldFilter(conditions, "email", filters.email, STRING_OP_MAP);
|
|
1902
|
+
if (filters.ip) mapFieldFilter(conditions, "ip", filters.ip, STRING_OP_MAP);
|
|
1903
|
+
if (filters.createdAt) {
|
|
1904
|
+
const { $in: inDays, ...rest } = filters.createdAt;
|
|
1905
|
+
if (Array.isArray(inDays) && inDays.length > 0) {
|
|
1906
|
+
conditions.push(expandCreatedAtInToDayRanges(inDays));
|
|
1907
|
+
}
|
|
1908
|
+
if (Object.keys(rest).length > 0) {
|
|
1909
|
+
mapFieldFilter(conditions, "createdAt", rest, DATE_OP_MAP);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (conditions.length === 0) return {};
|
|
1913
|
+
if (conditions.length === 1) return conditions[0];
|
|
1914
|
+
return { $and: conditions };
|
|
1915
|
+
}
|
|
1516
1916
|
function auditLogService({ strapi: strapi2 }) {
|
|
1517
1917
|
return {
|
|
1518
1918
|
async log({ action, email, ip, detailsKey, detailsParams }) {
|
|
@@ -1535,19 +1935,33 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1535
1935
|
});
|
|
1536
1936
|
}
|
|
1537
1937
|
},
|
|
1538
|
-
async find({
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
}
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1938
|
+
async find({
|
|
1939
|
+
page = 1,
|
|
1940
|
+
pageSize = 25,
|
|
1941
|
+
filters
|
|
1942
|
+
} = {}) {
|
|
1943
|
+
const where = filters ? buildWhereClause(filters) : {};
|
|
1944
|
+
const dbQuery = strapi2.db.query("plugin::strapi-plugin-oidc.audit-log");
|
|
1945
|
+
const [rows, total] = await Promise.all([
|
|
1946
|
+
dbQuery.findMany({
|
|
1947
|
+
where,
|
|
1948
|
+
orderBy: [{ createdAt: "desc" }],
|
|
1949
|
+
limit: pageSize,
|
|
1950
|
+
offset: (page - 1) * pageSize
|
|
1951
|
+
}),
|
|
1952
|
+
dbQuery.count({ where })
|
|
1953
|
+
]);
|
|
1548
1954
|
return {
|
|
1549
|
-
results
|
|
1550
|
-
|
|
1955
|
+
results: rows.map((row) => ({
|
|
1956
|
+
...row,
|
|
1957
|
+
details: row.detailsKey ? translateDetails(row.detailsKey, row.detailsParams) : null
|
|
1958
|
+
})),
|
|
1959
|
+
pagination: {
|
|
1960
|
+
page,
|
|
1961
|
+
pageSize,
|
|
1962
|
+
total,
|
|
1963
|
+
pageCount: Math.ceil(total / pageSize)
|
|
1964
|
+
}
|
|
1551
1965
|
};
|
|
1552
1966
|
},
|
|
1553
1967
|
async clearAll() {
|
|
@@ -1559,7 +1973,7 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1559
1973
|
} while (deletedCount === BATCH_SIZE);
|
|
1560
1974
|
},
|
|
1561
1975
|
async cleanup(retentionDays) {
|
|
1562
|
-
const cutoff = new Date(Date.now() - retentionDays *
|
|
1976
|
+
const cutoff = new Date(Date.now() - retentionDays * DAY_MS);
|
|
1563
1977
|
await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").deleteMany({ where: { createdAt: { $lt: cutoff } } });
|
|
1564
1978
|
}
|
|
1565
1979
|
};
|