strapi-plugin-oidc 1.6.5 → 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-HQ2uuypE.mjs → index-CKyNupYU.mjs} +231 -103
- package/dist/admin/{index-DgUClS5s.mjs → index-D2aMSVmR.mjs} +2 -5
- package/dist/admin/{index-pWwCtdNu.js → index-DUFtPEHD.js} +230 -102
- package/dist/admin/{index-C2BnnDzh.js → index-DVjS4hOr.js} +2 -5
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +238 -162
- package/dist/server/index.mjs +238 -162
- 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",
|
|
@@ -352,6 +364,53 @@ const userFacingMessages = {
|
|
|
352
364
|
return en["user.signInError"];
|
|
353
365
|
}
|
|
354
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
|
+
};
|
|
355
414
|
const REQUIRED_CONFIG_KEYS = [
|
|
356
415
|
"OIDC_CLIENT_ID",
|
|
357
416
|
"OIDC_CLIENT_SECRET",
|
|
@@ -364,6 +423,7 @@ const REQUIRED_CONFIG_KEYS = [
|
|
|
364
423
|
"OIDC_GIVEN_NAME_FIELD",
|
|
365
424
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
366
425
|
];
|
|
426
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 3e3;
|
|
367
427
|
function configValidation() {
|
|
368
428
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
369
429
|
const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
|
|
@@ -381,7 +441,6 @@ async function oidcSignIn(ctx) {
|
|
|
381
441
|
const cookieOptions = {
|
|
382
442
|
httpOnly: true,
|
|
383
443
|
maxAge: 6e5,
|
|
384
|
-
// 10 minutes
|
|
385
444
|
secure: isProduction && ctx.request.secure,
|
|
386
445
|
sameSite: "lax"
|
|
387
446
|
};
|
|
@@ -411,7 +470,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
411
470
|
}
|
|
412
471
|
});
|
|
413
472
|
if (!response.ok) {
|
|
414
|
-
throw new
|
|
473
|
+
throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
|
|
415
474
|
}
|
|
416
475
|
const tokenData = await response.json();
|
|
417
476
|
if (tokenData.id_token) {
|
|
@@ -419,23 +478,23 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
419
478
|
const payloadB64 = tokenData.id_token.split(".")[1];
|
|
420
479
|
const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
421
480
|
if (idTokenPayload.nonce !== expectedNonce) {
|
|
422
|
-
throw new
|
|
481
|
+
throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
|
|
423
482
|
}
|
|
424
483
|
} catch (e) {
|
|
425
|
-
if (e.
|
|
426
|
-
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);
|
|
427
486
|
}
|
|
428
487
|
}
|
|
429
488
|
const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
430
489
|
headers: { Authorization: `Bearer ${tokenData.access_token}` }
|
|
431
490
|
});
|
|
432
491
|
if (!userResponse.ok) {
|
|
433
|
-
throw new
|
|
492
|
+
throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
|
|
434
493
|
}
|
|
435
494
|
const userInfo = await userResponse.json();
|
|
436
495
|
return { userInfo, accessToken: tokenData.access_token };
|
|
437
496
|
}
|
|
438
|
-
function
|
|
497
|
+
function collectGroupMapRoleNames(userInfo, config2) {
|
|
439
498
|
const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
|
|
440
499
|
if (!Array.isArray(rawGroups) || rawGroups.length === 0) return [];
|
|
441
500
|
const groups = rawGroups.filter((g) => typeof g === "string");
|
|
@@ -446,22 +505,15 @@ function resolveRolesFromGroups(userInfo, config2, availableRoles) {
|
|
|
446
505
|
} catch {
|
|
447
506
|
return [];
|
|
448
507
|
}
|
|
449
|
-
const
|
|
508
|
+
const roleNameSet = /* @__PURE__ */ new Set();
|
|
450
509
|
for (const group of groups) {
|
|
451
510
|
const roleNames = groupRoleMap[group];
|
|
452
511
|
if (!roleNames) continue;
|
|
453
512
|
for (const name of roleNames) {
|
|
454
|
-
|
|
455
|
-
if (match) roleIdSet.add(String(match.id));
|
|
513
|
+
roleNameSet.add(name);
|
|
456
514
|
}
|
|
457
515
|
}
|
|
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 };
|
|
516
|
+
return [...roleNameSet];
|
|
465
517
|
}
|
|
466
518
|
async function registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2) {
|
|
467
519
|
const defaultLocale = oauthService2.localeFindByHeader(
|
|
@@ -479,10 +531,7 @@ async function registerNewUser(oauthService2, email, userResponseData, config2,
|
|
|
479
531
|
}
|
|
480
532
|
function rolesChanged(current, next) {
|
|
481
533
|
if (current.size !== next.size) return true;
|
|
482
|
-
|
|
483
|
-
if (!current.has(id)) return true;
|
|
484
|
-
}
|
|
485
|
-
return false;
|
|
534
|
+
return [...next].some((id) => !current.has(id));
|
|
486
535
|
}
|
|
487
536
|
async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
488
537
|
try {
|
|
@@ -508,27 +557,51 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
|
508
557
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
509
558
|
const rawEmail = String(userResponseData.email ?? "");
|
|
510
559
|
const email = rawEmail.toLowerCase();
|
|
511
|
-
if (!email ||
|
|
512
|
-
throw new
|
|
560
|
+
if (!email || !isValidEmail(email)) {
|
|
561
|
+
throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
|
|
513
562
|
}
|
|
514
563
|
await whitelistService2.checkWhitelistForEmail(email);
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
+
}
|
|
523
591
|
let userCreated = false;
|
|
524
592
|
let rolesUpdated = false;
|
|
525
593
|
let user = await userService.findOneByEmail(email, ["roles"]);
|
|
526
594
|
if (!user) {
|
|
527
|
-
|
|
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
|
+
}
|
|
528
601
|
userCreated = true;
|
|
529
602
|
rolesUpdated = true;
|
|
530
603
|
} else if (fromGroupMapping && roles2.length > 0) {
|
|
531
|
-
const currentRoleIds = new Set(user.roles.map((r) => String(r.id)));
|
|
604
|
+
const currentRoleIds = new Set((user.roles ?? []).map((r) => String(r.id)));
|
|
532
605
|
if (rolesChanged(currentRoleIds, new Set(roles2))) {
|
|
533
606
|
await updateUserRoles(user, currentRoleIds, roles2);
|
|
534
607
|
rolesUpdated = true;
|
|
@@ -538,55 +611,30 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
|
|
|
538
611
|
oauthService2.triggerSignInSuccess(user);
|
|
539
612
|
return { activateUser: user, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
|
|
540
613
|
}
|
|
541
|
-
function classifyOidcError(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
|
|
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
|
-
};
|
|
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 };
|
|
575
623
|
}
|
|
576
624
|
return {
|
|
577
|
-
action:
|
|
578
|
-
code:
|
|
579
|
-
key:
|
|
580
|
-
params
|
|
625
|
+
action: dispatch.action,
|
|
626
|
+
code: dispatch.code,
|
|
627
|
+
key: dispatch.key,
|
|
628
|
+
params
|
|
581
629
|
};
|
|
582
630
|
}
|
|
583
631
|
async function oidcSignInCallback(ctx) {
|
|
584
632
|
const config2 = configValidation();
|
|
585
|
-
const userService =
|
|
586
|
-
const oauthService2 =
|
|
587
|
-
const roleService2 =
|
|
588
|
-
const whitelistService2 =
|
|
589
|
-
const auditLog2 =
|
|
633
|
+
const userService = getAdminUserService();
|
|
634
|
+
const oauthService2 = getOauthService();
|
|
635
|
+
const roleService2 = getRoleService();
|
|
636
|
+
const whitelistService2 = getWhitelistService();
|
|
637
|
+
const auditLog2 = getAuditLogService();
|
|
590
638
|
if (!ctx.query.code) {
|
|
591
639
|
await auditLog2.log({ action: "missing_code", ip: ctx.ip });
|
|
592
640
|
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
|
|
@@ -618,7 +666,6 @@ async function oidcSignInCallback(ctx) {
|
|
|
618
666
|
ctx.cookies.set("oidc_access_token", accessToken, {
|
|
619
667
|
httpOnly: true,
|
|
620
668
|
maxAge: 3e5,
|
|
621
|
-
// 5 minutes — matches typical provider access token lifetime
|
|
622
669
|
secure: isProduction && ctx.request.secure,
|
|
623
670
|
sameSite: "lax"
|
|
624
671
|
});
|
|
@@ -659,19 +706,18 @@ async function oidcSignInCallback(ctx) {
|
|
|
659
706
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
660
707
|
ctx.send(html);
|
|
661
708
|
} catch (e) {
|
|
662
|
-
const
|
|
663
|
-
const errorInfo = classifyOidcError(msg, userInfo);
|
|
709
|
+
const errorInfo = classifyOidcError(e, userInfo);
|
|
664
710
|
await auditLog2.log({
|
|
665
711
|
action: errorInfo.action,
|
|
666
712
|
email: userInfo?.email,
|
|
667
713
|
ip: ctx.ip,
|
|
668
714
|
detailsKey: errorInfo.action,
|
|
669
|
-
detailsParams: errorInfo.action === "login_failure" ? { message:
|
|
715
|
+
detailsParams: errorInfo.action === "login_failure" ? { message: e instanceof Error ? e.message : String(e) } : void 0
|
|
670
716
|
});
|
|
671
717
|
strapi.log.error({
|
|
672
718
|
code: errorInfo.code,
|
|
673
719
|
phase: "oidc_callback",
|
|
674
|
-
message:
|
|
720
|
+
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
675
721
|
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
676
722
|
email: userInfo?.email
|
|
677
723
|
});
|
|
@@ -680,7 +726,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
680
726
|
}
|
|
681
727
|
async function logout(ctx) {
|
|
682
728
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
683
|
-
const auditLog2 =
|
|
729
|
+
const auditLog2 = getAuditLogService();
|
|
684
730
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
685
731
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
686
732
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
@@ -690,7 +736,8 @@ async function logout(ctx) {
|
|
|
690
736
|
if (logoutUrl && isOidcSession && accessToken) {
|
|
691
737
|
try {
|
|
692
738
|
const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
693
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
739
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
740
|
+
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
694
741
|
});
|
|
695
742
|
if (response.ok) {
|
|
696
743
|
if (userEmail)
|
|
@@ -721,7 +768,7 @@ const oidc = {
|
|
|
721
768
|
logout
|
|
722
769
|
};
|
|
723
770
|
async function find$1(ctx) {
|
|
724
|
-
const roleService2 =
|
|
771
|
+
const roleService2 = getRoleService();
|
|
725
772
|
const roles2 = await roleService2.find();
|
|
726
773
|
const oidcConstants = roleService2.getOidcRoles();
|
|
727
774
|
for (const oidc2 of oidcConstants) {
|
|
@@ -735,7 +782,7 @@ async function find$1(ctx) {
|
|
|
735
782
|
async function update(ctx) {
|
|
736
783
|
try {
|
|
737
784
|
const { roles: roles2 } = ctx.request.body;
|
|
738
|
-
const roleService2 =
|
|
785
|
+
const roleService2 = getRoleService();
|
|
739
786
|
await roleService2.update(roles2);
|
|
740
787
|
ctx.send({}, 204);
|
|
741
788
|
} catch (e) {
|
|
@@ -756,8 +803,17 @@ function formatDatetimeForFilename(date) {
|
|
|
756
803
|
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
757
804
|
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
|
|
758
805
|
}
|
|
759
|
-
function
|
|
760
|
-
|
|
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");
|
|
761
817
|
}
|
|
762
818
|
async function info(ctx) {
|
|
763
819
|
const whitelistService2 = getWhitelistService();
|
|
@@ -772,8 +828,9 @@ async function info(ctx) {
|
|
|
772
828
|
};
|
|
773
829
|
}
|
|
774
830
|
async function updateSettings(ctx) {
|
|
775
|
-
const
|
|
776
|
-
|
|
831
|
+
const body = ctx.request.body;
|
|
832
|
+
const { useWhitelist } = body;
|
|
833
|
+
let { enforceOIDC } = body;
|
|
777
834
|
const whitelistService2 = getWhitelistService();
|
|
778
835
|
if (useWhitelist && enforceOIDC) {
|
|
779
836
|
const users = await whitelistService2.getUsers();
|
|
@@ -802,11 +859,9 @@ async function register(ctx) {
|
|
|
802
859
|
const rawEmails = Array.isArray(email) ? email : email.split(",");
|
|
803
860
|
const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
|
|
804
861
|
const whitelistService2 = getWhitelistService();
|
|
805
|
-
|
|
862
|
+
const matchedExistingUsersCount = await whitelistService2.countAdminUsersByEmails(emailList);
|
|
806
863
|
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 } });
|
|
864
|
+
const alreadyWhitelisted = await whitelistService2.hasUser(singleEmail);
|
|
810
865
|
if (!alreadyWhitelisted) {
|
|
811
866
|
await whitelistService2.registerUser(singleEmail);
|
|
812
867
|
}
|
|
@@ -820,13 +875,12 @@ async function removeEmail(ctx) {
|
|
|
820
875
|
ctx.body = {};
|
|
821
876
|
}
|
|
822
877
|
async function deleteAll(ctx) {
|
|
823
|
-
|
|
878
|
+
const whitelistService2 = getWhitelistService();
|
|
879
|
+
await whitelistService2.deleteAllUsers();
|
|
824
880
|
ctx.body = {};
|
|
825
881
|
}
|
|
826
882
|
async function exportWhitelist(ctx) {
|
|
827
|
-
|
|
828
|
-
ctx.set("Content-Type", "application/json");
|
|
829
|
-
ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-whitelist-${datetime}.json"`);
|
|
883
|
+
setJsonAttachmentHeaders(ctx, "strapi-oidc-whitelist");
|
|
830
884
|
const whitelistService2 = getWhitelistService();
|
|
831
885
|
const users = await whitelistService2.getUsers();
|
|
832
886
|
ctx.body = users.map((u) => ({ email: u.email }));
|
|
@@ -838,7 +892,7 @@ async function importUsers(ctx) {
|
|
|
838
892
|
ctx.body = { error: "Expected { users: [{email}] }" };
|
|
839
893
|
return;
|
|
840
894
|
}
|
|
841
|
-
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);
|
|
842
896
|
const deduped = [...new Set(normalized)];
|
|
843
897
|
const whitelistService2 = getWhitelistService();
|
|
844
898
|
const existing = await whitelistService2.getUsers();
|
|
@@ -853,7 +907,7 @@ async function importUsers(ctx) {
|
|
|
853
907
|
}
|
|
854
908
|
async function syncUsers(ctx) {
|
|
855
909
|
const { users: rawUsers } = ctx.request.body;
|
|
856
|
-
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(
|
|
910
|
+
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(isValidEmail);
|
|
857
911
|
const whitelistService2 = getWhitelistService();
|
|
858
912
|
const currentUsers = await whitelistService2.getUsers();
|
|
859
913
|
const syncEmailSet = new Set(emails);
|
|
@@ -881,44 +935,49 @@ const whitelist = {
|
|
|
881
935
|
importUsers,
|
|
882
936
|
exportWhitelist
|
|
883
937
|
};
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
887
|
-
async function find(ctx) {
|
|
888
|
-
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
889
|
-
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
|
|
890
|
-
ctx.body = await getAuditLogService().find({ page, pageSize });
|
|
891
|
-
}
|
|
892
|
-
async function exportLogs(ctx) {
|
|
893
|
-
const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
|
|
894
|
-
ctx.set("Content-Type", "application/json");
|
|
895
|
-
ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-audit-log-${datetime}.json"`);
|
|
896
|
-
const service = getAuditLogService();
|
|
897
|
-
const PAGE_SIZE = 1e3;
|
|
898
|
-
const allRows = [];
|
|
938
|
+
const EXPORT_PAGE_SIZE = 500;
|
|
939
|
+
async function* ndjsonRowStream(service) {
|
|
899
940
|
let page = 1;
|
|
900
941
|
while (true) {
|
|
901
|
-
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 = "";
|
|
902
945
|
for (const row of results) {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
createdAt: row.createdAt,
|
|
946
|
+
chunk += JSON.stringify({
|
|
947
|
+
datetime: row.createdAt,
|
|
906
948
|
action: row.action,
|
|
907
949
|
email: row.email ?? null,
|
|
908
950
|
ip: row.ip ?? null,
|
|
909
951
|
details: row.details
|
|
910
|
-
});
|
|
952
|
+
}) + "\n";
|
|
911
953
|
}
|
|
912
|
-
|
|
954
|
+
yield Buffer.from(chunk, "utf8");
|
|
955
|
+
if (results.length < EXPORT_PAGE_SIZE) return;
|
|
913
956
|
page++;
|
|
914
957
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
})
|
|
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);
|
|
922
981
|
}
|
|
923
982
|
async function clearAll(ctx) {
|
|
924
983
|
await getAuditLogService().clearAll();
|
|
@@ -1153,7 +1212,7 @@ function renderHtmlTemplate(title, content) {
|
|
|
1153
1212
|
--icon-color: #d02b20;
|
|
1154
1213
|
--success-bg: #eafbe7;
|
|
1155
1214
|
--success-color: #328048;
|
|
1156
|
-
--shadow: 0 1px
|
|
1215
|
+
--shadow: 0 1px 4 rgba(33, 33, 52, 0.1);
|
|
1157
1216
|
}
|
|
1158
1217
|
@media (prefers-color-scheme: dark) {
|
|
1159
1218
|
:root {
|
|
@@ -1168,7 +1227,7 @@ function renderHtmlTemplate(title, content) {
|
|
|
1168
1227
|
--icon-color: #f23628;
|
|
1169
1228
|
--success-bg: #1c3523;
|
|
1170
1229
|
--success-color: #55ca76;
|
|
1171
|
-
--shadow: 0 1px
|
|
1230
|
+
--shadow: 0 1px 4 rgba(0, 0, 0, 0.5);
|
|
1172
1231
|
}
|
|
1173
1232
|
}
|
|
1174
1233
|
body {
|
|
@@ -1253,14 +1312,11 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1253
1312
|
return {
|
|
1254
1313
|
async createUser(email, lastname, firstname, locale, roles2 = []) {
|
|
1255
1314
|
const userService = strapi2.service("admin::user");
|
|
1256
|
-
|
|
1257
|
-
const dbUser = await userService.findOneByEmail(email.toLocaleLowerCase());
|
|
1258
|
-
if (dbUser) return dbUser;
|
|
1259
|
-
}
|
|
1315
|
+
const normalizedEmail = email.toLowerCase();
|
|
1260
1316
|
const createdUser = await userService.create({
|
|
1261
1317
|
firstname: firstname || "unset",
|
|
1262
1318
|
lastname: lastname || "",
|
|
1263
|
-
email:
|
|
1319
|
+
email: normalizedEmail,
|
|
1264
1320
|
roles: roles2,
|
|
1265
1321
|
preferedLanguage: locale
|
|
1266
1322
|
});
|
|
@@ -1291,35 +1347,35 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1291
1347
|
},
|
|
1292
1348
|
async triggerWebHook(user) {
|
|
1293
1349
|
let ENTRY_CREATE;
|
|
1294
|
-
const webhookStore = strapi2.serviceMap
|
|
1295
|
-
const eventHub = strapi2.serviceMap
|
|
1350
|
+
const webhookStore = strapi2.serviceMap?.get("webhookStore");
|
|
1351
|
+
const eventHub = strapi2.serviceMap?.get("eventHub");
|
|
1296
1352
|
if (webhookStore) {
|
|
1297
1353
|
ENTRY_CREATE = webhookStore.allowedEvents.get("ENTRY_CREATE");
|
|
1298
1354
|
}
|
|
1299
1355
|
const modelDef = strapi2.getModel("admin::user");
|
|
1300
1356
|
const sanitizedEntity = await strapiUtils.sanitize.sanitizers.defaultSanitizeOutput(
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
},
|
|
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
|
|
1305
1360
|
user
|
|
1306
1361
|
);
|
|
1307
|
-
eventHub
|
|
1362
|
+
eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
|
|
1308
1363
|
model: modelDef.modelName,
|
|
1309
1364
|
entry: sanitizedEntity
|
|
1310
1365
|
});
|
|
1311
1366
|
},
|
|
1312
1367
|
triggerSignInSuccess(user) {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
eventHub.
|
|
1316
|
-
|
|
1368
|
+
const userCopy = { ...user };
|
|
1369
|
+
delete userCopy.password;
|
|
1370
|
+
const eventHub = strapi2.serviceMap?.get("eventHub");
|
|
1371
|
+
eventHub?.emit("admin.auth.success", {
|
|
1372
|
+
user: userCopy,
|
|
1317
1373
|
provider: "strapi-plugin-oidc"
|
|
1318
1374
|
});
|
|
1319
1375
|
},
|
|
1320
1376
|
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
1321
1377
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1322
|
-
const isRememberMe = !!config2
|
|
1378
|
+
const isRememberMe = !!config2?.REMEMBER_ME;
|
|
1323
1379
|
const content = `
|
|
1324
1380
|
<noscript>
|
|
1325
1381
|
<div class="card">
|
|
@@ -1370,12 +1426,15 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1370
1426
|
const userId = String(user.id);
|
|
1371
1427
|
const deviceId = randomUUID();
|
|
1372
1428
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1373
|
-
const rememberMe = !!config2
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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
|
+
);
|
|
1379
1438
|
const isProduction = strapi2.config.get("environment") === "production";
|
|
1380
1439
|
const domain = strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain");
|
|
1381
1440
|
const path = strapi2.config.get("admin.auth.cookie.path", "/admin");
|
|
@@ -1392,7 +1451,6 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1392
1451
|
const idleLifespanSec = strapi2.config.get(
|
|
1393
1452
|
"admin.auth.sessions.idleRefreshTokenLifespan",
|
|
1394
1453
|
1209600
|
|
1395
|
-
// 14 days — Strapi default
|
|
1396
1454
|
);
|
|
1397
1455
|
const idleMs = idleLifespanSec * 1e3;
|
|
1398
1456
|
const absoluteMs = new Date(absoluteExpiresAt).getTime() - Date.now();
|
|
@@ -1402,7 +1460,7 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1402
1460
|
}
|
|
1403
1461
|
ctx.cookies.set("strapi_admin_refresh", refreshToken, cookieOptions);
|
|
1404
1462
|
ctx.cookies.set("oidc_authenticated", "1", { ...cookieOptions, path: "/" });
|
|
1405
|
-
const accessResult = await
|
|
1463
|
+
const accessResult = await smAdmin.generateAccessToken(refreshToken);
|
|
1406
1464
|
if ("error" in accessResult) {
|
|
1407
1465
|
throw new Error(accessResult.error);
|
|
1408
1466
|
}
|
|
@@ -1493,8 +1551,23 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1493
1551
|
const result = await getWhitelistQuery().findOne({
|
|
1494
1552
|
where: { email }
|
|
1495
1553
|
});
|
|
1496
|
-
if (!result) throw new
|
|
1554
|
+
if (!result) throw new OidcError("whitelist_rejected", errorMessages.WHITELIST_NOT_PRESENT);
|
|
1497
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;
|
|
1498
1571
|
}
|
|
1499
1572
|
};
|
|
1500
1573
|
}
|
|
@@ -1529,7 +1602,10 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1529
1602
|
});
|
|
1530
1603
|
}
|
|
1531
1604
|
},
|
|
1532
|
-
async find({
|
|
1605
|
+
async find({
|
|
1606
|
+
page = 1,
|
|
1607
|
+
pageSize = 25
|
|
1608
|
+
} = {}) {
|
|
1533
1609
|
const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").findPage({
|
|
1534
1610
|
sort: { createdAt: "desc" },
|
|
1535
1611
|
page,
|