strapi-plugin-oidc 1.8.0 → 1.8.1
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 +6 -12
- package/dist/admin/{index-B-K4X_N9.mjs → index-Bb9-aYb4.mjs} +53 -11
- package/dist/admin/{index-CgG_mHzZ.js → index-Bmg4eTYb.js} +113 -88
- package/dist/admin/{index-8YTLPV3h.mjs → index-BqWd-Iiq.mjs} +72 -47
- package/dist/admin/{index-BSgVStns.js → index-Dk6TYtio.js} +57 -13
- package/dist/admin/index.js +3 -1
- package/dist/admin/index.mjs +3 -1
- package/dist/server/index.js +86 -51
- package/dist/server/index.mjs +86 -51
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -38,8 +38,44 @@ const getRoleService = () => strapi.plugin(PLUGIN_NAME).service("role");
|
|
|
38
38
|
const getWhitelistService = () => strapi.plugin(PLUGIN_NAME).service("whitelist");
|
|
39
39
|
const getAuditLogService = () => strapi.plugin(PLUGIN_NAME).service("auditLog");
|
|
40
40
|
const getAdminUserService = () => strapi.service("admin::user");
|
|
41
|
+
const DISCOVERY_TIMEOUT_MS = 5e3;
|
|
42
|
+
const FIELD_MAP = [
|
|
43
|
+
["issuer", "OIDC_ISSUER"],
|
|
44
|
+
["authorization_endpoint", "OIDC_AUTHORIZATION_ENDPOINT"],
|
|
45
|
+
["token_endpoint", "OIDC_TOKEN_ENDPOINT"],
|
|
46
|
+
["userinfo_endpoint", "OIDC_USERINFO_ENDPOINT"],
|
|
47
|
+
["end_session_endpoint", "OIDC_END_SESSION_ENDPOINT"],
|
|
48
|
+
["jwks_uri", "OIDC_JWKS_URI"]
|
|
49
|
+
];
|
|
50
|
+
async function applyDiscovery(strapi2) {
|
|
51
|
+
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
52
|
+
const discoveryUrl = config2.OIDC_DISCOVERY_URL;
|
|
53
|
+
if (!discoveryUrl) return;
|
|
54
|
+
let doc;
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(discoveryUrl, { signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS) });
|
|
57
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
58
|
+
doc = await res.json();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
strapi2.log.error(
|
|
61
|
+
`[strapi-plugin-oidc] Failed to fetch OIDC discovery document from ${discoveryUrl}: ${e instanceof Error ? e.message : String(e)}`
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const updates = {};
|
|
66
|
+
for (const [docField, configKey] of FIELD_MAP) {
|
|
67
|
+
if (doc[docField]) {
|
|
68
|
+
updates[configKey] = doc[docField];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (Object.keys(updates).length > 0) {
|
|
72
|
+
strapi2.config.set("plugin::strapi-plugin-oidc", { ...config2, ...updates });
|
|
73
|
+
strapi2.log.info(`[strapi-plugin-oidc] Discovery applied: ${Object.keys(updates).join(", ")}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
41
76
|
const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
|
|
42
77
|
async function bootstrap({ strapi: strapi2 }) {
|
|
78
|
+
await applyDiscovery(strapi2);
|
|
43
79
|
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
44
80
|
const tokenRefreshPath = `${adminUrl}/token/refresh`;
|
|
45
81
|
const enforceOidcMiddleware = async (ctx, next) => {
|
|
@@ -151,17 +187,13 @@ function destroy() {
|
|
|
151
187
|
const config = {
|
|
152
188
|
default: {
|
|
153
189
|
REMEMBER_ME: false,
|
|
190
|
+
OIDC_DISCOVERY_URL: "",
|
|
154
191
|
OIDC_REDIRECT_URI: "http://localhost:1337/strapi-plugin-oidc/oidc/callback",
|
|
155
192
|
OIDC_CLIENT_ID: "",
|
|
156
193
|
OIDC_CLIENT_SECRET: "",
|
|
157
194
|
OIDC_SCOPE: "openid profile email",
|
|
158
|
-
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
159
|
-
OIDC_TOKEN_ENDPOINT: "",
|
|
160
|
-
OIDC_USERINFO_ENDPOINT: "",
|
|
161
|
-
OIDC_GRANT_TYPE: "authorization_code",
|
|
162
195
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
163
196
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
164
|
-
OIDC_END_SESSION_ENDPOINT: "",
|
|
165
197
|
OIDC_SSO_BUTTON_TEXT: "Login via SSO",
|
|
166
198
|
OIDC_ENFORCE: null,
|
|
167
199
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
@@ -170,9 +202,14 @@ const config = {
|
|
|
170
202
|
OIDC_GROUP_ROLE_MAP: "{}",
|
|
171
203
|
OIDC_REQUIRE_EMAIL_VERIFIED: true,
|
|
172
204
|
OIDC_TRUSTED_IP_HEADER: "",
|
|
205
|
+
OIDC_FORCE_SECURE_COOKIES: false,
|
|
206
|
+
// Populated at bootstrap from OIDC_DISCOVERY_URL — not user-configurable directly
|
|
207
|
+
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
208
|
+
OIDC_TOKEN_ENDPOINT: "",
|
|
209
|
+
OIDC_USERINFO_ENDPOINT: "",
|
|
210
|
+
OIDC_END_SESSION_ENDPOINT: "",
|
|
173
211
|
OIDC_JWKS_URI: "",
|
|
174
|
-
OIDC_ISSUER: ""
|
|
175
|
-
OIDC_FORCE_SECURE_COOKIES: false
|
|
212
|
+
OIDC_ISSUER: ""
|
|
176
213
|
},
|
|
177
214
|
validator() {
|
|
178
215
|
}
|
|
@@ -228,8 +265,7 @@ function shouldMarkSecure(strapi2, ctx) {
|
|
|
228
265
|
if (config2.OIDC_FORCE_SECURE_COOKIES === true) return true;
|
|
229
266
|
if (ctx.request.secure) return true;
|
|
230
267
|
const proxyTrusted = ctx.app?.proxy === true;
|
|
231
|
-
if (proxyTrusted &&
|
|
232
|
-
return true;
|
|
268
|
+
if (proxyTrusted && ctx.get("x-forwarded-proto") === "https") return true;
|
|
233
269
|
return false;
|
|
234
270
|
}
|
|
235
271
|
function getExpiredCookieOptions(strapi2, ctx) {
|
|
@@ -363,7 +399,6 @@ const en = {
|
|
|
363
399
|
"auditlog.table.ip": "IP",
|
|
364
400
|
"auditlog.table.details": "Details",
|
|
365
401
|
"auditlog.table.empty": "No audit log entries",
|
|
366
|
-
"auditlog.loading": "Loading…",
|
|
367
402
|
"auditlog.clear": "Clear Logs",
|
|
368
403
|
"auditlog.clear.title": "Clear All Logs",
|
|
369
404
|
"auditlog.clear.description": "This will permanently delete all {count, plural, one {# audit log entry} other {# audit log entries}}. This action cannot be undone.",
|
|
@@ -418,7 +453,6 @@ const locales = Object.fromEntries(
|
|
|
418
453
|
return [code ?? "", mod.default];
|
|
419
454
|
})
|
|
420
455
|
);
|
|
421
|
-
Object.keys(locales).filter(Boolean);
|
|
422
456
|
const DEFAULT_LOCALE = "en";
|
|
423
457
|
function parseAcceptLanguage(header) {
|
|
424
458
|
return header.split(",").map((part) => {
|
|
@@ -521,13 +555,13 @@ const OIDC_ERROR_DISPATCH = {
|
|
|
521
555
|
key: "sign_in_unknown"
|
|
522
556
|
}
|
|
523
557
|
};
|
|
524
|
-
const
|
|
558
|
+
const TRUSTED_IP_HEADER = "cf-connecting-ip";
|
|
525
559
|
function getTrustedHeaderName() {
|
|
526
560
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc") ?? {};
|
|
527
561
|
const raw = config2.OIDC_TRUSTED_IP_HEADER;
|
|
528
562
|
if (typeof raw !== "string" || !raw) return void 0;
|
|
529
563
|
const normalized = raw.trim().toLowerCase();
|
|
530
|
-
return
|
|
564
|
+
return normalized === TRUSTED_IP_HEADER ? normalized : void 0;
|
|
531
565
|
}
|
|
532
566
|
function getClientIp(ctx) {
|
|
533
567
|
const proxyTrusted = ctx.app?.proxy === true;
|
|
@@ -544,19 +578,23 @@ function getClientIp(ctx) {
|
|
|
544
578
|
}
|
|
545
579
|
return ctx.ip;
|
|
546
580
|
}
|
|
581
|
+
function toMessage(e) {
|
|
582
|
+
return e instanceof Error ? e.message : String(e);
|
|
583
|
+
}
|
|
547
584
|
const REQUIRED_CONFIG_KEYS = [
|
|
585
|
+
"OIDC_DISCOVERY_URL",
|
|
548
586
|
"OIDC_CLIENT_ID",
|
|
549
587
|
"OIDC_CLIENT_SECRET",
|
|
550
588
|
"OIDC_REDIRECT_URI",
|
|
551
589
|
"OIDC_SCOPE",
|
|
552
|
-
"OIDC_TOKEN_ENDPOINT",
|
|
553
|
-
"OIDC_USERINFO_ENDPOINT",
|
|
554
|
-
"OIDC_GRANT_TYPE",
|
|
555
590
|
"OIDC_FAMILY_NAME_FIELD",
|
|
556
591
|
"OIDC_GIVEN_NAME_FIELD",
|
|
592
|
+
// Populated at bootstrap from OIDC_DISCOVERY_URL — checked here as a runtime safety net
|
|
593
|
+
"OIDC_TOKEN_ENDPOINT",
|
|
594
|
+
"OIDC_USERINFO_ENDPOINT",
|
|
557
595
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
558
596
|
];
|
|
559
|
-
const LOGOUT_USERINFO_TIMEOUT_MS =
|
|
597
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
|
|
560
598
|
const jwksCache = /* @__PURE__ */ new Map();
|
|
561
599
|
let jwksDisabledWarned = false;
|
|
562
600
|
function getJwks(uri) {
|
|
@@ -588,7 +626,7 @@ async function verifyIdToken(idToken, config2) {
|
|
|
588
626
|
return payload;
|
|
589
627
|
} catch (e) {
|
|
590
628
|
if (e instanceof errors.JWTClaimValidationFailed || e instanceof errors.JWSSignatureVerificationFailed || e instanceof errors.JWTExpired || e instanceof errors.JWTInvalid || e instanceof errors.JWSInvalid) {
|
|
591
|
-
const msg =
|
|
629
|
+
const msg = toMessage(e);
|
|
592
630
|
throw new OidcError("id_token_invalid", msg, e);
|
|
593
631
|
}
|
|
594
632
|
throw e;
|
|
@@ -779,7 +817,7 @@ async function ensureUser(userService, oauthService2, email, userResponseData, c
|
|
|
779
817
|
);
|
|
780
818
|
return { user, userCreated: true, rolesUpdated: true };
|
|
781
819
|
} catch (e) {
|
|
782
|
-
const msg =
|
|
820
|
+
const msg = toMessage(e);
|
|
783
821
|
throw new OidcError("user_creation_failed", msg, e);
|
|
784
822
|
}
|
|
785
823
|
}
|
|
@@ -829,7 +867,7 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
|
|
|
829
867
|
function classifyOidcError(e, userInfo) {
|
|
830
868
|
const kind = e instanceof OidcError ? e.kind : "unknown";
|
|
831
869
|
const dispatch = OIDC_ERROR_DISPATCH[kind];
|
|
832
|
-
const msg =
|
|
870
|
+
const msg = toMessage(e);
|
|
833
871
|
let params;
|
|
834
872
|
if (kind === "id_token_parse_failed" || kind === "id_token_invalid" || kind === "unknown") {
|
|
835
873
|
params = { error: msg };
|
|
@@ -878,7 +916,7 @@ async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated
|
|
|
878
916
|
}
|
|
879
917
|
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
880
918
|
const errorInfo = classifyOidcError(e, userInfo);
|
|
881
|
-
const message =
|
|
919
|
+
const message = toMessage(e);
|
|
882
920
|
await auditLog2.log({
|
|
883
921
|
action: errorInfo.action,
|
|
884
922
|
email: userInfo?.email,
|
|
@@ -919,7 +957,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
919
957
|
client_id: config2.OIDC_CLIENT_ID,
|
|
920
958
|
client_secret: config2.OIDC_CLIENT_SECRET,
|
|
921
959
|
redirect_uri: config2.OIDC_REDIRECT_URI,
|
|
922
|
-
grant_type:
|
|
960
|
+
grant_type: "authorization_code",
|
|
923
961
|
code_verifier: codeVerifier ?? ""
|
|
924
962
|
});
|
|
925
963
|
let userInfo;
|
|
@@ -963,13 +1001,13 @@ async function oidcSignInCallback(ctx) {
|
|
|
963
1001
|
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
964
1002
|
}
|
|
965
1003
|
}
|
|
966
|
-
async function
|
|
1004
|
+
async function isProviderSessionExpired(userinfoEndpoint, accessToken) {
|
|
967
1005
|
try {
|
|
968
1006
|
const response = await fetch(userinfoEndpoint, {
|
|
969
1007
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
970
1008
|
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
971
1009
|
});
|
|
972
|
-
return response.ok;
|
|
1010
|
+
return !response.ok;
|
|
973
1011
|
} catch {
|
|
974
1012
|
return false;
|
|
975
1013
|
}
|
|
@@ -989,14 +1027,14 @@ async function logout(ctx) {
|
|
|
989
1027
|
}
|
|
990
1028
|
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: getClientIp(ctx) }) : Promise.resolve();
|
|
991
1029
|
if (logoutUrl && accessToken) {
|
|
992
|
-
const
|
|
993
|
-
if (
|
|
994
|
-
logAudit("
|
|
995
|
-
|
|
996
|
-
return ctx.redirect(logoutUrl);
|
|
1030
|
+
const expired = await isProviderSessionExpired(config2.OIDC_USERINFO_ENDPOINT, accessToken);
|
|
1031
|
+
if (expired) {
|
|
1032
|
+
await logAudit("session_expired");
|
|
1033
|
+
return ctx.redirect(loginUrl);
|
|
997
1034
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1035
|
+
logAudit("logout").catch(() => {
|
|
1036
|
+
});
|
|
1037
|
+
return ctx.redirect(logoutUrl);
|
|
1000
1038
|
}
|
|
1001
1039
|
await logAudit("logout");
|
|
1002
1040
|
ctx.redirect(logoutUrl || loginUrl);
|
|
@@ -1154,13 +1192,9 @@ async function importUsers(ctx) {
|
|
|
1154
1192
|
const whitelistService2 = getWhitelistService();
|
|
1155
1193
|
const existing = await whitelistService2.getUsers();
|
|
1156
1194
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
await whitelistService2.registerUser(email);
|
|
1161
|
-
importedCount++;
|
|
1162
|
-
}
|
|
1163
|
-
ctx.body = { importedCount };
|
|
1195
|
+
const toImport = deduped.filter((email) => !existingEmails.has(email));
|
|
1196
|
+
await Promise.all(toImport.map((email) => whitelistService2.registerUser(email)));
|
|
1197
|
+
ctx.body = { importedCount: toImport.length };
|
|
1164
1198
|
}
|
|
1165
1199
|
async function syncUsers(ctx) {
|
|
1166
1200
|
const { users: rawUsers } = ctx.request.body;
|
|
@@ -1169,16 +1203,10 @@ async function syncUsers(ctx) {
|
|
|
1169
1203
|
const currentUsers = await whitelistService2.getUsers();
|
|
1170
1204
|
const syncEmailSet = new Set(emails);
|
|
1171
1205
|
const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
}
|
|
1177
|
-
for (const email of emails) {
|
|
1178
|
-
if (!currentUsersByEmail.has(email)) {
|
|
1179
|
-
await whitelistService2.registerUser(email);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1206
|
+
await Promise.all([
|
|
1207
|
+
...currentUsers.filter((u) => !syncEmailSet.has(u.email)).map((u) => whitelistService2.removeUser(u.email)),
|
|
1208
|
+
...emails.filter((email) => !currentUsersByEmail.has(email)).map((email) => whitelistService2.registerUser(email))
|
|
1209
|
+
]);
|
|
1182
1210
|
ctx.body = {};
|
|
1183
1211
|
}
|
|
1184
1212
|
const whitelist = {
|
|
@@ -1482,6 +1510,12 @@ const routes = {
|
|
|
1482
1510
|
handler: "oidc.oidcSignInCallback",
|
|
1483
1511
|
config: { auth: false, middlewares: [rateLimitMiddleware] }
|
|
1484
1512
|
},
|
|
1513
|
+
{
|
|
1514
|
+
method: "GET",
|
|
1515
|
+
path: "/logout",
|
|
1516
|
+
handler: "oidc.logout",
|
|
1517
|
+
config: { auth: false }
|
|
1518
|
+
},
|
|
1485
1519
|
{
|
|
1486
1520
|
method: "POST",
|
|
1487
1521
|
path: "/logout",
|
|
@@ -1790,9 +1824,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1790
1824
|
}
|
|
1791
1825
|
const modelDef = strapi2.getModel("admin::user");
|
|
1792
1826
|
const sanitizedEntity = await strapiUtils.sanitize.sanitizers.defaultSanitizeOutput(
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1827
|
+
{
|
|
1828
|
+
schema: modelDef,
|
|
1829
|
+
getModel: (uid2) => strapi2.getModel(uid2)
|
|
1830
|
+
},
|
|
1796
1831
|
user
|
|
1797
1832
|
);
|
|
1798
1833
|
eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
|
package/package.json
CHANGED