strapi-plugin-oidc 1.5.2 → 1.6.0
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 +143 -26
- package/dist/admin/{index-DWm8oOJF.js → index-DTOcUHZi.js} +342 -177
- package/dist/admin/{index-Dz3WlTpL.mjs → index-DmJadA2p.mjs} +344 -179
- package/dist/admin/{index-Dzf0bJC1.mjs → index-P9HriRms.mjs} +31 -6
- package/dist/admin/{index-CqgjBmJ5.js → index-f3cmU_tE.js} +31 -6
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +647 -175
- package/dist/server/index.mjs +648 -176
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -24,18 +24,31 @@ function resolveEnforceOIDC(strapi2, dbValue) {
|
|
|
24
24
|
if (configValue !== null) return configValue;
|
|
25
25
|
return dbValue ?? false;
|
|
26
26
|
}
|
|
27
|
+
const PLUGIN_UID = "plugin::strapi-plugin-oidc";
|
|
28
|
+
const DEFAULT_RETENTION_DAYS = 90;
|
|
29
|
+
function getPluginConfig() {
|
|
30
|
+
return strapi.config.get(PLUGIN_UID);
|
|
31
|
+
}
|
|
32
|
+
function getRetentionDays() {
|
|
33
|
+
const config2 = getPluginConfig();
|
|
34
|
+
return Number(config2.AUDIT_LOG_RETENTION_DAYS ?? DEFAULT_RETENTION_DAYS);
|
|
35
|
+
}
|
|
36
|
+
function isAuditLogEnabled() {
|
|
37
|
+
return getRetentionDays() !== 0;
|
|
38
|
+
}
|
|
27
39
|
async function bootstrap({ strapi: strapi2 }) {
|
|
40
|
+
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
41
|
+
const authRoutes = [
|
|
42
|
+
`${adminUrl}/login`,
|
|
43
|
+
`${adminUrl}/register`,
|
|
44
|
+
`${adminUrl}/register-admin`,
|
|
45
|
+
`${adminUrl}/forgot-password`,
|
|
46
|
+
`${adminUrl}/reset-password`
|
|
47
|
+
];
|
|
48
|
+
const tokenRefreshPath = `${adminUrl}/token/refresh`;
|
|
28
49
|
const enforceOidcMiddleware = async (ctx, next) => {
|
|
29
|
-
const adminUrl = strapi2.config.get("admin.url", "/admin");
|
|
30
|
-
const authRoutes = [
|
|
31
|
-
`${adminUrl}/login`,
|
|
32
|
-
`${adminUrl}/register`,
|
|
33
|
-
`${adminUrl}/register-admin`,
|
|
34
|
-
`${adminUrl}/forgot-password`,
|
|
35
|
-
`${adminUrl}/reset-password`
|
|
36
|
-
];
|
|
37
50
|
const isPostAuth = authRoutes.includes(ctx.request.path) && ctx.request.method === "POST";
|
|
38
|
-
const isTokenRefresh = ctx.request.path ===
|
|
51
|
+
const isTokenRefresh = ctx.request.path === tokenRefreshPath && ctx.request.method === "POST";
|
|
39
52
|
if (isPostAuth || isTokenRefresh) {
|
|
40
53
|
try {
|
|
41
54
|
const whitelistService2 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
|
|
@@ -116,14 +129,12 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
116
129
|
where: { oauth_type: "4" }
|
|
117
130
|
});
|
|
118
131
|
if (oidcRoleCount === 0) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
});
|
|
122
|
-
if (editorRole) {
|
|
132
|
+
const defaultRole = await strapi2.query("admin::role").findOne({ where: { code: "strapi-editor" } }) ?? await strapi2.query("admin::role").findOne({});
|
|
133
|
+
if (defaultRole) {
|
|
123
134
|
await strapi2.query("plugin::strapi-plugin-oidc.roles").create({
|
|
124
135
|
data: {
|
|
125
136
|
oauth_type: "4",
|
|
126
|
-
roles: [
|
|
137
|
+
roles: [defaultRole.id.toString()]
|
|
127
138
|
}
|
|
128
139
|
});
|
|
129
140
|
}
|
|
@@ -131,25 +142,18 @@ async function bootstrap({ strapi: strapi2 }) {
|
|
|
131
142
|
} catch (err) {
|
|
132
143
|
strapi2.log.warn("Could not initialize default OIDC role:", err.message);
|
|
133
144
|
}
|
|
134
|
-
strapi2.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (userWithRoles?.roles) {
|
|
147
|
-
const roleIds = userWithRoles.roles.map((r) => r.id.toString());
|
|
148
|
-
await query.update({
|
|
149
|
-
where: { id: whitelistEntry.id },
|
|
150
|
-
data: { roles: roleIds }
|
|
151
|
-
});
|
|
152
|
-
}
|
|
145
|
+
strapi2.cron.add({
|
|
146
|
+
"strapi-plugin-oidc-audit-log-cleanup": {
|
|
147
|
+
task: async () => {
|
|
148
|
+
try {
|
|
149
|
+
const retentionDays = getRetentionDays();
|
|
150
|
+
await strapi2.plugin("strapi-plugin-oidc").service("auditLog").cleanup(retentionDays);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
strapi2.log.warn("[strapi-plugin-oidc] Audit log cleanup failed:", err.message);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
options: { rule: "0 0 * * *" }
|
|
156
|
+
// daily at midnight
|
|
153
157
|
}
|
|
154
158
|
});
|
|
155
159
|
}
|
|
@@ -170,41 +174,58 @@ const config = {
|
|
|
170
174
|
OIDC_GIVEN_NAME_FIELD: "given_name",
|
|
171
175
|
OIDC_END_SESSION_ENDPOINT: "",
|
|
172
176
|
OIDC_SSO_BUTTON_TEXT: "Login via SSO",
|
|
173
|
-
OIDC_ENFORCE: null
|
|
177
|
+
OIDC_ENFORCE: null,
|
|
174
178
|
// null = use DB setting; true/false = override DB (useful for lockout recovery)
|
|
179
|
+
AUDIT_LOG_RETENTION_DAYS: 90,
|
|
180
|
+
OIDC_GROUP_FIELD: "groups",
|
|
181
|
+
OIDC_GROUP_ROLE_MAP: "{}"
|
|
175
182
|
},
|
|
176
183
|
validator() {
|
|
177
184
|
}
|
|
178
185
|
};
|
|
179
|
-
const info$
|
|
186
|
+
const info$3 = { "singularName": "roles", "pluralName": "oidc-roles", "collectionName": "oidc-roles", "displayName": "oidc-role", "description": "" };
|
|
187
|
+
const options$2 = { "draftAndPublish": false };
|
|
188
|
+
const pluginOptions$2 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
|
|
189
|
+
const attributes$2 = { "oauth_type": { "type": "string", "configurable": false, "required": true }, "roles": { "type": "json", "configurable": false } };
|
|
190
|
+
const schema$2 = {
|
|
191
|
+
info: info$3,
|
|
192
|
+
options: options$2,
|
|
193
|
+
pluginOptions: pluginOptions$2,
|
|
194
|
+
attributes: attributes$2
|
|
195
|
+
};
|
|
196
|
+
const roles = {
|
|
197
|
+
schema: schema$2
|
|
198
|
+
};
|
|
199
|
+
const info$2 = { "singularName": "whitelists", "pluralName": "whitelists", "collectionName": "whitelists", "displayName": "whitelist", "description": "" };
|
|
180
200
|
const options$1 = { "draftAndPublish": false };
|
|
181
201
|
const pluginOptions$1 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
|
|
182
|
-
const attributes$1 = { "
|
|
202
|
+
const attributes$1 = { "email": { "type": "string", "configurable": false, "required": true, "unique": true } };
|
|
183
203
|
const schema$1 = {
|
|
184
204
|
info: info$2,
|
|
185
205
|
options: options$1,
|
|
186
206
|
pluginOptions: pluginOptions$1,
|
|
187
207
|
attributes: attributes$1
|
|
188
208
|
};
|
|
189
|
-
const
|
|
209
|
+
const whitelists = {
|
|
190
210
|
schema: schema$1
|
|
191
211
|
};
|
|
192
|
-
const info$1 = { "singularName": "
|
|
212
|
+
const info$1 = { "singularName": "audit-log", "pluralName": "audit-logs", "collectionName": "audit_logs", "displayName": "OIDC Audit Log" };
|
|
193
213
|
const options = { "draftAndPublish": false };
|
|
194
214
|
const pluginOptions = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
|
|
195
|
-
const attributes = { "
|
|
215
|
+
const attributes = { "action": { "type": "string", "required": true }, "email": { "type": "string" }, "ip": { "type": "string" }, "detailsKey": { "type": "string" }, "detailsParams": { "type": "json" } };
|
|
196
216
|
const schema = {
|
|
197
217
|
info: info$1,
|
|
198
218
|
options,
|
|
199
219
|
pluginOptions,
|
|
200
220
|
attributes
|
|
201
221
|
};
|
|
202
|
-
const
|
|
222
|
+
const auditLog$1 = {
|
|
203
223
|
schema
|
|
204
224
|
};
|
|
205
225
|
const contentTypes = {
|
|
206
226
|
roles,
|
|
207
|
-
whitelists
|
|
227
|
+
whitelists,
|
|
228
|
+
"audit-log": auditLog$1
|
|
208
229
|
};
|
|
209
230
|
function getExpiredCookieOptions(strapi2, ctx) {
|
|
210
231
|
const isProduction = strapi2.config.get("environment") === "production";
|
|
@@ -222,7 +243,131 @@ function clearAuthCookies(strapi2, ctx) {
|
|
|
222
243
|
const options2 = getExpiredCookieOptions(strapi2, ctx);
|
|
223
244
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
224
245
|
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
246
|
+
ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
|
|
247
|
+
ctx.cookies.set("oidc_user_email", "", { ...options2, path: "/" });
|
|
225
248
|
}
|
|
249
|
+
const errorCodes = {
|
|
250
|
+
TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
|
|
251
|
+
USERINFO_FETCH_FAILED: "USERINFO_FETCH_FAILED",
|
|
252
|
+
ID_TOKEN_PARSE_FAILED: "ID_TOKEN_PARSE_FAILED",
|
|
253
|
+
NONCE_MISMATCH: "NONCE_MISMATCH",
|
|
254
|
+
ROLE_UPDATE_FAILED: "ROLE_UPDATE_FAILED",
|
|
255
|
+
USER_CREATION_FAILED: "USER_CREATION_FAILED",
|
|
256
|
+
WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED"
|
|
257
|
+
};
|
|
258
|
+
function getErrorDetail(key, params) {
|
|
259
|
+
switch (key) {
|
|
260
|
+
case "token_exchange_failed":
|
|
261
|
+
return `Token exchange failed with HTTP status ${params?.status ?? "unknown"}`;
|
|
262
|
+
case "userinfo_fetch_failed":
|
|
263
|
+
return `UserInfo endpoint returned HTTP ${params?.status ?? "unknown"}`;
|
|
264
|
+
case "role_update_failed":
|
|
265
|
+
return `Role update failed for user ${params?.userId}: ${params?.error ?? "unknown"}`;
|
|
266
|
+
case "user_creation_failed":
|
|
267
|
+
return `User creation failed for ${params?.email}: ${params?.error ?? "unknown"}`;
|
|
268
|
+
case "id_token_parse_failed":
|
|
269
|
+
return `ID token parse failed: ${params?.error ?? "unknown"}`;
|
|
270
|
+
case "sign_in_unknown":
|
|
271
|
+
return `Unknown sign-in error: ${params?.error ?? "unknown"}`;
|
|
272
|
+
default:
|
|
273
|
+
return void 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const en = {
|
|
277
|
+
"global.plugins.strapi-plugin-oidc": "OIDC Plugin",
|
|
278
|
+
"page.title": "Configure OIDC default role(s) and access controls.",
|
|
279
|
+
"roles.notes": "Select the default role(s) assigned to new users upon their first login. This setting does not affect existing users.",
|
|
280
|
+
"page.save": "Save Changes",
|
|
281
|
+
"page.save.success": "Updated settings",
|
|
282
|
+
"page.save.error": "Update failed.",
|
|
283
|
+
"page.add": "Add",
|
|
284
|
+
"page.cancel": "Cancel",
|
|
285
|
+
"page.ok": "OK",
|
|
286
|
+
"roles.title": "Default Role(s)",
|
|
287
|
+
"roles.placeholder": "Select default role(s)",
|
|
288
|
+
"whitelist.title": "Whitelist",
|
|
289
|
+
"whitelist.error.unique": "Already registered email address.",
|
|
290
|
+
"whitelist.description": "Restrict OIDC authentication to specific email addresses. Users receive roles from the default OIDC role mapping or their OIDC group.",
|
|
291
|
+
"alert.title.success": "Success",
|
|
292
|
+
"alert.title.error": "Error",
|
|
293
|
+
"alert.title.info": "Info",
|
|
294
|
+
"pagination.previous": "Go to previous page",
|
|
295
|
+
"pagination.page": "Go to page {page}",
|
|
296
|
+
"pagination.next": "Go to next page",
|
|
297
|
+
"whitelist.table.no": "No.",
|
|
298
|
+
"whitelist.table.email": "Email",
|
|
299
|
+
"whitelist.table.created": "Created At",
|
|
300
|
+
"whitelist.delete.title": "Confirmation",
|
|
301
|
+
"whitelist.delete.description": "Are you sure you want to delete:",
|
|
302
|
+
"whitelist.delete.note": "This will not delete the user account in Strapi.",
|
|
303
|
+
"whitelist.toggle.enabled": "Enabled",
|
|
304
|
+
"whitelist.toggle.disabled": "Disabled",
|
|
305
|
+
"whitelist.email.placeholder": "Email address",
|
|
306
|
+
"whitelist.roles.placeholder": "Select specific role(s)",
|
|
307
|
+
"whitelist.table.roles": "Role(s)",
|
|
308
|
+
"whitelist.table.empty": "No email addresses",
|
|
309
|
+
"whitelist.delete.label": "Delete",
|
|
310
|
+
"page.title.oidc": "OIDC",
|
|
311
|
+
"enforce.title": "Enforce OIDC Login",
|
|
312
|
+
"enforce.toggle.enabled": "Enabled",
|
|
313
|
+
"enforce.toggle.disabled": "Disabled",
|
|
314
|
+
"enforce.warning": "Make sure OIDC is setup correctly before saving changes, you won't be able to login normally.",
|
|
315
|
+
"enforce.config.info": "Enforcement is controlled by the OIDC_ENFORCE config variable and cannot be changed here.",
|
|
316
|
+
"login.settings.title": "Login Settings",
|
|
317
|
+
"login.sso": "Login via SSO",
|
|
318
|
+
"whitelist.count": "{count, plural, one {# entry} other {# entries}}",
|
|
319
|
+
"whitelist.import": "Import",
|
|
320
|
+
"whitelist.export": "Export",
|
|
321
|
+
"whitelist.delete.all.label": "Delete All",
|
|
322
|
+
"whitelist.delete.all.title": "Delete All Entries",
|
|
323
|
+
"whitelist.delete.all.description": "This will permanently remove all {count, plural, one {# entry} other {# entries}} from the whitelist. Unsaved changes will be lost.",
|
|
324
|
+
"whitelist.import.error": "Invalid file — expected a JSON array of objects with an email field.",
|
|
325
|
+
"whitelist.import.success": "Imported {count, plural, one {# new entry} other {# new entries}}.",
|
|
326
|
+
"whitelist.import.none": "No new entries — all emails are already in the whitelist.",
|
|
327
|
+
"unsaved.title": "Unsaved Changes",
|
|
328
|
+
"unsaved.description": "You have unsaved changes that will be lost if you leave. Do you want to continue?",
|
|
329
|
+
"unsaved.confirm": "Leave",
|
|
330
|
+
"unsaved.cancel": "Stay",
|
|
331
|
+
"whitelist.table.roles.default": "(Default)",
|
|
332
|
+
"auditlog.title": "Audit Logs",
|
|
333
|
+
"auditlog.export": "Download",
|
|
334
|
+
"auditlog.table.timestamp": "Timestamp",
|
|
335
|
+
"auditlog.table.action": "Action",
|
|
336
|
+
"auditlog.table.email": "Email",
|
|
337
|
+
"auditlog.table.ip": "IP",
|
|
338
|
+
"auditlog.table.details": "Details",
|
|
339
|
+
"auditlog.table.empty": "No audit log entries",
|
|
340
|
+
"auditlog.clear": "Clear Logs",
|
|
341
|
+
"auditlog.clear.title": "Clear All Logs",
|
|
342
|
+
"auditlog.clear.description": "This will permanently delete all {count, plural, one {# audit log entry} other {# audit log entries}}. This action cannot be undone.",
|
|
343
|
+
"auditlog.clear.success": "Audit logs cleared",
|
|
344
|
+
"auditlog.clear.error": "Failed to clear audit logs",
|
|
345
|
+
"auditlog.export.error": "Failed to export audit logs",
|
|
346
|
+
"auditlog.action.login_success": "User successfully authenticated via OIDC and was granted access.",
|
|
347
|
+
"auditlog.action.user_created": "A new Strapi admin account was created for this user on their first OIDC login.",
|
|
348
|
+
"auditlog.action.logout": "User logged out and their OIDC session was ended.",
|
|
349
|
+
"auditlog.action.session_expired": "The OIDC access token had expired by the time the user logged out, so the provider session could not be terminated remotely.",
|
|
350
|
+
"auditlog.action.login_failure": "An unexpected error occurred during the OIDC login flow.",
|
|
351
|
+
"auditlog.action.missing_code": "The OIDC callback was received without an authorisation code. This may indicate a misconfigured provider or a tampered request.",
|
|
352
|
+
"auditlog.action.state_mismatch": "The state parameter in the callback did not match the one stored in the session. This may indicate a CSRF attempt or an expired login session.",
|
|
353
|
+
"auditlog.action.nonce_mismatch": "The nonce in the ID token did not match the one generated at login. This may indicate a token replay attack.",
|
|
354
|
+
"auditlog.action.token_exchange_failed": "The authorisation code could not be exchanged for tokens. The OIDC provider rejected the request.",
|
|
355
|
+
"auditlog.action.whitelist_rejected": "The user's email address is not on the whitelist. Access was denied.",
|
|
356
|
+
"user.missing_code": "Authorisation code was not received from the OIDC provider.",
|
|
357
|
+
"user.invalid_state": "State parameter mismatch. Please restart the login flow.",
|
|
358
|
+
"user.signInError": "Authentication failed. Please try again."
|
|
359
|
+
};
|
|
360
|
+
const userFacingMessages = {
|
|
361
|
+
get missing_code() {
|
|
362
|
+
return en["user.missing_code"];
|
|
363
|
+
},
|
|
364
|
+
get invalid_state() {
|
|
365
|
+
return en["user.invalid_state"];
|
|
366
|
+
},
|
|
367
|
+
get signInError() {
|
|
368
|
+
return en["user.signInError"];
|
|
369
|
+
}
|
|
370
|
+
};
|
|
226
371
|
const REQUIRED_CONFIG_KEYS = [
|
|
227
372
|
"OIDC_CLIENT_ID",
|
|
228
373
|
"OIDC_CLIENT_SECRET",
|
|
@@ -260,15 +405,16 @@ async function oidcSignIn(ctx) {
|
|
|
260
405
|
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
261
406
|
ctx.cookies.set("oidc_state", state, cookieOptions);
|
|
262
407
|
ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
|
|
263
|
-
const params = new URLSearchParams(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
408
|
+
const params = new URLSearchParams({
|
|
409
|
+
response_type: "code",
|
|
410
|
+
client_id: OIDC_CLIENT_ID,
|
|
411
|
+
redirect_uri: OIDC_REDIRECT_URI,
|
|
412
|
+
scope: OIDC_SCOPE,
|
|
413
|
+
code_challenge: codeChallenge,
|
|
414
|
+
code_challenge_method: "S256",
|
|
415
|
+
state,
|
|
416
|
+
nonce
|
|
417
|
+
});
|
|
272
418
|
const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
|
|
273
419
|
ctx.set("Location", authorizationUrl);
|
|
274
420
|
return ctx.send({}, 302);
|
|
@@ -303,16 +449,38 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
303
449
|
if (!userResponse.ok) {
|
|
304
450
|
throw new Error("Failed to fetch user info");
|
|
305
451
|
}
|
|
306
|
-
|
|
452
|
+
const userInfo = await userResponse.json();
|
|
453
|
+
return { userInfo, accessToken: tokenData.access_token };
|
|
307
454
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
455
|
+
function resolveRolesFromGroups(userInfo, config2, availableRoles) {
|
|
456
|
+
const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
|
|
457
|
+
if (!Array.isArray(rawGroups) || rawGroups.length === 0) return [];
|
|
458
|
+
const groups = rawGroups.filter((g) => typeof g === "string");
|
|
459
|
+
const raw = config2.OIDC_GROUP_ROLE_MAP;
|
|
460
|
+
let groupRoleMap;
|
|
461
|
+
try {
|
|
462
|
+
groupRoleMap = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
463
|
+
} catch {
|
|
464
|
+
return [];
|
|
315
465
|
}
|
|
466
|
+
const roleIdSet = /* @__PURE__ */ new Set();
|
|
467
|
+
for (const group of groups) {
|
|
468
|
+
const roleNames = groupRoleMap[group];
|
|
469
|
+
if (!roleNames) continue;
|
|
470
|
+
for (const name of roleNames) {
|
|
471
|
+
const match = availableRoles.find((r) => r.name === name);
|
|
472
|
+
if (match) roleIdSet.add(String(match.id));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return [...roleIdSet];
|
|
476
|
+
}
|
|
477
|
+
async function resolveRoles(userInfo, config2, roleService2, availableRoles) {
|
|
478
|
+
const groupRoles = resolveRolesFromGroups(userInfo, config2, availableRoles);
|
|
479
|
+
if (groupRoles.length > 0) return groupRoles;
|
|
480
|
+
const oidcRoles = await roleService2.oidcRoles();
|
|
481
|
+
return oidcRoles?.roles || [];
|
|
482
|
+
}
|
|
483
|
+
async function registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2) {
|
|
316
484
|
const defaultLocale = oauthService2.localeFindByHeader(
|
|
317
485
|
ctx.request.headers
|
|
318
486
|
);
|
|
@@ -327,21 +495,106 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
|
|
|
327
495
|
return activateUser;
|
|
328
496
|
}
|
|
329
497
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
);
|
|
498
|
+
const rawEmail = String(userResponseData.email ?? "");
|
|
499
|
+
const email = rawEmail.toLowerCase();
|
|
500
|
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
501
|
+
throw new Error("Invalid email address received from OIDC provider");
|
|
502
|
+
}
|
|
503
|
+
await whitelistService2.checkWhitelistForEmail(email);
|
|
504
|
+
const allRoles = await strapi.db.query("admin::role").findMany();
|
|
505
|
+
const roles2 = await resolveRoles(userResponseData, config2, roleService2, allRoles);
|
|
506
|
+
const resolvedRoleNames = allRoles.filter((r) => roles2.includes(String(r.id))).map((r) => r.name);
|
|
507
|
+
let userCreated = false;
|
|
508
|
+
let rolesUpdated = false;
|
|
509
|
+
let activateUser = await userService.findOneByEmail(email);
|
|
510
|
+
if (!activateUser) {
|
|
511
|
+
activateUser = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
|
|
512
|
+
userCreated = true;
|
|
513
|
+
} else if (roles2.length > 0) {
|
|
514
|
+
const currentRoleIds = new Set((activateUser.roles ?? []).map((r) => String(r.id)));
|
|
515
|
+
const newRoleIds = new Set(roles2);
|
|
516
|
+
const rolesChanged = currentRoleIds.size !== newRoleIds.size || [...newRoleIds].some((id) => !currentRoleIds.has(id));
|
|
517
|
+
if (rolesChanged) {
|
|
518
|
+
try {
|
|
519
|
+
strapi.log.info(
|
|
520
|
+
`[OIDC] Roles updated for user ${activateUser.id}: [${[...currentRoleIds].join(",")}] -> [${roles2.join(",")}]`
|
|
521
|
+
);
|
|
522
|
+
await strapi.db.query("admin::user").update({
|
|
523
|
+
where: { id: activateUser.id },
|
|
524
|
+
data: { roles: roles2 }
|
|
525
|
+
});
|
|
526
|
+
rolesUpdated = true;
|
|
527
|
+
} catch (updateErr) {
|
|
528
|
+
strapi.log.error({
|
|
529
|
+
code: errorCodes.ROLE_UPDATE_FAILED,
|
|
530
|
+
userId: activateUser.id,
|
|
531
|
+
detail: getErrorDetail("role_update_failed", {
|
|
532
|
+
userId: activateUser.id,
|
|
533
|
+
error: updateErr.message
|
|
534
|
+
})
|
|
535
|
+
});
|
|
536
|
+
throw updateErr;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
342
540
|
const jwtToken = await oauthService2.generateToken(activateUser, ctx);
|
|
343
541
|
oauthService2.triggerSignInSuccess(activateUser);
|
|
344
|
-
return { activateUser, jwtToken };
|
|
542
|
+
return { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
|
|
543
|
+
}
|
|
544
|
+
function classifyOidcError(msg, userInfo) {
|
|
545
|
+
const errorMap = [
|
|
546
|
+
{
|
|
547
|
+
test: (m) => m.includes("whitelist"),
|
|
548
|
+
result: {
|
|
549
|
+
action: "whitelist_rejected",
|
|
550
|
+
code: errorCodes.WHITELIST_CHECK_FAILED,
|
|
551
|
+
key: "whitelist_rejected"
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
test: (m) => m === "Nonce mismatch",
|
|
556
|
+
result: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH }
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
test: (m) => m === "Token exchange failed",
|
|
560
|
+
result: { action: "token_exchange_failed", code: errorCodes.TOKEN_EXCHANGE_FAILED }
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
test: (m) => m === "Failed to fetch user info",
|
|
564
|
+
result: {
|
|
565
|
+
action: "login_failure",
|
|
566
|
+
code: errorCodes.USERINFO_FETCH_FAILED,
|
|
567
|
+
key: "userinfo_fetch_failed"
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
test: (m) => m === "Failed to parse ID token",
|
|
572
|
+
result: {
|
|
573
|
+
action: "login_failure",
|
|
574
|
+
code: errorCodes.ID_TOKEN_PARSE_FAILED,
|
|
575
|
+
key: "id_token_parse_failed",
|
|
576
|
+
params: { error: msg }
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
test: (m) => m === "User creation failed" || m.includes("createUser"),
|
|
581
|
+
result: {
|
|
582
|
+
action: "login_failure",
|
|
583
|
+
code: errorCodes.USER_CREATION_FAILED,
|
|
584
|
+
key: "user_creation_failed",
|
|
585
|
+
params: userInfo?.email ? { email: userInfo.email, error: msg } : void 0
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
];
|
|
589
|
+
for (const { test, result } of errorMap) {
|
|
590
|
+
if (test(msg)) return result;
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
action: "login_failure",
|
|
594
|
+
code: errorCodes.TOKEN_EXCHANGE_FAILED,
|
|
595
|
+
key: "sign_in_unknown",
|
|
596
|
+
params: { error: msg || "unknown" }
|
|
597
|
+
};
|
|
345
598
|
}
|
|
346
599
|
async function oidcSignInCallback(ctx) {
|
|
347
600
|
const config2 = configValidation();
|
|
@@ -349,8 +602,10 @@ async function oidcSignInCallback(ctx) {
|
|
|
349
602
|
const oauthService2 = strapi.plugin("strapi-plugin-oidc").service("oauth");
|
|
350
603
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
351
604
|
const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
|
|
605
|
+
const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
|
|
352
606
|
if (!ctx.query.code) {
|
|
353
|
-
|
|
607
|
+
await auditLog2.log({ action: "missing_code", ip: ctx.ip });
|
|
608
|
+
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
|
|
354
609
|
}
|
|
355
610
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
356
611
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
@@ -359,53 +614,129 @@ async function oidcSignInCallback(ctx) {
|
|
|
359
614
|
ctx.cookies.set("oidc_code_verifier", null);
|
|
360
615
|
ctx.cookies.set("oidc_nonce", null);
|
|
361
616
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
362
|
-
|
|
617
|
+
await auditLog2.log({ action: "state_mismatch", ip: ctx.ip });
|
|
618
|
+
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.invalid_state));
|
|
363
619
|
}
|
|
364
|
-
const params = new URLSearchParams(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
620
|
+
const params = new URLSearchParams({
|
|
621
|
+
code: ctx.query.code,
|
|
622
|
+
client_id: config2.OIDC_CLIENT_ID,
|
|
623
|
+
client_secret: config2.OIDC_CLIENT_SECRET,
|
|
624
|
+
redirect_uri: config2.OIDC_REDIRECT_URI,
|
|
625
|
+
grant_type: config2.OIDC_GRANT_TYPE,
|
|
626
|
+
code_verifier: codeVerifier ?? ""
|
|
627
|
+
});
|
|
628
|
+
let userInfo;
|
|
371
629
|
try {
|
|
372
|
-
const
|
|
373
|
-
|
|
630
|
+
const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
631
|
+
userInfo = exchangeResult.userInfo;
|
|
632
|
+
const accessToken = exchangeResult.accessToken;
|
|
633
|
+
const isProduction = strapi.config.get("environment") === "production";
|
|
634
|
+
ctx.cookies.set("oidc_access_token", accessToken, {
|
|
635
|
+
httpOnly: true,
|
|
636
|
+
maxAge: 3e5,
|
|
637
|
+
// 5 minutes — matches typical provider access token lifetime
|
|
638
|
+
secure: isProduction && ctx.request.secure,
|
|
639
|
+
sameSite: "lax"
|
|
640
|
+
});
|
|
641
|
+
const { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames } = await handleUserAuthentication(
|
|
374
642
|
userService,
|
|
375
643
|
oauthService2,
|
|
376
644
|
roleService2,
|
|
377
645
|
whitelistService2,
|
|
378
|
-
|
|
646
|
+
userInfo,
|
|
379
647
|
config2,
|
|
380
648
|
ctx
|
|
381
649
|
);
|
|
650
|
+
const identityCookieOptions = {
|
|
651
|
+
httpOnly: true,
|
|
652
|
+
path: "/",
|
|
653
|
+
secure: isProduction && ctx.request.secure,
|
|
654
|
+
sameSite: "lax"
|
|
655
|
+
};
|
|
656
|
+
ctx.cookies.set("oidc_user_email", activateUser.email, identityCookieOptions);
|
|
657
|
+
if (userCreated) {
|
|
658
|
+
await auditLog2.log({
|
|
659
|
+
action: "user_created",
|
|
660
|
+
email: activateUser.email,
|
|
661
|
+
ip: ctx.ip,
|
|
662
|
+
detailsKey: "user_created",
|
|
663
|
+
detailsParams: { roles: resolvedRoleNames.join(", ") }
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
await auditLog2.log({
|
|
667
|
+
action: "login_success",
|
|
668
|
+
email: activateUser.email,
|
|
669
|
+
ip: ctx.ip,
|
|
670
|
+
detailsKey: rolesUpdated ? "roles_updated" : void 0,
|
|
671
|
+
detailsParams: rolesUpdated ? { roles: resolvedRoleNames.join(", ") } : void 0
|
|
672
|
+
});
|
|
382
673
|
const nonce = node_crypto.randomUUID();
|
|
383
674
|
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
384
675
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
385
676
|
ctx.send(html);
|
|
386
677
|
} catch (e) {
|
|
387
|
-
|
|
388
|
-
|
|
678
|
+
const msg = e.message ?? "";
|
|
679
|
+
const errorInfo = classifyOidcError(msg, userInfo);
|
|
680
|
+
await auditLog2.log({
|
|
681
|
+
action: errorInfo.action,
|
|
682
|
+
email: userInfo?.email,
|
|
683
|
+
ip: ctx.ip,
|
|
684
|
+
detailsKey: errorInfo.action,
|
|
685
|
+
detailsParams: errorInfo.action === "login_failure" ? { message: msg } : void 0
|
|
686
|
+
});
|
|
687
|
+
strapi.log.error({
|
|
688
|
+
code: errorInfo.code,
|
|
689
|
+
phase: "oidc_callback",
|
|
690
|
+
message: msg || "Unknown sign-in error",
|
|
691
|
+
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
692
|
+
email: userInfo?.email
|
|
693
|
+
});
|
|
694
|
+
ctx.send(oauthService2.renderSignUpError(userFacingMessages.signInError));
|
|
389
695
|
}
|
|
390
696
|
}
|
|
391
697
|
async function logout(ctx) {
|
|
392
698
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
699
|
+
const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
|
|
393
700
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
701
|
+
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
394
702
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
703
|
+
const accessToken = ctx.cookies.get("oidc_access_token");
|
|
704
|
+
const userEmail = ctx.cookies.get("oidc_user_email") ?? void 0;
|
|
395
705
|
clearAuthCookies(strapi, ctx);
|
|
706
|
+
if (logoutUrl && isOidcSession && accessToken) {
|
|
707
|
+
try {
|
|
708
|
+
const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
|
|
709
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
710
|
+
});
|
|
711
|
+
if (response.ok) {
|
|
712
|
+
if (userEmail)
|
|
713
|
+
auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip }).catch(() => {
|
|
714
|
+
});
|
|
715
|
+
return ctx.redirect(logoutUrl);
|
|
716
|
+
}
|
|
717
|
+
if (userEmail)
|
|
718
|
+
await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
|
|
719
|
+
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
720
|
+
} catch {
|
|
721
|
+
if (userEmail)
|
|
722
|
+
await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
|
|
723
|
+
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (isOidcSession && userEmail) {
|
|
727
|
+
await auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip });
|
|
728
|
+
}
|
|
396
729
|
if (logoutUrl && isOidcSession) {
|
|
397
|
-
ctx.redirect(logoutUrl);
|
|
398
|
-
} else {
|
|
399
|
-
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
400
|
-
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
730
|
+
return ctx.redirect(logoutUrl);
|
|
401
731
|
}
|
|
732
|
+
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
402
733
|
}
|
|
403
734
|
const oidc = {
|
|
404
735
|
oidcSignIn,
|
|
405
736
|
oidcSignInCallback,
|
|
406
737
|
logout
|
|
407
738
|
};
|
|
408
|
-
async function find(ctx) {
|
|
739
|
+
async function find$1(ctx) {
|
|
409
740
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
410
741
|
const roles2 = await roleService2.find();
|
|
411
742
|
const oidcConstants = roleService2.getOidcRoles();
|
|
@@ -424,14 +755,23 @@ async function update(ctx) {
|
|
|
424
755
|
await roleService2.update(roles2);
|
|
425
756
|
ctx.send({}, 204);
|
|
426
757
|
} catch (e) {
|
|
427
|
-
|
|
758
|
+
strapi.log.error(e);
|
|
428
759
|
ctx.send({}, 400);
|
|
429
760
|
}
|
|
430
761
|
}
|
|
431
762
|
const role = {
|
|
432
|
-
find,
|
|
763
|
+
find: find$1,
|
|
433
764
|
update
|
|
434
765
|
};
|
|
766
|
+
function formatDatetimeForFilename(date) {
|
|
767
|
+
const year = date.getFullYear();
|
|
768
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
769
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
770
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
771
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
772
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
773
|
+
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
|
|
774
|
+
}
|
|
435
775
|
function getWhitelistService() {
|
|
436
776
|
return strapi.plugin("strapi-plugin-oidc").service("whitelist");
|
|
437
777
|
}
|
|
@@ -443,7 +783,8 @@ async function info(ctx) {
|
|
|
443
783
|
useWhitelist: settings.useWhitelist,
|
|
444
784
|
enforceOIDC: resolveEnforceOIDC(strapi, settings.enforceOIDC),
|
|
445
785
|
enforceOIDCConfig: getEnforceOIDCConfig(strapi),
|
|
446
|
-
whitelistUsers
|
|
786
|
+
whitelistUsers,
|
|
787
|
+
auditLogEnabled: isAuditLogEnabled()
|
|
447
788
|
};
|
|
448
789
|
}
|
|
449
790
|
async function updateSettings(ctx) {
|
|
@@ -469,32 +810,25 @@ async function publicSettings(ctx) {
|
|
|
469
810
|
};
|
|
470
811
|
}
|
|
471
812
|
async function register(ctx) {
|
|
472
|
-
const { email
|
|
813
|
+
const { email } = ctx.request.body;
|
|
473
814
|
if (!email) {
|
|
474
815
|
ctx.body = { message: "Please enter a valid email address" };
|
|
475
816
|
return;
|
|
476
817
|
}
|
|
477
818
|
const rawEmails = Array.isArray(email) ? email : email.split(",");
|
|
478
819
|
const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
|
|
479
|
-
const existingUsers = await strapi.query("admin::user").findMany({
|
|
480
|
-
where: { email: { $in: emailList } },
|
|
481
|
-
populate: ["roles"]
|
|
482
|
-
});
|
|
483
|
-
const existingUsersByEmail = new Map(existingUsers.map((u) => [u.email, u]));
|
|
484
820
|
const whitelistService2 = getWhitelistService();
|
|
485
821
|
let matchedExistingUsersCount = 0;
|
|
486
822
|
for (const singleEmail of emailList) {
|
|
487
|
-
const existingUser =
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
matchedExistingUsersCount++;
|
|
492
|
-
}
|
|
823
|
+
const existingUser = await strapi.query("admin::user").findOne({
|
|
824
|
+
where: { email: singleEmail }
|
|
825
|
+
});
|
|
826
|
+
if (existingUser) matchedExistingUsersCount++;
|
|
493
827
|
const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({
|
|
494
828
|
where: { email: singleEmail }
|
|
495
829
|
});
|
|
496
830
|
if (!alreadyWhitelisted) {
|
|
497
|
-
await whitelistService2.registerUser(singleEmail
|
|
831
|
+
await whitelistService2.registerUser(singleEmail);
|
|
498
832
|
}
|
|
499
833
|
}
|
|
500
834
|
ctx.body = { matchedExistingUsersCount };
|
|
@@ -509,81 +843,52 @@ async function deleteAll(ctx) {
|
|
|
509
843
|
await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
|
|
510
844
|
ctx.body = {};
|
|
511
845
|
}
|
|
846
|
+
async function exportWhitelist(ctx) {
|
|
847
|
+
const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
|
|
848
|
+
ctx.set("Content-Type", "application/json");
|
|
849
|
+
ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-whitelist-${datetime}.json"`);
|
|
850
|
+
const whitelistService2 = getWhitelistService();
|
|
851
|
+
const users = await whitelistService2.getUsers();
|
|
852
|
+
ctx.body = users.map((u) => ({ email: u.email }));
|
|
853
|
+
}
|
|
512
854
|
async function importUsers(ctx) {
|
|
513
855
|
const { users } = ctx.request.body;
|
|
514
856
|
if (!Array.isArray(users)) {
|
|
515
857
|
ctx.status = 400;
|
|
516
|
-
ctx.body = { error: "Expected { users: [{email
|
|
858
|
+
ctx.body = { error: "Expected { users: [{email}] }" };
|
|
517
859
|
return;
|
|
518
860
|
}
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
const resolveRole = (nameOrId) => roleNameToId.get(nameOrId) ?? nameOrId;
|
|
522
|
-
const normalized = users.filter((u) => u?.email).map((u) => ({
|
|
523
|
-
email: String(u.email).trim().toLowerCase(),
|
|
524
|
-
roles: (Array.isArray(u.roles) ? u.roles : []).map(resolveRole)
|
|
525
|
-
}));
|
|
526
|
-
const seen = /* @__PURE__ */ new Set();
|
|
527
|
-
const deduped = normalized.filter((u) => {
|
|
528
|
-
if (seen.has(u.email)) return false;
|
|
529
|
-
seen.add(u.email);
|
|
530
|
-
return true;
|
|
531
|
-
});
|
|
532
|
-
const strapiUsers = await strapi.query("admin::user").findMany({
|
|
533
|
-
where: { email: { $in: deduped.map((u) => u.email) } },
|
|
534
|
-
populate: ["roles"]
|
|
535
|
-
});
|
|
536
|
-
const strapiUserMap = new Map(strapiUsers.map((u) => [u.email, u]));
|
|
861
|
+
const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter((email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email));
|
|
862
|
+
const deduped = [...new Set(normalized)];
|
|
537
863
|
const whitelistService2 = getWhitelistService();
|
|
538
864
|
const existing = await whitelistService2.getUsers();
|
|
539
865
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
540
866
|
let importedCount = 0;
|
|
541
|
-
for (const
|
|
542
|
-
if (existingEmails.has(
|
|
543
|
-
|
|
544
|
-
const finalRoles = strapiUser?.roles?.length ? strapiUser.roles.map((r) => String(r.id)) : user.roles;
|
|
545
|
-
await whitelistService2.registerUser(user.email, finalRoles);
|
|
867
|
+
for (const email of deduped) {
|
|
868
|
+
if (existingEmails.has(email)) continue;
|
|
869
|
+
await whitelistService2.registerUser(email);
|
|
546
870
|
importedCount++;
|
|
547
871
|
}
|
|
548
872
|
ctx.body = { importedCount };
|
|
549
873
|
}
|
|
550
874
|
async function syncUsers(ctx) {
|
|
551
875
|
const { users: rawUsers } = ctx.request.body;
|
|
552
|
-
const
|
|
876
|
+
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter((e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e));
|
|
553
877
|
const whitelistService2 = getWhitelistService();
|
|
554
878
|
const currentUsers = await whitelistService2.getUsers();
|
|
555
|
-
|
|
556
|
-
const emailsToSync = users.map((u) => u.email);
|
|
557
|
-
const existingStrapiUsers = await strapi.query("admin::user").findMany({
|
|
558
|
-
where: { email: { $in: emailsToSync } },
|
|
559
|
-
populate: ["roles"]
|
|
560
|
-
});
|
|
561
|
-
const syncEmailSet = new Set(emailsToSync);
|
|
879
|
+
const syncEmailSet = new Set(emails);
|
|
562
880
|
const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
|
|
563
|
-
const strapiUsersByEmail = new Map(existingStrapiUsers.map((u) => [u.email, u]));
|
|
564
881
|
for (const currUser of currentUsers) {
|
|
565
882
|
if (!syncEmailSet.has(currUser.email)) {
|
|
566
883
|
await whitelistService2.removeUser(currUser.id);
|
|
567
884
|
}
|
|
568
885
|
}
|
|
569
|
-
for (const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (!currUser) {
|
|
573
|
-
const existingStrapiUser = strapiUsersByEmail.get(user.email);
|
|
574
|
-
if (existingStrapiUser?.roles) {
|
|
575
|
-
finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
|
|
576
|
-
matchedExistingUsersCount++;
|
|
577
|
-
}
|
|
578
|
-
await whitelistService2.registerUser(user.email, finalRoles);
|
|
579
|
-
} else {
|
|
580
|
-
await strapi.query("plugin::strapi-plugin-oidc.whitelists").update({
|
|
581
|
-
where: { id: currUser.id },
|
|
582
|
-
data: { roles: finalRoles }
|
|
583
|
-
});
|
|
886
|
+
for (const email of emails) {
|
|
887
|
+
if (!currentUsersByEmail.has(email)) {
|
|
888
|
+
await whitelistService2.registerUser(email);
|
|
584
889
|
}
|
|
585
890
|
}
|
|
586
|
-
ctx.body = { matchedExistingUsersCount };
|
|
891
|
+
ctx.body = { matchedExistingUsersCount: 0 };
|
|
587
892
|
}
|
|
588
893
|
const whitelist = {
|
|
589
894
|
info,
|
|
@@ -593,39 +898,99 @@ const whitelist = {
|
|
|
593
898
|
removeEmail,
|
|
594
899
|
deleteAll,
|
|
595
900
|
syncUsers,
|
|
596
|
-
importUsers
|
|
901
|
+
importUsers,
|
|
902
|
+
exportWhitelist
|
|
903
|
+
};
|
|
904
|
+
function getAuditLogService() {
|
|
905
|
+
return strapi.plugin("strapi-plugin-oidc").service("auditLog");
|
|
906
|
+
}
|
|
907
|
+
async function find(ctx) {
|
|
908
|
+
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
909
|
+
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
|
|
910
|
+
ctx.body = await getAuditLogService().find({ page, pageSize });
|
|
911
|
+
}
|
|
912
|
+
async function exportLogs(ctx) {
|
|
913
|
+
const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
|
|
914
|
+
ctx.set("Content-Type", "application/json");
|
|
915
|
+
ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-audit-log-${datetime}.json"`);
|
|
916
|
+
const service = getAuditLogService();
|
|
917
|
+
const PAGE_SIZE = 1e3;
|
|
918
|
+
const allRows = [];
|
|
919
|
+
let page = 1;
|
|
920
|
+
while (true) {
|
|
921
|
+
const { results } = await service.find({ page, pageSize: PAGE_SIZE });
|
|
922
|
+
for (const row of results) {
|
|
923
|
+
allRows.push({
|
|
924
|
+
id: row.id,
|
|
925
|
+
createdAt: row.createdAt,
|
|
926
|
+
action: row.action,
|
|
927
|
+
email: row.email ?? null,
|
|
928
|
+
ip: row.ip ?? null,
|
|
929
|
+
details: row.details
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
if (results.length < PAGE_SIZE) break;
|
|
933
|
+
page++;
|
|
934
|
+
}
|
|
935
|
+
ctx.body = allRows.map((row) => ({
|
|
936
|
+
datetime: row.createdAt,
|
|
937
|
+
action: row.action,
|
|
938
|
+
email: row.email,
|
|
939
|
+
ip: row.ip,
|
|
940
|
+
details: row.details
|
|
941
|
+
}));
|
|
942
|
+
}
|
|
943
|
+
async function clearAll(ctx) {
|
|
944
|
+
await getAuditLogService().clearAll();
|
|
945
|
+
ctx.status = 204;
|
|
946
|
+
}
|
|
947
|
+
const auditLog = {
|
|
948
|
+
find,
|
|
949
|
+
export: exportLogs,
|
|
950
|
+
clearAll
|
|
597
951
|
};
|
|
598
952
|
const controllers = {
|
|
599
953
|
oidc,
|
|
600
954
|
role,
|
|
601
|
-
whitelist
|
|
955
|
+
whitelist,
|
|
956
|
+
auditLog
|
|
602
957
|
};
|
|
603
958
|
const rateLimitMap = /* @__PURE__ */ new Map();
|
|
604
959
|
const RATE_LIMIT_WINDOW = 6e4;
|
|
605
|
-
const MAX_REQUESTS =
|
|
606
|
-
|
|
960
|
+
const MAX_REQUESTS = 1e3;
|
|
961
|
+
function getRateLimitKey(ctx) {
|
|
607
962
|
const ip = ctx.request.ip;
|
|
963
|
+
const ua = ctx.request.header["user-agent"] ?? "";
|
|
964
|
+
const uaHash = node_crypto.createHash("sha256").update(ua).digest("hex").slice(0, 16);
|
|
965
|
+
return `${ip}:${uaHash}`;
|
|
966
|
+
}
|
|
967
|
+
function rateLimitMiddleware(ctx, next) {
|
|
968
|
+
const key = getRateLimitKey(ctx);
|
|
608
969
|
const now = Date.now();
|
|
609
970
|
const windowStart = now - RATE_LIMIT_WINDOW;
|
|
610
|
-
const requestStamps = (rateLimitMap.get(
|
|
971
|
+
const requestStamps = (rateLimitMap.get(key) || []).filter(
|
|
972
|
+
(timestamp) => timestamp > windowStart
|
|
973
|
+
);
|
|
611
974
|
if (requestStamps.length >= MAX_REQUESTS) {
|
|
612
975
|
ctx.status = 429;
|
|
613
976
|
ctx.body = "Too Many Requests";
|
|
614
977
|
return;
|
|
615
978
|
}
|
|
616
979
|
requestStamps.push(now);
|
|
617
|
-
rateLimitMap.set(
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
980
|
+
rateLimitMap.set(key, requestStamps);
|
|
981
|
+
return next();
|
|
982
|
+
}
|
|
983
|
+
function adminPolicies(action) {
|
|
984
|
+
return {
|
|
985
|
+
policies: [
|
|
986
|
+
"admin::isAuthenticatedAdmin",
|
|
987
|
+
{
|
|
988
|
+
name: "admin::hasPermissions",
|
|
989
|
+
config: { actions: [`plugin::strapi-plugin-oidc.${action}`] }
|
|
990
|
+
}
|
|
991
|
+
]
|
|
992
|
+
};
|
|
993
|
+
}
|
|
629
994
|
const routes = {
|
|
630
995
|
admin: {
|
|
631
996
|
type: "admin",
|
|
@@ -707,6 +1072,30 @@ const routes = {
|
|
|
707
1072
|
path: "/whitelist",
|
|
708
1073
|
handler: "whitelist.deleteAll",
|
|
709
1074
|
config: adminPolicies("update")
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
method: "GET",
|
|
1078
|
+
path: "/whitelist/export",
|
|
1079
|
+
handler: "whitelist.exportWhitelist",
|
|
1080
|
+
config: adminPolicies("read")
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
method: "GET",
|
|
1084
|
+
path: "/audit-logs",
|
|
1085
|
+
handler: "auditLog.find",
|
|
1086
|
+
config: { policies: ["admin::isAuthenticatedAdmin"] }
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
method: "GET",
|
|
1090
|
+
path: "/audit-logs/export",
|
|
1091
|
+
handler: "auditLog.export",
|
|
1092
|
+
config: { policies: ["admin::isAuthenticatedAdmin"] }
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
method: "DELETE",
|
|
1096
|
+
path: "/audit-logs",
|
|
1097
|
+
handler: "auditLog.clearAll",
|
|
1098
|
+
config: { policies: ["admin::isAuthenticatedAdmin"] }
|
|
710
1099
|
}
|
|
711
1100
|
]
|
|
712
1101
|
},
|
|
@@ -740,6 +1129,26 @@ const routes = {
|
|
|
740
1129
|
method: "DELETE",
|
|
741
1130
|
path: "/whitelist",
|
|
742
1131
|
handler: "whitelist.deleteAll"
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
method: "GET",
|
|
1135
|
+
path: "/whitelist/export",
|
|
1136
|
+
handler: "whitelist.exportWhitelist"
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
method: "GET",
|
|
1140
|
+
path: "/audit-logs",
|
|
1141
|
+
handler: "auditLog.find"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
method: "GET",
|
|
1145
|
+
path: "/audit-logs/export",
|
|
1146
|
+
handler: "auditLog.export"
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
method: "DELETE",
|
|
1150
|
+
path: "/audit-logs",
|
|
1151
|
+
handler: "auditLog.clearAll"
|
|
743
1152
|
}
|
|
744
1153
|
]
|
|
745
1154
|
}
|
|
@@ -1110,9 +1519,9 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1110
1519
|
async getUsers() {
|
|
1111
1520
|
return getWhitelistQuery().findMany();
|
|
1112
1521
|
},
|
|
1113
|
-
async registerUser(email
|
|
1522
|
+
async registerUser(email) {
|
|
1114
1523
|
await getWhitelistQuery().create({
|
|
1115
|
-
data: { email
|
|
1524
|
+
data: { email }
|
|
1116
1525
|
});
|
|
1117
1526
|
},
|
|
1118
1527
|
async removeUser(id) {
|
|
@@ -1135,10 +1544,73 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1135
1544
|
}
|
|
1136
1545
|
};
|
|
1137
1546
|
}
|
|
1547
|
+
function interpolate(str, params) {
|
|
1548
|
+
if (!params) return str;
|
|
1549
|
+
return str.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
|
|
1550
|
+
}
|
|
1551
|
+
function translateDetails(key, params) {
|
|
1552
|
+
const translation = en[`audit.${key}`];
|
|
1553
|
+
if (!translation) return null;
|
|
1554
|
+
return interpolate(translation, params);
|
|
1555
|
+
}
|
|
1556
|
+
function auditLogService({ strapi: strapi2 }) {
|
|
1557
|
+
return {
|
|
1558
|
+
async log({ action, email, ip, detailsKey, detailsParams }) {
|
|
1559
|
+
if (!isAuditLogEnabled()) return;
|
|
1560
|
+
await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").create({
|
|
1561
|
+
data: {
|
|
1562
|
+
action,
|
|
1563
|
+
email: email ?? null,
|
|
1564
|
+
ip: ip ?? null,
|
|
1565
|
+
detailsKey: detailsKey ?? null,
|
|
1566
|
+
detailsParams: detailsParams ?? null
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
const eventHub = strapi2.serviceMap?.get?.("eventHub") ?? strapi2.eventHub;
|
|
1570
|
+
if (eventHub) {
|
|
1571
|
+
eventHub.emit(`strapi-plugin-oidc::auth.${action}`, {
|
|
1572
|
+
email,
|
|
1573
|
+
ip,
|
|
1574
|
+
provider: "strapi-plugin-oidc"
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
async find({ page = 1, pageSize = 25 } = {}) {
|
|
1579
|
+
const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").findPage({
|
|
1580
|
+
sort: { createdAt: "desc" },
|
|
1581
|
+
page,
|
|
1582
|
+
pageSize
|
|
1583
|
+
});
|
|
1584
|
+
const results = result.results.map((row) => ({
|
|
1585
|
+
...row,
|
|
1586
|
+
details: row.detailsKey ? translateDetails(row.detailsKey, row.detailsParams) : null
|
|
1587
|
+
}));
|
|
1588
|
+
return {
|
|
1589
|
+
results,
|
|
1590
|
+
pagination: result.pagination
|
|
1591
|
+
};
|
|
1592
|
+
},
|
|
1593
|
+
async clearAll() {
|
|
1594
|
+
const BATCH_SIZE = 1e3;
|
|
1595
|
+
let deletedCount;
|
|
1596
|
+
do {
|
|
1597
|
+
const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").deleteMany({ limit: BATCH_SIZE });
|
|
1598
|
+
deletedCount = result.count;
|
|
1599
|
+
} while (deletedCount === BATCH_SIZE);
|
|
1600
|
+
},
|
|
1601
|
+
async cleanup(retentionDays) {
|
|
1602
|
+
const cutoff = new Date(Date.now() - retentionDays * 864e5);
|
|
1603
|
+
await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").deleteMany({
|
|
1604
|
+
where: { createdAt: { $lt: cutoff } }
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1138
1609
|
const services = {
|
|
1139
1610
|
oauth: oauthService,
|
|
1140
1611
|
role: roleService,
|
|
1141
|
-
whitelist: whitelistService
|
|
1612
|
+
whitelist: whitelistService,
|
|
1613
|
+
auditLog: auditLogService
|
|
1142
1614
|
};
|
|
1143
1615
|
const index = {
|
|
1144
1616
|
register: register$1,
|