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