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