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.js
CHANGED
|
@@ -12,6 +12,58 @@ const strapiUtils__default = /* @__PURE__ */ _interopDefault(strapiUtils);
|
|
|
12
12
|
const generator__default = /* @__PURE__ */ _interopDefault(generator);
|
|
13
13
|
function register$1() {
|
|
14
14
|
}
|
|
15
|
+
const errorCodes = {
|
|
16
|
+
TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
|
|
17
|
+
USERINFO_FETCH_FAILED: "USERINFO_FETCH_FAILED",
|
|
18
|
+
ID_TOKEN_PARSE_FAILED: "ID_TOKEN_PARSE_FAILED",
|
|
19
|
+
NONCE_MISMATCH: "NONCE_MISMATCH",
|
|
20
|
+
ROLE_UPDATE_FAILED: "ROLE_UPDATE_FAILED",
|
|
21
|
+
USER_CREATION_FAILED: "USER_CREATION_FAILED",
|
|
22
|
+
WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED",
|
|
23
|
+
EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED",
|
|
24
|
+
ID_TOKEN_INVALID: "ID_TOKEN_INVALID"
|
|
25
|
+
};
|
|
26
|
+
const ERROR_DETAIL_TEMPLATES = {
|
|
27
|
+
token_exchange_failed: "Token exchange failed with HTTP status {status}",
|
|
28
|
+
userinfo_fetch_failed: "UserInfo endpoint returned HTTP {status}",
|
|
29
|
+
role_update_failed: "Role update failed for user {userId}: {error}",
|
|
30
|
+
user_creation_failed: "User creation failed for {email}: {error}",
|
|
31
|
+
id_token_parse_failed: "ID token parse failed: {error}",
|
|
32
|
+
sign_in_unknown: "Unknown sign-in error: {error}",
|
|
33
|
+
invalid_email: "Invalid email address received from OIDC provider",
|
|
34
|
+
email_not_verified: "Email address has not been verified by the OIDC provider",
|
|
35
|
+
id_token_invalid: "ID token verification failed: {error}",
|
|
36
|
+
whitelist_not_present: "Email not present in whitelist",
|
|
37
|
+
session_manager_unsupported: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later."
|
|
38
|
+
};
|
|
39
|
+
function interpolate$1(template, params) {
|
|
40
|
+
if (!params) return template;
|
|
41
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
|
|
42
|
+
}
|
|
43
|
+
function getErrorDetail(key, params) {
|
|
44
|
+
const template = ERROR_DETAIL_TEMPLATES[key];
|
|
45
|
+
if (!template) return void 0;
|
|
46
|
+
return interpolate$1(template, params);
|
|
47
|
+
}
|
|
48
|
+
const errorMessages = {
|
|
49
|
+
TOKEN_EXCHANGE_FAILED: "Token exchange failed",
|
|
50
|
+
USERINFO_FETCH_FAILED: "Failed to fetch user info",
|
|
51
|
+
ID_TOKEN_PARSE_FAILED: "Failed to parse ID token",
|
|
52
|
+
NONCE_MISMATCH: "Nonce mismatch",
|
|
53
|
+
INVALID_EMAIL: "Invalid email address received from OIDC provider",
|
|
54
|
+
EMAIL_NOT_VERIFIED: "Email address has not been verified by the OIDC provider",
|
|
55
|
+
ID_TOKEN_INVALID: "ID token verification failed",
|
|
56
|
+
WHITELIST_NOT_PRESENT: "Not present in whitelist",
|
|
57
|
+
SESSION_MANAGER_UNSUPPORTED: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
58
|
+
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.",
|
|
59
|
+
ENFORCE_MIDDLEWARE_ERROR: "Error checking OIDC enforcement in middleware:",
|
|
60
|
+
ENFORCE_SYNC_ERROR: "[strapi-plugin-oidc] Failed to sync OIDC_ENFORCE to database:",
|
|
61
|
+
DEFAULT_ROLE_INIT_ERROR: "Could not initialize default OIDC role:",
|
|
62
|
+
AUDIT_LOG_CLEANUP_ERROR: "[strapi-plugin-oidc] Audit log cleanup failed:",
|
|
63
|
+
AUDIT_LOG_EXPORT_ERROR: "NDJSON export stream failed",
|
|
64
|
+
DISCOVERY_FETCH_ERROR: (url, reason) => `[strapi-plugin-oidc] Failed to fetch OIDC discovery document from ${url}: ${reason}`,
|
|
65
|
+
MISSING_CONFIG: (keys) => `Missing required config keys: ${keys}`
|
|
66
|
+
};
|
|
15
67
|
function getEnforceOIDCConfig(strapi2) {
|
|
16
68
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
17
69
|
const val = config2.OIDC_ENFORCE;
|
|
@@ -44,8 +96,43 @@ const getRoleService = () => strapi.plugin(PLUGIN_NAME).service("role");
|
|
|
44
96
|
const getWhitelistService = () => strapi.plugin(PLUGIN_NAME).service("whitelist");
|
|
45
97
|
const getAuditLogService = () => strapi.plugin(PLUGIN_NAME).service("auditLog");
|
|
46
98
|
const getAdminUserService = () => strapi.service("admin::user");
|
|
99
|
+
const DISCOVERY_TIMEOUT_MS = 5e3;
|
|
100
|
+
const FIELD_MAP = [
|
|
101
|
+
["issuer", "OIDC_ISSUER"],
|
|
102
|
+
["authorization_endpoint", "OIDC_AUTHORIZATION_ENDPOINT"],
|
|
103
|
+
["token_endpoint", "OIDC_TOKEN_ENDPOINT"],
|
|
104
|
+
["userinfo_endpoint", "OIDC_USERINFO_ENDPOINT"],
|
|
105
|
+
["end_session_endpoint", "OIDC_END_SESSION_ENDPOINT"],
|
|
106
|
+
["jwks_uri", "OIDC_JWKS_URI"]
|
|
107
|
+
];
|
|
108
|
+
async function applyDiscovery(strapi2) {
|
|
109
|
+
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
110
|
+
const discoveryUrl = config2.OIDC_DISCOVERY_URL;
|
|
111
|
+
if (!discoveryUrl) return;
|
|
112
|
+
let doc;
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch(discoveryUrl, { signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS) });
|
|
115
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
116
|
+
doc = await res.json();
|
|
117
|
+
} catch (e) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
errorMessages.DISCOVERY_FETCH_ERROR(discoveryUrl, e instanceof Error ? e.message : String(e))
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const updates = {};
|
|
123
|
+
for (const [docField, configKey] of FIELD_MAP) {
|
|
124
|
+
if (doc[docField]) {
|
|
125
|
+
updates[configKey] = doc[docField];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (Object.keys(updates).length > 0) {
|
|
129
|
+
strapi2.config.set("plugin::strapi-plugin-oidc", { ...config2, ...updates });
|
|
130
|
+
strapi2.log.info(`[strapi-plugin-oidc] Discovery applied: ${Object.keys(updates).join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
47
133
|
const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
|
|
48
134
|
async function bootstrap({ strapi: strapi2 }) {
|
|
135
|
+
await applyDiscovery(strapi2);
|
|
49
136
|
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
50
137
|
const tokenRefreshPath = `${adminUrl}/token/refresh`;
|
|
51
138
|
const enforceOidcMiddleware = async (ctx, next) => {
|
|
@@ -85,7 +172,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
85
172
|
return;
|
|
86
173
|
}
|
|
87
174
|
} catch (err) {
|
|
88
|
-
strapi2.log.error(
|
|
175
|
+
strapi2.log.error(errorMessages.ENFORCE_MIDDLEWARE_ERROR, err);
|
|
89
176
|
}
|
|
90
177
|
}
|
|
91
178
|
await next();
|
|
@@ -122,7 +209,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
122
209
|
);
|
|
123
210
|
}
|
|
124
211
|
} catch (err) {
|
|
125
|
-
strapi2.log.error(
|
|
212
|
+
strapi2.log.error(errorMessages.ENFORCE_SYNC_ERROR, err);
|
|
126
213
|
}
|
|
127
214
|
}
|
|
128
215
|
try {
|
|
@@ -136,7 +223,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
136
223
|
}
|
|
137
224
|
}
|
|
138
225
|
} catch (err) {
|
|
139
|
-
strapi2.log.warn(
|
|
226
|
+
strapi2.log.warn(errorMessages.DEFAULT_ROLE_INIT_ERROR, err.message);
|
|
140
227
|
}
|
|
141
228
|
strapi2.cron.add({
|
|
142
229
|
"strapi-plugin-oidc-audit-log-cleanup": {
|
|
@@ -145,7 +232,7 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
145
232
|
const retentionDays = getRetentionDays();
|
|
146
233
|
await getAuditLogService().cleanup(retentionDays);
|
|
147
234
|
} catch (err) {
|
|
148
|
-
strapi2.log.warn(
|
|
235
|
+
strapi2.log.warn(errorMessages.AUDIT_LOG_CLEANUP_ERROR, err.message);
|
|
149
236
|
}
|
|
150
237
|
},
|
|
151
238
|
options: { rule: "0 0 * * *" }
|
|
@@ -157,17 +244,13 @@ function destroy() {
|
|
|
157
244
|
const config = {
|
|
158
245
|
default: {
|
|
159
246
|
REMEMBER_ME: false,
|
|
247
|
+
OIDC_DISCOVERY_URL: "",
|
|
160
248
|
OIDC_REDIRECT_URI: "http://localhost:1337/strapi-plugin-oidc/oidc/callback",
|
|
161
249
|
OIDC_CLIENT_ID: "",
|
|
162
250
|
OIDC_CLIENT_SECRET: "",
|
|
163
251
|
OIDC_SCOPE: "openid profile email",
|
|
164
|
-
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
165
|
-
OIDC_TOKEN_ENDPOINT: "",
|
|
166
|
-
OIDC_USERINFO_ENDPOINT: "",
|
|
167
|
-
OIDC_GRANT_TYPE: "authorization_code",
|
|
168
252
|
OIDC_FAMILY_NAME_FIELD: "family_name",
|
|
169
253
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
170
|
-
OIDC_END_SESSION_ENDPOINT: "",
|
|
171
254
|
OIDC_SSO_BUTTON_TEXT: "Login via SSO",
|
|
172
255
|
OIDC_ENFORCE: null,
|
|
173
256
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
@@ -176,9 +259,14 @@ const config = {
|
|
|
176
259
|
OIDC_GROUP_ROLE_MAP: "{}",
|
|
177
260
|
OIDC_REQUIRE_EMAIL_VERIFIED: true,
|
|
178
261
|
OIDC_TRUSTED_IP_HEADER: "",
|
|
262
|
+
OIDC_FORCE_SECURE_COOKIES: false,
|
|
263
|
+
// Populated at bootstrap from OIDC_DISCOVERY_URL — not user-configurable directly
|
|
264
|
+
OIDC_AUTHORIZATION_ENDPOINT: "",
|
|
265
|
+
OIDC_TOKEN_ENDPOINT: "",
|
|
266
|
+
OIDC_USERINFO_ENDPOINT: "",
|
|
267
|
+
OIDC_END_SESSION_ENDPOINT: "",
|
|
179
268
|
OIDC_JWKS_URI: "",
|
|
180
|
-
OIDC_ISSUER: ""
|
|
181
|
-
OIDC_FORCE_SECURE_COOKIES: false
|
|
269
|
+
OIDC_ISSUER: ""
|
|
182
270
|
},
|
|
183
271
|
validator() {
|
|
184
272
|
}
|
|
@@ -234,8 +322,7 @@ function shouldMarkSecure(strapi2, ctx) {
|
|
|
234
322
|
if (config2.OIDC_FORCE_SECURE_COOKIES === true) return true;
|
|
235
323
|
if (ctx.request.secure) return true;
|
|
236
324
|
const proxyTrusted = ctx.app?.proxy === true;
|
|
237
|
-
if (proxyTrusted &&
|
|
238
|
-
return true;
|
|
325
|
+
if (proxyTrusted && ctx.get("x-forwarded-proto") === "https") return true;
|
|
239
326
|
return false;
|
|
240
327
|
}
|
|
241
328
|
function getExpiredCookieOptions(strapi2, ctx) {
|
|
@@ -261,52 +348,6 @@ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
261
348
|
function isValidEmail(email) {
|
|
262
349
|
return EMAIL_REGEX.test(email);
|
|
263
350
|
}
|
|
264
|
-
const errorCodes = {
|
|
265
|
-
TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
|
|
266
|
-
USERINFO_FETCH_FAILED: "USERINFO_FETCH_FAILED",
|
|
267
|
-
ID_TOKEN_PARSE_FAILED: "ID_TOKEN_PARSE_FAILED",
|
|
268
|
-
NONCE_MISMATCH: "NONCE_MISMATCH",
|
|
269
|
-
ROLE_UPDATE_FAILED: "ROLE_UPDATE_FAILED",
|
|
270
|
-
USER_CREATION_FAILED: "USER_CREATION_FAILED",
|
|
271
|
-
WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED",
|
|
272
|
-
EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED",
|
|
273
|
-
ID_TOKEN_INVALID: "ID_TOKEN_INVALID"
|
|
274
|
-
};
|
|
275
|
-
const ERROR_DETAIL_TEMPLATES = {
|
|
276
|
-
token_exchange_failed: "Token exchange failed with HTTP status {status}",
|
|
277
|
-
userinfo_fetch_failed: "UserInfo endpoint returned HTTP {status}",
|
|
278
|
-
role_update_failed: "Role update failed for user {userId}: {error}",
|
|
279
|
-
user_creation_failed: "User creation failed for {email}: {error}",
|
|
280
|
-
id_token_parse_failed: "ID token parse failed: {error}",
|
|
281
|
-
sign_in_unknown: "Unknown sign-in error: {error}",
|
|
282
|
-
invalid_email: "Invalid email address received from OIDC provider",
|
|
283
|
-
email_not_verified: "Email address has not been verified by the OIDC provider",
|
|
284
|
-
id_token_invalid: "ID token verification failed: {error}",
|
|
285
|
-
whitelist_not_present: "Email not present in whitelist",
|
|
286
|
-
session_manager_unsupported: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
287
|
-
missing_config: "Missing required config keys: {keys}"
|
|
288
|
-
};
|
|
289
|
-
function interpolate$1(template, params) {
|
|
290
|
-
if (!params) return template;
|
|
291
|
-
return template.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
|
|
292
|
-
}
|
|
293
|
-
function getErrorDetail(key, params) {
|
|
294
|
-
const template = ERROR_DETAIL_TEMPLATES[key];
|
|
295
|
-
if (!template) return void 0;
|
|
296
|
-
return interpolate$1(template, params);
|
|
297
|
-
}
|
|
298
|
-
const errorMessages = {
|
|
299
|
-
TOKEN_EXCHANGE_FAILED: "Token exchange failed",
|
|
300
|
-
USERINFO_FETCH_FAILED: "Failed to fetch user info",
|
|
301
|
-
ID_TOKEN_PARSE_FAILED: "Failed to parse ID token",
|
|
302
|
-
NONCE_MISMATCH: "Nonce mismatch",
|
|
303
|
-
INVALID_EMAIL: "Invalid email address received from OIDC provider",
|
|
304
|
-
EMAIL_NOT_VERIFIED: "Email address has not been verified by the OIDC provider",
|
|
305
|
-
ID_TOKEN_INVALID: "ID token verification failed",
|
|
306
|
-
WHITELIST_NOT_PRESENT: "Not present in whitelist",
|
|
307
|
-
SESSION_MANAGER_UNSUPPORTED: "sessionManager is not supported. Please upgrade to Strapi v5.24.1 or later.",
|
|
308
|
-
MISSING_CONFIG: (keys) => `Missing required config keys: ${keys}`
|
|
309
|
-
};
|
|
310
351
|
const en = {
|
|
311
352
|
"global.plugins.strapi-plugin-oidc": "OIDC Plugin",
|
|
312
353
|
"page.title": "Configure OIDC default role(s) and access controls.",
|
|
@@ -369,7 +410,6 @@ const en = {
|
|
|
369
410
|
"auditlog.table.ip": "IP",
|
|
370
411
|
"auditlog.table.details": "Details",
|
|
371
412
|
"auditlog.table.empty": "No audit log entries",
|
|
372
|
-
"auditlog.loading": "Loading…",
|
|
373
413
|
"auditlog.clear": "Clear Logs",
|
|
374
414
|
"auditlog.clear.title": "Clear All Logs",
|
|
375
415
|
"auditlog.clear.description": "This will permanently delete all {count, plural, one {# audit log entry} other {# audit log entries}}. This action cannot be undone.",
|
|
@@ -424,7 +464,6 @@ const locales = Object.fromEntries(
|
|
|
424
464
|
return [code ?? "", mod.default];
|
|
425
465
|
})
|
|
426
466
|
);
|
|
427
|
-
Object.keys(locales).filter(Boolean);
|
|
428
467
|
const DEFAULT_LOCALE = "en";
|
|
429
468
|
function parseAcceptLanguage(header) {
|
|
430
469
|
return header.split(",").map((part) => {
|
|
@@ -527,13 +566,13 @@ const OIDC_ERROR_DISPATCH = {
|
|
|
527
566
|
key: "sign_in_unknown"
|
|
528
567
|
}
|
|
529
568
|
};
|
|
530
|
-
const
|
|
569
|
+
const TRUSTED_IP_HEADER = "cf-connecting-ip";
|
|
531
570
|
function getTrustedHeaderName() {
|
|
532
571
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc") ?? {};
|
|
533
572
|
const raw = config2.OIDC_TRUSTED_IP_HEADER;
|
|
534
573
|
if (typeof raw !== "string" || !raw) return void 0;
|
|
535
574
|
const normalized = raw.trim().toLowerCase();
|
|
536
|
-
return
|
|
575
|
+
return normalized === TRUSTED_IP_HEADER ? normalized : void 0;
|
|
537
576
|
}
|
|
538
577
|
function getClientIp(ctx) {
|
|
539
578
|
const proxyTrusted = ctx.app?.proxy === true;
|
|
@@ -550,19 +589,23 @@ function getClientIp(ctx) {
|
|
|
550
589
|
}
|
|
551
590
|
return ctx.ip;
|
|
552
591
|
}
|
|
592
|
+
function toMessage(e) {
|
|
593
|
+
return e instanceof Error ? e.message : String(e);
|
|
594
|
+
}
|
|
553
595
|
const REQUIRED_CONFIG_KEYS = [
|
|
596
|
+
"OIDC_DISCOVERY_URL",
|
|
554
597
|
"OIDC_CLIENT_ID",
|
|
555
598
|
"OIDC_CLIENT_SECRET",
|
|
556
599
|
"OIDC_REDIRECT_URI",
|
|
557
600
|
"OIDC_SCOPE",
|
|
558
|
-
"OIDC_TOKEN_ENDPOINT",
|
|
559
|
-
"OIDC_USERINFO_ENDPOINT",
|
|
560
|
-
"OIDC_GRANT_TYPE",
|
|
561
601
|
"OIDC_FAMILY_NAME_FIELD",
|
|
562
602
|
"OIDC_GIVEN_NAME_FIELD",
|
|
603
|
+
// Populated at bootstrap from OIDC_DISCOVERY_URL — checked here as a runtime safety net
|
|
604
|
+
"OIDC_TOKEN_ENDPOINT",
|
|
605
|
+
"OIDC_USERINFO_ENDPOINT",
|
|
563
606
|
"OIDC_AUTHORIZATION_ENDPOINT"
|
|
564
607
|
];
|
|
565
|
-
const LOGOUT_USERINFO_TIMEOUT_MS =
|
|
608
|
+
const LOGOUT_USERINFO_TIMEOUT_MS = 1500;
|
|
566
609
|
const jwksCache = /* @__PURE__ */ new Map();
|
|
567
610
|
let jwksDisabledWarned = false;
|
|
568
611
|
function getJwks(uri) {
|
|
@@ -579,9 +622,7 @@ async function verifyIdToken(idToken, config2) {
|
|
|
579
622
|
if (!jwksUri) {
|
|
580
623
|
if (!jwksDisabledWarned) {
|
|
581
624
|
jwksDisabledWarned = true;
|
|
582
|
-
strapi.log.warn(
|
|
583
|
-
"[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."
|
|
584
|
-
);
|
|
625
|
+
strapi.log.warn(errorMessages.JWKS_URI_NOT_CONFIGURED);
|
|
585
626
|
}
|
|
586
627
|
return null;
|
|
587
628
|
}
|
|
@@ -594,7 +635,7 @@ async function verifyIdToken(idToken, config2) {
|
|
|
594
635
|
return payload;
|
|
595
636
|
} catch (e) {
|
|
596
637
|
if (e instanceof jose.errors.JWTClaimValidationFailed || e instanceof jose.errors.JWSSignatureVerificationFailed || e instanceof jose.errors.JWTExpired || e instanceof jose.errors.JWTInvalid || e instanceof jose.errors.JWSInvalid) {
|
|
597
|
-
const msg =
|
|
638
|
+
const msg = toMessage(e);
|
|
598
639
|
throw new OidcError("id_token_invalid", msg, e);
|
|
599
640
|
}
|
|
600
641
|
throw e;
|
|
@@ -785,7 +826,7 @@ async function ensureUser(userService, oauthService2, email, userResponseData, c
|
|
|
785
826
|
);
|
|
786
827
|
return { user, userCreated: true, rolesUpdated: true };
|
|
787
828
|
} catch (e) {
|
|
788
|
-
const msg =
|
|
829
|
+
const msg = toMessage(e);
|
|
789
830
|
throw new OidcError("user_creation_failed", msg, e);
|
|
790
831
|
}
|
|
791
832
|
}
|
|
@@ -835,7 +876,7 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
|
|
|
835
876
|
function classifyOidcError(e, userInfo) {
|
|
836
877
|
const kind = e instanceof OidcError ? e.kind : "unknown";
|
|
837
878
|
const dispatch = OIDC_ERROR_DISPATCH[kind];
|
|
838
|
-
const msg =
|
|
879
|
+
const msg = toMessage(e);
|
|
839
880
|
let params;
|
|
840
881
|
if (kind === "id_token_parse_failed" || kind === "id_token_invalid" || kind === "unknown") {
|
|
841
882
|
params = { error: msg };
|
|
@@ -884,7 +925,7 @@ async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated
|
|
|
884
925
|
}
|
|
885
926
|
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
886
927
|
const errorInfo = classifyOidcError(e, userInfo);
|
|
887
|
-
const message =
|
|
928
|
+
const message = toMessage(e);
|
|
888
929
|
await auditLog2.log({
|
|
889
930
|
action: errorInfo.action,
|
|
890
931
|
email: userInfo?.email,
|
|
@@ -925,7 +966,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
925
966
|
client_id: config2.OIDC_CLIENT_ID,
|
|
926
967
|
client_secret: config2.OIDC_CLIENT_SECRET,
|
|
927
968
|
redirect_uri: config2.OIDC_REDIRECT_URI,
|
|
928
|
-
grant_type:
|
|
969
|
+
grant_type: "authorization_code",
|
|
929
970
|
code_verifier: codeVerifier ?? ""
|
|
930
971
|
});
|
|
931
972
|
let userInfo;
|
|
@@ -969,13 +1010,13 @@ async function oidcSignInCallback(ctx) {
|
|
|
969
1010
|
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
970
1011
|
}
|
|
971
1012
|
}
|
|
972
|
-
async function
|
|
1013
|
+
async function isProviderSessionExpired(userinfoEndpoint, accessToken) {
|
|
973
1014
|
try {
|
|
974
1015
|
const response = await fetch(userinfoEndpoint, {
|
|
975
1016
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
976
1017
|
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
977
1018
|
});
|
|
978
|
-
return response.ok;
|
|
1019
|
+
return !response.ok;
|
|
979
1020
|
} catch {
|
|
980
1021
|
return false;
|
|
981
1022
|
}
|
|
@@ -995,14 +1036,14 @@ async function logout(ctx) {
|
|
|
995
1036
|
}
|
|
996
1037
|
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: getClientIp(ctx) }) : Promise.resolve();
|
|
997
1038
|
if (logoutUrl && accessToken) {
|
|
998
|
-
const
|
|
999
|
-
if (
|
|
1000
|
-
logAudit("
|
|
1001
|
-
|
|
1002
|
-
return ctx.redirect(logoutUrl);
|
|
1039
|
+
const expired = await isProviderSessionExpired(config2.OIDC_USERINFO_ENDPOINT, accessToken);
|
|
1040
|
+
if (expired) {
|
|
1041
|
+
await logAudit("session_expired");
|
|
1042
|
+
return ctx.redirect(loginUrl);
|
|
1003
1043
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1044
|
+
logAudit("logout").catch(() => {
|
|
1045
|
+
});
|
|
1046
|
+
return ctx.redirect(logoutUrl);
|
|
1006
1047
|
}
|
|
1007
1048
|
await logAudit("logout");
|
|
1008
1049
|
ctx.redirect(logoutUrl || loginUrl);
|
|
@@ -1160,13 +1201,9 @@ async function importUsers(ctx) {
|
|
|
1160
1201
|
const whitelistService2 = getWhitelistService();
|
|
1161
1202
|
const existing = await whitelistService2.getUsers();
|
|
1162
1203
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
await whitelistService2.registerUser(email);
|
|
1167
|
-
importedCount++;
|
|
1168
|
-
}
|
|
1169
|
-
ctx.body = { importedCount };
|
|
1204
|
+
const toImport = deduped.filter((email) => !existingEmails.has(email));
|
|
1205
|
+
await Promise.all(toImport.map((email) => whitelistService2.registerUser(email)));
|
|
1206
|
+
ctx.body = { importedCount: toImport.length };
|
|
1170
1207
|
}
|
|
1171
1208
|
async function syncUsers(ctx) {
|
|
1172
1209
|
const { users: rawUsers } = ctx.request.body;
|
|
@@ -1175,16 +1212,10 @@ async function syncUsers(ctx) {
|
|
|
1175
1212
|
const currentUsers = await whitelistService2.getUsers();
|
|
1176
1213
|
const syncEmailSet = new Set(emails);
|
|
1177
1214
|
const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1183
|
-
for (const email of emails) {
|
|
1184
|
-
if (!currentUsersByEmail.has(email)) {
|
|
1185
|
-
await whitelistService2.registerUser(email);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1215
|
+
await Promise.all([
|
|
1216
|
+
...currentUsers.filter((u) => !syncEmailSet.has(u.email)).map((u) => whitelistService2.removeUser(u.email)),
|
|
1217
|
+
...emails.filter((email) => !currentUsersByEmail.has(email)).map((email) => whitelistService2.registerUser(email))
|
|
1218
|
+
]);
|
|
1188
1219
|
ctx.body = {};
|
|
1189
1220
|
}
|
|
1190
1221
|
const whitelist = {
|
|
@@ -1363,7 +1394,7 @@ function errorAwareNdjsonStream(strapi2, service, filters) {
|
|
|
1363
1394
|
const gen = ndjsonRowStream(service, filters);
|
|
1364
1395
|
const readable = node_stream.Readable.from(gen);
|
|
1365
1396
|
readable.on("error", (err) => {
|
|
1366
|
-
strapi2.log.error({ phase: "audit_log_export", err },
|
|
1397
|
+
strapi2.log.error({ phase: "audit_log_export", err }, errorMessages.AUDIT_LOG_EXPORT_ERROR);
|
|
1367
1398
|
});
|
|
1368
1399
|
return readable;
|
|
1369
1400
|
}
|
|
@@ -1488,6 +1519,12 @@ const routes = {
|
|
|
1488
1519
|
handler: "oidc.oidcSignInCallback",
|
|
1489
1520
|
config: { auth: false, middlewares: [rateLimitMiddleware] }
|
|
1490
1521
|
},
|
|
1522
|
+
{
|
|
1523
|
+
method: "GET",
|
|
1524
|
+
path: "/logout",
|
|
1525
|
+
handler: "oidc.logout",
|
|
1526
|
+
config: { auth: false }
|
|
1527
|
+
},
|
|
1491
1528
|
{
|
|
1492
1529
|
method: "POST",
|
|
1493
1530
|
path: "/logout",
|
|
@@ -1796,9 +1833,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1796
1833
|
}
|
|
1797
1834
|
const modelDef = strapi2.getModel("admin::user");
|
|
1798
1835
|
const sanitizedEntity = await strapiUtils__default.default.sanitize.sanitizers.defaultSanitizeOutput(
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1836
|
+
{
|
|
1837
|
+
schema: modelDef,
|
|
1838
|
+
getModel: (uid2) => strapi2.getModel(uid2)
|
|
1839
|
+
},
|
|
1802
1840
|
user
|
|
1803
1841
|
);
|
|
1804
1842
|
eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
|