strapi-plugin-oidc 1.8.0 → 1.8.2
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 +143 -105
- package/dist/server/index.mjs +143 -105
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -6,6 +6,58 @@ import strapiUtils from "@strapi/utils";
|
|
|
6
6
|
import generator from "generate-password";
|
|
7
7
|
function register$1() {
|
|
8
8
|
}
|
|
9
|
+
const errorCodes = {
|
|
10
|
+
TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
|
|
11
|
+
USERINFO_FETCH_FAILED: "USERINFO_FETCH_FAILED",
|
|
12
|
+
ID_TOKEN_PARSE_FAILED: "ID_TOKEN_PARSE_FAILED",
|
|
13
|
+
NONCE_MISMATCH: "NONCE_MISMATCH",
|
|
14
|
+
ROLE_UPDATE_FAILED: "ROLE_UPDATE_FAILED",
|
|
15
|
+
USER_CREATION_FAILED: "USER_CREATION_FAILED",
|
|
16
|
+
WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED",
|
|
17
|
+
EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED",
|
|
18
|
+
ID_TOKEN_INVALID: "ID_TOKEN_INVALID"
|
|
19
|
+
};
|
|
20
|
+
const ERROR_DETAIL_TEMPLATES = {
|
|
21
|
+
token_exchange_failed: "Token exchange failed with HTTP status {status}",
|
|
22
|
+
userinfo_fetch_failed: "UserInfo endpoint returned HTTP {status}",
|
|
23
|
+
role_update_failed: "Role update failed for user {userId}: {error}",
|
|
24
|
+
user_creation_failed: "User creation failed for {email}: {error}",
|
|
25
|
+
id_token_parse_failed: "ID token parse failed: {error}",
|
|
26
|
+
sign_in_unknown: "Unknown sign-in error: {error}",
|
|
27
|
+
invalid_email: "Invalid email address received from OIDC provider",
|
|
28
|
+
email_not_verified: "Email address has not been verified by the OIDC provider",
|
|
29
|
+
id_token_invalid: "ID token verification failed: {error}",
|
|
30
|
+
whitelist_not_present: "Email not present in whitelist",
|
|
31
|
+
session_manager_unsupported: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later."
|
|
32
|
+
};
|
|
33
|
+
function interpolate$1(template, params) {
|
|
34
|
+
if (!params) return template;
|
|
35
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
|
|
36
|
+
}
|
|
37
|
+
function getErrorDetail(key, params) {
|
|
38
|
+
const template = ERROR_DETAIL_TEMPLATES[key];
|
|
39
|
+
if (!template) return void 0;
|
|
40
|
+
return interpolate$1(template, params);
|
|
41
|
+
}
|
|
42
|
+
const errorMessages = {
|
|
43
|
+
TOKEN_EXCHANGE_FAILED: "Token exchange failed",
|
|
44
|
+
USERINFO_FETCH_FAILED: "Failed to fetch user info",
|
|
45
|
+
ID_TOKEN_PARSE_FAILED: "Failed to parse ID token",
|
|
46
|
+
NONCE_MISMATCH: "Nonce mismatch",
|
|
47
|
+
INVALID_EMAIL: "Invalid email address received from OIDC provider",
|
|
48
|
+
EMAIL_NOT_VERIFIED: "Email address has not been verified by the OIDC provider",
|
|
49
|
+
ID_TOKEN_INVALID: "ID token verification failed",
|
|
50
|
+
WHITELIST_NOT_PRESENT: "Not present in whitelist",
|
|
51
|
+
SESSION_MANAGER_UNSUPPORTED: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
52
|
+
JWKS_URI_NOT_CONFIGURED: "[OIDC] OIDC_JWKS_URI is not configured — ID token signature verification is disabled. Set OIDC_JWKS_URI and OIDC_ISSUER from your provider's discovery document.",
|
|
53
|
+
ENFORCE_MIDDLEWARE_ERROR: "Error checking OIDC enforcement in middleware:",
|
|
54
|
+
ENFORCE_SYNC_ERROR: "[strapi-plugin-oidc] Failed to sync OIDC_ENFORCE to database:",
|
|
55
|
+
DEFAULT_ROLE_INIT_ERROR: "Could not initialize default OIDC role:",
|
|
56
|
+
AUDIT_LOG_CLEANUP_ERROR: "[strapi-plugin-oidc] Audit log cleanup failed:",
|
|
57
|
+
AUDIT_LOG_EXPORT_ERROR: "NDJSON export stream failed",
|
|
58
|
+
DISCOVERY_FETCH_ERROR: (url, reason) => `[strapi-plugin-oidc] Failed to fetch OIDC discovery document from ${url}: ${reason}`,
|
|
59
|
+
MISSING_CONFIG: (keys) => `Missing required config keys: ${keys}`
|
|
60
|
+
};
|
|
9
61
|
function getEnforceOIDCConfig(strapi2) {
|
|
10
62
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
11
63
|
const val = config2.OIDC_ENFORCE;
|
|
@@ -38,8 +90,43 @@ const getRoleService = () => strapi.plugin(PLUGIN_NAME).service("role");
|
|
|
38
90
|
const getWhitelistService = () => strapi.plugin(PLUGIN_NAME).service("whitelist");
|
|
39
91
|
const getAuditLogService = () => strapi.plugin(PLUGIN_NAME).service("auditLog");
|
|
40
92
|
const getAdminUserService = () => strapi.service("admin::user");
|
|
93
|
+
const DISCOVERY_TIMEOUT_MS = 5e3;
|
|
94
|
+
const FIELD_MAP = [
|
|
95
|
+
["issuer", "OIDC_ISSUER"],
|
|
96
|
+
["authorization_endpoint", "OIDC_AUTHORIZATION_ENDPOINT"],
|
|
97
|
+
["token_endpoint", "OIDC_TOKEN_ENDPOINT"],
|
|
98
|
+
["userinfo_endpoint", "OIDC_USERINFO_ENDPOINT"],
|
|
99
|
+
["end_session_endpoint", "OIDC_END_SESSION_ENDPOINT"],
|
|
100
|
+
["jwks_uri", "OIDC_JWKS_URI"]
|
|
101
|
+
];
|
|
102
|
+
async function applyDiscovery(strapi2) {
|
|
103
|
+
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
104
|
+
const discoveryUrl = config2.OIDC_DISCOVERY_URL;
|
|
105
|
+
if (!discoveryUrl) return;
|
|
106
|
+
let doc;
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch(discoveryUrl, { signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS) });
|
|
109
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
110
|
+
doc = await res.json();
|
|
111
|
+
} catch (e) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
errorMessages.DISCOVERY_FETCH_ERROR(discoveryUrl, e instanceof Error ? e.message : String(e))
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const updates = {};
|
|
117
|
+
for (const [docField, configKey] of FIELD_MAP) {
|
|
118
|
+
if (doc[docField]) {
|
|
119
|
+
updates[configKey] = doc[docField];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (Object.keys(updates).length > 0) {
|
|
123
|
+
strapi2.config.set("plugin::strapi-plugin-oidc", { ...config2, ...updates });
|
|
124
|
+
strapi2.log.info(`[strapi-plugin-oidc] Discovery applied: ${Object.keys(updates).join(", ")}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
41
127
|
const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
|
|
42
128
|
async function bootstrap({ strapi: strapi2 }) {
|
|
129
|
+
await applyDiscovery(strapi2);
|
|
43
130
|
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
44
131
|
const tokenRefreshPath = `${adminUrl}/token/refresh`;
|
|
45
132
|
const enforceOidcMiddleware = async (ctx, next) => {
|
|
@@ -79,7 +166,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
79
166
|
return;
|
|
80
167
|
}
|
|
81
168
|
} catch (err) {
|
|
82
|
-
strapi2.log.error(
|
|
169
|
+
strapi2.log.error(errorMessages.ENFORCE_MIDDLEWARE_ERROR, err);
|
|
83
170
|
}
|
|
84
171
|
}
|
|
85
172
|
await next();
|
|
@@ -116,7 +203,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
116
203
|
);
|
|
117
204
|
}
|
|
118
205
|
} catch (err) {
|
|
119
|
-
strapi2.log.error(
|
|
206
|
+
strapi2.log.error(errorMessages.ENFORCE_SYNC_ERROR, err);
|
|
120
207
|
}
|
|
121
208
|
}
|
|
122
209
|
try {
|
|
@@ -130,7 +217,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
130
217
|
}
|
|
131
218
|
}
|
|
132
219
|
} catch (err) {
|
|
133
|
-
strapi2.log.warn(
|
|
220
|
+
strapi2.log.warn(errorMessages.DEFAULT_ROLE_INIT_ERROR, err.message);
|
|
134
221
|
}
|
|
135
222
|
strapi2.cron.add({
|
|
136
223
|
"strapi-plugin-oidc-audit-log-cleanup": {
|
|
@@ -139,7 +226,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
139
226
|
const retentionDays = getRetentionDays();
|
|
140
227
|
await getAuditLogService().cleanup(retentionDays);
|
|
141
228
|
} catch (err) {
|
|
142
|
-
strapi2.log.warn(
|
|
229
|
+
strapi2.log.warn(errorMessages.AUDIT_LOG_CLEANUP_ERROR, err.message);
|
|
143
230
|
}
|
|
144
231
|
},
|
|
145
232
|
options: { rule: "0 0 * * *" }
|
|
@@ -151,17 +238,13 @@ function destroy() {
|
|
|
151
238
|
const config = {
|
|
152
239
|
default: {
|
|
153
240
|
REMEMBER_ME: false,
|
|
241
|
+
OIDC_DISCOVERY_URL: "",
|
|
154
242
|
OIDC_REDIRECT_URI: "http://localhost:1337/strapi-plugin-oidc/oidc/callback",
|
|
155
243
|
OIDC_CLIENT_ID: "",
|
|
156
244
|
OIDC_CLIENT_SECRET: "",
|
|
157
245
|
OIDC_SCOPE: "openid profile email",
|
|
158
|
-
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
159
|
-
OIDC_TOKEN_ENDPOINT: "",
|
|
160
|
-
OIDC_USERINFO_ENDPOINT: "",
|
|
161
|
-
OIDC_GRANT_TYPE: "authorization_code",
|
|
162
246
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
163
247
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
164
|
-
OIDC_END_SESSION_ENDPOINT: "",
|
|
165
248
|
OIDC_SSO_BUTTON_TEXT: "Login via SSO",
|
|
166
249
|
OIDC_ENFORCE: null,
|
|
167
250
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
@@ -170,9 +253,14 @@ const config = {
|
|
|
170
253
|
OIDC_GROUP_ROLE_MAP: "{}",
|
|
171
254
|
OIDC_REQUIRE_EMAIL_VERIFIED: true,
|
|
172
255
|
OIDC_TRUSTED_IP_HEADER: "",
|
|
256
|
+
OIDC_FORCE_SECURE_COOKIES: false,
|
|
257
|
+
// Populated at bootstrap from OIDC_DISCOVERY_URL — not user-configurable directly
|
|
258
|
+
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
259
|
+
OIDC_TOKEN_ENDPOINT: "",
|
|
260
|
+
OIDC_USERINFO_ENDPOINT: "",
|
|
261
|
+
OIDC_END_SESSION_ENDPOINT: "",
|
|
173
262
|
OIDC_JWKS_URI: "",
|
|
174
|
-
OIDC_ISSUER: ""
|
|
175
|
-
OIDC_FORCE_SECURE_COOKIES: false
|
|
263
|
+
OIDC_ISSUER: ""
|
|
176
264
|
},
|
|
177
265
|
validator() {
|
|
178
266
|
}
|
|
@@ -228,8 +316,7 @@ function shouldMarkSecure(strapi2, ctx) {
|
|
|
228
316
|
if (config2.OIDC_FORCE_SECURE_COOKIES === true) return true;
|
|
229
317
|
if (ctx.request.secure) return true;
|
|
230
318
|
const proxyTrusted = ctx.app?.proxy === true;
|
|
231
|
-
if (proxyTrusted &&
|
|
232
|
-
return true;
|
|
319
|
+
if (proxyTrusted && ctx.get("x-forwarded-proto") === "https") return true;
|
|
233
320
|
return false;
|
|
234
321
|
}
|
|
235
322
|
function getExpiredCookieOptions(strapi2, ctx) {
|
|
@@ -255,52 +342,6 @@ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
255
342
|
function isValidEmail(email) {
|
|
256
343
|
return EMAIL_REGEX.test(email);
|
|
257
344
|
}
|
|
258
|
-
const errorCodes = {
|
|
259
|
-
TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
|
|
260
|
-
USERINFO_FETCH_FAILED: "USERINFO_FETCH_FAILED",
|
|
261
|
-
ID_TOKEN_PARSE_FAILED: "ID_TOKEN_PARSE_FAILED",
|
|
262
|
-
NONCE_MISMATCH: "NONCE_MISMATCH",
|
|
263
|
-
ROLE_UPDATE_FAILED: "ROLE_UPDATE_FAILED",
|
|
264
|
-
USER_CREATION_FAILED: "USER_CREATION_FAILED",
|
|
265
|
-
WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED",
|
|
266
|
-
EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED",
|
|
267
|
-
ID_TOKEN_INVALID: "ID_TOKEN_INVALID"
|
|
268
|
-
};
|
|
269
|
-
const ERROR_DETAIL_TEMPLATES = {
|
|
270
|
-
token_exchange_failed: "Token exchange failed with HTTP status {status}",
|
|
271
|
-
userinfo_fetch_failed: "UserInfo endpoint returned HTTP {status}",
|
|
272
|
-
role_update_failed: "Role update failed for user {userId}: {error}",
|
|
273
|
-
user_creation_failed: "User creation failed for {email}: {error}",
|
|
274
|
-
id_token_parse_failed: "ID token parse failed: {error}",
|
|
275
|
-
sign_in_unknown: "Unknown sign-in error: {error}",
|
|
276
|
-
invalid_email: "Invalid email address received from OIDC provider",
|
|
277
|
-
email_not_verified: "Email address has not been verified by the OIDC provider",
|
|
278
|
-
id_token_invalid: "ID token verification failed: {error}",
|
|
279
|
-
whitelist_not_present: "Email not present in whitelist",
|
|
280
|
-
session_manager_unsupported: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
281
|
-
missing_config: "Missing required config keys: {keys}"
|
|
282
|
-
};
|
|
283
|
-
function interpolate$1(template, params) {
|
|
284
|
-
if (!params) return template;
|
|
285
|
-
return template.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
|
|
286
|
-
}
|
|
287
|
-
function getErrorDetail(key, params) {
|
|
288
|
-
const template = ERROR_DETAIL_TEMPLATES[key];
|
|
289
|
-
if (!template) return void 0;
|
|
290
|
-
return interpolate$1(template, params);
|
|
291
|
-
}
|
|
292
|
-
const errorMessages = {
|
|
293
|
-
TOKEN_EXCHANGE_FAILED: "Token exchange failed",
|
|
294
|
-
USERINFO_FETCH_FAILED: "Failed to fetch user info",
|
|
295
|
-
ID_TOKEN_PARSE_FAILED: "Failed to parse ID token",
|
|
296
|
-
NONCE_MISMATCH: "Nonce mismatch",
|
|
297
|
-
INVALID_EMAIL: "Invalid email address received from OIDC provider",
|
|
298
|
-
EMAIL_NOT_VERIFIED: "Email address has not been verified by the OIDC provider",
|
|
299
|
-
ID_TOKEN_INVALID: "ID token verification failed",
|
|
300
|
-
WHITELIST_NOT_PRESENT: "Not present in whitelist",
|
|
301
|
-
SESSION_MANAGER_UNSUPPORTED: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
302
|
-
MISSING_CONFIG: (keys) => `Missing required config keys: ${keys}`
|
|
303
|
-
};
|
|
304
345
|
const en = {
|
|
305
346
|
"global.plugins.strapi-plugin-oidc": "OIDC Plugin",
|
|
306
347
|
"page.title": "Configure OIDC default role(s) and access controls.",
|
|
@@ -363,7 +404,6 @@ const en = {
|
|
|
363
404
|
"auditlog.table.ip": "IP",
|
|
364
405
|
"auditlog.table.details": "Details",
|
|
365
406
|
"auditlog.table.empty": "No audit log entries",
|
|
366
|
-
"auditlog.loading": "Loading…",
|
|
367
407
|
"auditlog.clear": "Clear Logs",
|
|
368
408
|
"auditlog.clear.title": "Clear All Logs",
|
|
369
409
|
"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 +458,6 @@ const locales = Object.fromEntries(
|
|
|
418
458
|
return [code ?? "", mod.default];
|
|
419
459
|
})
|
|
420
460
|
);
|
|
421
|
-
Object.keys(locales).filter(Boolean);
|
|
422
461
|
const DEFAULT_LOCALE = "en";
|
|
423
462
|
function parseAcceptLanguage(header) {
|
|
424
463
|
return header.split(",").map((part) => {
|
|
@@ -521,13 +560,13 @@ const OIDC_ERROR_DISPATCH = {
|
|
|
521
560
|
key: "sign_in_unknown"
|
|
522
561
|
}
|
|
523
562
|
};
|
|
524
|
-
const
|
|
563
|
+
const TRUSTED_IP_HEADER = "cf-connecting-ip";
|
|
525
564
|
function getTrustedHeaderName() {
|
|
526
565
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc") ?? {};
|
|
527
566
|
const raw = config2.OIDC_TRUSTED_IP_HEADER;
|
|
528
567
|
if (typeof raw !== "string" || !raw) return void 0;
|
|
529
568
|
const normalized = raw.trim().toLowerCase();
|
|
530
|
-
return
|
|
569
|
+
return normalized === TRUSTED_IP_HEADER ? normalized : void 0;
|
|
531
570
|
}
|
|
532
571
|
function getClientIp(ctx) {
|
|
533
572
|
const proxyTrusted = ctx.app?.proxy === true;
|
|
@@ -544,19 +583,23 @@ function getClientIp(ctx) {
|
|
|
544
583
|
}
|
|
545
584
|
return ctx.ip;
|
|
546
585
|
}
|
|
586
|
+
function toMessage(e) {
|
|
587
|
+
return e instanceof Error ? e.message : String(e);
|
|
588
|
+
}
|
|
547
589
|
const REQUIRED_CONFIG_KEYS = [
|
|
590
|
+
"OIDC_DISCOVERY_URL",
|
|
548
591
|
"OIDC_CLIENT_ID",
|
|
549
592
|
"OIDC_CLIENT_SECRET",
|
|
550
593
|
"OIDC_REDIRECT_URI",
|
|
551
594
|
"OIDC_SCOPE",
|
|
552
|
-
"OIDC_TOKEN_ENDPOINT",
|
|
553
|
-
"OIDC_USERINFO_ENDPOINT",
|
|
554
|
-
"OIDC_GRANT_TYPE",
|
|
555
595
|
"OIDC_FAMILY_NAME_FIELD",
|
|
556
596
|
"OIDC_GIVEN_NAME_FIELD",
|
|
597
|
+
// Populated at bootstrap from OIDC_DISCOVERY_URL — checked here as a runtime safety net
|
|
598
|
+
"OIDC_TOKEN_ENDPOINT",
|
|
599
|
+
"OIDC_USERINFO_ENDPOINT",
|
|
557
600
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
558
601
|
];
|
|
559
|
-
const LOGOUT_USERINFO_TIMEOUT_MS =
|
|
602
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
|
|
560
603
|
const jwksCache = /* @__PURE__ */ new Map();
|
|
561
604
|
let jwksDisabledWarned = false;
|
|
562
605
|
function getJwks(uri) {
|
|
@@ -573,9 +616,7 @@ async function verifyIdToken(idToken, config2) {
|
|
|
573
616
|
if (!jwksUri) {
|
|
574
617
|
if (!jwksDisabledWarned) {
|
|
575
618
|
jwksDisabledWarned = true;
|
|
576
|
-
strapi.log.warn(
|
|
577
|
-
"[OIDC] OIDC_JWKS_URI is not configured — ID token signature verification is disabled. Set OIDC_JWKS_URI and OIDC_ISSUER from your provider's discovery document."
|
|
578
|
-
);
|
|
619
|
+
strapi.log.warn(errorMessages.JWKS_URI_NOT_CONFIGURED);
|
|
579
620
|
}
|
|
580
621
|
return null;
|
|
581
622
|
}
|
|
@@ -588,7 +629,7 @@ async function verifyIdToken(idToken, config2) {
|
|
|
588
629
|
return payload;
|
|
589
630
|
} catch (e) {
|
|
590
631
|
if (e instanceof errors.JWTClaimValidationFailed || e instanceof errors.JWSSignatureVerificationFailed || e instanceof errors.JWTExpired || e instanceof errors.JWTInvalid || e instanceof errors.JWSInvalid) {
|
|
591
|
-
const msg =
|
|
632
|
+
const msg = toMessage(e);
|
|
592
633
|
throw new OidcError("id_token_invalid", msg, e);
|
|
593
634
|
}
|
|
594
635
|
throw e;
|
|
@@ -779,7 +820,7 @@ async function ensureUser(userService, oauthService2, email, userResponseData, c
|
|
|
779
820
|
);
|
|
780
821
|
return { user, userCreated: true, rolesUpdated: true };
|
|
781
822
|
} catch (e) {
|
|
782
|
-
const msg =
|
|
823
|
+
const msg = toMessage(e);
|
|
783
824
|
throw new OidcError("user_creation_failed", msg, e);
|
|
784
825
|
}
|
|
785
826
|
}
|
|
@@ -829,7 +870,7 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
|
|
|
829
870
|
function classifyOidcError(e, userInfo) {
|
|
830
871
|
const kind = e instanceof OidcError ? e.kind : "unknown";
|
|
831
872
|
const dispatch = OIDC_ERROR_DISPATCH[kind];
|
|
832
|
-
const msg =
|
|
873
|
+
const msg = toMessage(e);
|
|
833
874
|
let params;
|
|
834
875
|
if (kind === "id_token_parse_failed" || kind === "id_token_invalid" || kind === "unknown") {
|
|
835
876
|
params = { error: msg };
|
|
@@ -878,7 +919,7 @@ async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated
|
|
|
878
919
|
}
|
|
879
920
|
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
880
921
|
const errorInfo = classifyOidcError(e, userInfo);
|
|
881
|
-
const message =
|
|
922
|
+
const message = toMessage(e);
|
|
882
923
|
await auditLog2.log({
|
|
883
924
|
action: errorInfo.action,
|
|
884
925
|
email: userInfo?.email,
|
|
@@ -919,7 +960,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
919
960
|
client_id: config2.OIDC_CLIENT_ID,
|
|
920
961
|
client_secret: config2.OIDC_CLIENT_SECRET,
|
|
921
962
|
redirect_uri: config2.OIDC_REDIRECT_URI,
|
|
922
|
-
grant_type:
|
|
963
|
+
grant_type: "authorization_code",
|
|
923
964
|
code_verifier: codeVerifier ?? ""
|
|
924
965
|
});
|
|
925
966
|
let userInfo;
|
|
@@ -963,13 +1004,13 @@ async function oidcSignInCallback(ctx) {
|
|
|
963
1004
|
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
964
1005
|
}
|
|
965
1006
|
}
|
|
966
|
-
async function
|
|
1007
|
+
async function isProviderSessionExpired(userinfoEndpoint, accessToken) {
|
|
967
1008
|
try {
|
|
968
1009
|
const response = await fetch(userinfoEndpoint, {
|
|
969
1010
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
970
1011
|
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
971
1012
|
});
|
|
972
|
-
return response.ok;
|
|
1013
|
+
return !response.ok;
|
|
973
1014
|
} catch {
|
|
974
1015
|
return false;
|
|
975
1016
|
}
|
|
@@ -989,14 +1030,14 @@ async function logout(ctx) {
|
|
|
989
1030
|
}
|
|
990
1031
|
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: getClientIp(ctx) }) : Promise.resolve();
|
|
991
1032
|
if (logoutUrl && accessToken) {
|
|
992
|
-
const
|
|
993
|
-
if (
|
|
994
|
-
logAudit("
|
|
995
|
-
|
|
996
|
-
return ctx.redirect(logoutUrl);
|
|
1033
|
+
const expired = await isProviderSessionExpired(config2.OIDC_USERINFO_ENDPOINT, accessToken);
|
|
1034
|
+
if (expired) {
|
|
1035
|
+
await logAudit("session_expired");
|
|
1036
|
+
return ctx.redirect(loginUrl);
|
|
997
1037
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1038
|
+
logAudit("logout").catch(() => {
|
|
1039
|
+
});
|
|
1040
|
+
return ctx.redirect(logoutUrl);
|
|
1000
1041
|
}
|
|
1001
1042
|
await logAudit("logout");
|
|
1002
1043
|
ctx.redirect(logoutUrl || loginUrl);
|
|
@@ -1154,13 +1195,9 @@ async function importUsers(ctx) {
|
|
|
1154
1195
|
const whitelistService2 = getWhitelistService();
|
|
1155
1196
|
const existing = await whitelistService2.getUsers();
|
|
1156
1197
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
await whitelistService2.registerUser(email);
|
|
1161
|
-
importedCount++;
|
|
1162
|
-
}
|
|
1163
|
-
ctx.body = { importedCount };
|
|
1198
|
+
const toImport = deduped.filter((email) => !existingEmails.has(email));
|
|
1199
|
+
await Promise.all(toImport.map((email) => whitelistService2.registerUser(email)));
|
|
1200
|
+
ctx.body = { importedCount: toImport.length };
|
|
1164
1201
|
}
|
|
1165
1202
|
async function syncUsers(ctx) {
|
|
1166
1203
|
const { users: rawUsers } = ctx.request.body;
|
|
@@ -1169,16 +1206,10 @@ async function syncUsers(ctx) {
|
|
|
1169
1206
|
const currentUsers = await whitelistService2.getUsers();
|
|
1170
1207
|
const syncEmailSet = new Set(emails);
|
|
1171
1208
|
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
|
-
}
|
|
1209
|
+
await Promise.all([
|
|
1210
|
+
...currentUsers.filter((u) => !syncEmailSet.has(u.email)).map((u) => whitelistService2.removeUser(u.email)),
|
|
1211
|
+
...emails.filter((email) => !currentUsersByEmail.has(email)).map((email) => whitelistService2.registerUser(email))
|
|
1212
|
+
]);
|
|
1182
1213
|
ctx.body = {};
|
|
1183
1214
|
}
|
|
1184
1215
|
const whitelist = {
|
|
@@ -1357,7 +1388,7 @@ function errorAwareNdjsonStream(strapi2, service, filters) {
|
|
|
1357
1388
|
const gen = ndjsonRowStream(service, filters);
|
|
1358
1389
|
const readable = Readable.from(gen);
|
|
1359
1390
|
readable.on("error", (err) => {
|
|
1360
|
-
strapi2.log.error({ phase: "audit_log_export", err },
|
|
1391
|
+
strapi2.log.error({ phase: "audit_log_export", err }, errorMessages.AUDIT_LOG_EXPORT_ERROR);
|
|
1361
1392
|
});
|
|
1362
1393
|
return readable;
|
|
1363
1394
|
}
|
|
@@ -1482,6 +1513,12 @@ const routes = {
|
|
|
1482
1513
|
handler: "oidc.oidcSignInCallback",
|
|
1483
1514
|
config: { auth: false, middlewares: [rateLimitMiddleware] }
|
|
1484
1515
|
},
|
|
1516
|
+
{
|
|
1517
|
+
method: "GET",
|
|
1518
|
+
path: "/logout",
|
|
1519
|
+
handler: "oidc.logout",
|
|
1520
|
+
config: { auth: false }
|
|
1521
|
+
},
|
|
1485
1522
|
{
|
|
1486
1523
|
method: "POST",
|
|
1487
1524
|
path: "/logout",
|
|
@@ -1790,9 +1827,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1790
1827
|
}
|
|
1791
1828
|
const modelDef = strapi2.getModel("admin::user");
|
|
1792
1829
|
const sanitizedEntity = await strapiUtils.sanitize.sanitizers.defaultSanitizeOutput(
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1830
|
+
{
|
|
1831
|
+
schema: modelDef,
|
|
1832
|
+
getModel: (uid2) => strapi2.getModel(uid2)
|
|
1833
|
+
},
|
|
1796
1834
|
user
|
|
1797
1835
|
);
|
|
1798
1836
|
eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
|
package/package.json
CHANGED