strapi-plugin-oidc 1.5.3 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -34
- 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 +628 -182
- package/dist/server/index.mjs +629 -183
- 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";
|
|
@@ -223,7 +244,130 @@ function clearAuthCookies(strapi2, ctx) {
|
|
|
223
244
|
ctx.cookies.set("strapi_admin_refresh", "", options2);
|
|
224
245
|
ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
|
|
225
246
|
ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
|
|
247
|
+
ctx.cookies.set("oidc_user_email", "", { ...options2, path: "/" });
|
|
226
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
|
+
};
|
|
227
371
|
const REQUIRED_CONFIG_KEYS = [
|
|
228
372
|
"OIDC_CLIENT_ID",
|
|
229
373
|
"OIDC_CLIENT_SECRET",
|
|
@@ -261,15 +405,16 @@ async function oidcSignIn(ctx) {
|
|
|
261
405
|
ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
|
|
262
406
|
ctx.cookies.set("oidc_state", state, cookieOptions);
|
|
263
407
|
ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
|
|
264
|
-
const params = new URLSearchParams(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
});
|
|
273
418
|
const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
|
|
274
419
|
ctx.set("Location", authorizationUrl);
|
|
275
420
|
return ctx.send({}, 302);
|
|
@@ -307,14 +452,35 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
|
|
|
307
452
|
const userInfo = await userResponse.json();
|
|
308
453
|
return { userInfo, accessToken: tokenData.access_token };
|
|
309
454
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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 [];
|
|
317
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) {
|
|
318
484
|
const defaultLocale = oauthService2.localeFindByHeader(
|
|
319
485
|
ctx.request.headers
|
|
320
486
|
);
|
|
@@ -329,21 +495,106 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
|
|
|
329
495
|
return activateUser;
|
|
330
496
|
}
|
|
331
497
|
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
);
|
|
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
|
+
}
|
|
344
540
|
const jwtToken = await oauthService2.generateToken(activateUser, ctx);
|
|
345
541
|
oauthService2.triggerSignInSuccess(activateUser);
|
|
346
|
-
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
|
+
};
|
|
347
598
|
}
|
|
348
599
|
async function oidcSignInCallback(ctx) {
|
|
349
600
|
const config2 = configValidation();
|
|
@@ -351,8 +602,10 @@ async function oidcSignInCallback(ctx) {
|
|
|
351
602
|
const oauthService2 = strapi.plugin("strapi-plugin-oidc").service("oauth");
|
|
352
603
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
353
604
|
const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
|
|
605
|
+
const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
|
|
354
606
|
if (!ctx.query.code) {
|
|
355
|
-
|
|
607
|
+
await auditLog2.log({ action: "missing_code", ip: ctx.ip });
|
|
608
|
+
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
|
|
356
609
|
}
|
|
357
610
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
358
611
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
@@ -361,21 +614,22 @@ async function oidcSignInCallback(ctx) {
|
|
|
361
614
|
ctx.cookies.set("oidc_code_verifier", null);
|
|
362
615
|
ctx.cookies.set("oidc_nonce", null);
|
|
363
616
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
364
|
-
|
|
617
|
+
await auditLog2.log({ action: "state_mismatch", ip: ctx.ip });
|
|
618
|
+
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.invalid_state));
|
|
365
619
|
}
|
|
366
|
-
const params = new URLSearchParams(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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;
|
|
373
629
|
try {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
oidcNonce ?? ""
|
|
378
|
-
);
|
|
630
|
+
const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
631
|
+
userInfo = exchangeResult.userInfo;
|
|
632
|
+
const accessToken = exchangeResult.accessToken;
|
|
379
633
|
const isProduction = strapi.config.get("environment") === "production";
|
|
380
634
|
ctx.cookies.set("oidc_access_token", accessToken, {
|
|
381
635
|
httpOnly: true,
|
|
@@ -384,7 +638,7 @@ async function oidcSignInCallback(ctx) {
|
|
|
384
638
|
secure: isProduction && ctx.request.secure,
|
|
385
639
|
sameSite: "lax"
|
|
386
640
|
});
|
|
387
|
-
const { activateUser, jwtToken } = await handleUserAuthentication(
|
|
641
|
+
const { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames } = await handleUserAuthentication(
|
|
388
642
|
userService,
|
|
389
643
|
oauthService2,
|
|
390
644
|
roleService2,
|
|
@@ -393,21 +647,61 @@ async function oidcSignInCallback(ctx) {
|
|
|
393
647
|
config2,
|
|
394
648
|
ctx
|
|
395
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
|
+
});
|
|
396
673
|
const nonce = node_crypto.randomUUID();
|
|
397
674
|
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
398
675
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
399
676
|
ctx.send(html);
|
|
400
677
|
} catch (e) {
|
|
401
|
-
|
|
402
|
-
|
|
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));
|
|
403
695
|
}
|
|
404
696
|
}
|
|
405
697
|
async function logout(ctx) {
|
|
406
698
|
const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
|
|
699
|
+
const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
|
|
407
700
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
408
701
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
409
702
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
410
703
|
const accessToken = ctx.cookies.get("oidc_access_token");
|
|
704
|
+
const userEmail = ctx.cookies.get("oidc_user_email") ?? void 0;
|
|
411
705
|
clearAuthCookies(strapi, ctx);
|
|
412
706
|
if (logoutUrl && isOidcSession && accessToken) {
|
|
413
707
|
try {
|
|
@@ -415,11 +709,22 @@ async function logout(ctx) {
|
|
|
415
709
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
416
710
|
});
|
|
417
711
|
if (response.ok) {
|
|
712
|
+
if (userEmail)
|
|
713
|
+
auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip }).catch(() => {
|
|
714
|
+
});
|
|
418
715
|
return ctx.redirect(logoutUrl);
|
|
419
716
|
}
|
|
717
|
+
if (userEmail)
|
|
718
|
+
await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
|
|
719
|
+
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
420
720
|
} catch {
|
|
721
|
+
if (userEmail)
|
|
722
|
+
await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
|
|
723
|
+
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
421
724
|
}
|
|
422
|
-
|
|
725
|
+
}
|
|
726
|
+
if (isOidcSession && userEmail) {
|
|
727
|
+
await auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip });
|
|
423
728
|
}
|
|
424
729
|
if (logoutUrl && isOidcSession) {
|
|
425
730
|
return ctx.redirect(logoutUrl);
|
|
@@ -431,7 +736,7 @@ const oidc = {
|
|
|
431
736
|
oidcSignInCallback,
|
|
432
737
|
logout
|
|
433
738
|
};
|
|
434
|
-
async function find(ctx) {
|
|
739
|
+
async function find$1(ctx) {
|
|
435
740
|
const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
|
|
436
741
|
const roles2 = await roleService2.find();
|
|
437
742
|
const oidcConstants = roleService2.getOidcRoles();
|
|
@@ -450,14 +755,23 @@ async function update(ctx) {
|
|
|
450
755
|
await roleService2.update(roles2);
|
|
451
756
|
ctx.send({}, 204);
|
|
452
757
|
} catch (e) {
|
|
453
|
-
|
|
758
|
+
strapi.log.error(e);
|
|
454
759
|
ctx.send({}, 400);
|
|
455
760
|
}
|
|
456
761
|
}
|
|
457
762
|
const role = {
|
|
458
|
-
find,
|
|
763
|
+
find: find$1,
|
|
459
764
|
update
|
|
460
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
|
+
}
|
|
461
775
|
function getWhitelistService() {
|
|
462
776
|
return strapi.plugin("strapi-plugin-oidc").service("whitelist");
|
|
463
777
|
}
|
|
@@ -469,7 +783,8 @@ async function info(ctx) {
|
|
|
469
783
|
useWhitelist: settings.useWhitelist,
|
|
470
784
|
enforceOIDC: resolveEnforceOIDC(strapi, settings.enforceOIDC),
|
|
471
785
|
enforceOIDCConfig: getEnforceOIDCConfig(strapi),
|
|
472
|
-
whitelistUsers
|
|
786
|
+
whitelistUsers,
|
|
787
|
+
auditLogEnabled: isAuditLogEnabled()
|
|
473
788
|
};
|
|
474
789
|
}
|
|
475
790
|
async function updateSettings(ctx) {
|
|
@@ -495,121 +810,85 @@ async function publicSettings(ctx) {
|
|
|
495
810
|
};
|
|
496
811
|
}
|
|
497
812
|
async function register(ctx) {
|
|
498
|
-
const { email
|
|
813
|
+
const { email } = ctx.request.body;
|
|
499
814
|
if (!email) {
|
|
500
815
|
ctx.body = { message: "Please enter a valid email address" };
|
|
501
816
|
return;
|
|
502
817
|
}
|
|
503
818
|
const rawEmails = Array.isArray(email) ? email : email.split(",");
|
|
504
819
|
const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
|
|
505
|
-
const existingUsers = await strapi.query("admin::user").findMany({
|
|
506
|
-
where: { email: { $in: emailList } },
|
|
507
|
-
populate: ["roles"]
|
|
508
|
-
});
|
|
509
|
-
const existingUsersByEmail = new Map(existingUsers.map((u) => [u.email, u]));
|
|
510
820
|
const whitelistService2 = getWhitelistService();
|
|
511
821
|
let matchedExistingUsersCount = 0;
|
|
512
822
|
for (const singleEmail of emailList) {
|
|
513
|
-
const existingUser =
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
matchedExistingUsersCount++;
|
|
518
|
-
}
|
|
823
|
+
const existingUser = await strapi.query("admin::user").findOne({
|
|
824
|
+
where: { email: singleEmail }
|
|
825
|
+
});
|
|
826
|
+
if (existingUser) matchedExistingUsersCount++;
|
|
519
827
|
const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({
|
|
520
828
|
where: { email: singleEmail }
|
|
521
829
|
});
|
|
522
830
|
if (!alreadyWhitelisted) {
|
|
523
|
-
await whitelistService2.registerUser(singleEmail
|
|
831
|
+
await whitelistService2.registerUser(singleEmail);
|
|
524
832
|
}
|
|
525
833
|
}
|
|
526
834
|
ctx.body = { matchedExistingUsersCount };
|
|
527
835
|
}
|
|
528
836
|
async function removeEmail(ctx) {
|
|
529
|
-
const {
|
|
837
|
+
const { email } = ctx.params;
|
|
530
838
|
const whitelistService2 = getWhitelistService();
|
|
531
|
-
await whitelistService2.removeUser(
|
|
839
|
+
await whitelistService2.removeUser(email);
|
|
532
840
|
ctx.body = {};
|
|
533
841
|
}
|
|
534
842
|
async function deleteAll(ctx) {
|
|
535
843
|
await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
|
|
536
844
|
ctx.body = {};
|
|
537
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
|
+
}
|
|
538
854
|
async function importUsers(ctx) {
|
|
539
855
|
const { users } = ctx.request.body;
|
|
540
856
|
if (!Array.isArray(users)) {
|
|
541
857
|
ctx.status = 400;
|
|
542
|
-
ctx.body = { error: "Expected { users: [{email
|
|
858
|
+
ctx.body = { error: "Expected { users: [{email}] }" };
|
|
543
859
|
return;
|
|
544
860
|
}
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
const resolveRole = (nameOrId) => roleNameToId.get(nameOrId) ?? nameOrId;
|
|
548
|
-
const normalized = users.filter((u) => u?.email).map((u) => ({
|
|
549
|
-
email: String(u.email).trim().toLowerCase(),
|
|
550
|
-
roles: (Array.isArray(u.roles) ? u.roles : []).map(resolveRole)
|
|
551
|
-
}));
|
|
552
|
-
const seen = /* @__PURE__ */ new Set();
|
|
553
|
-
const deduped = normalized.filter((u) => {
|
|
554
|
-
if (seen.has(u.email)) return false;
|
|
555
|
-
seen.add(u.email);
|
|
556
|
-
return true;
|
|
557
|
-
});
|
|
558
|
-
const strapiUsers = await strapi.query("admin::user").findMany({
|
|
559
|
-
where: { email: { $in: deduped.map((u) => u.email) } },
|
|
560
|
-
populate: ["roles"]
|
|
561
|
-
});
|
|
562
|
-
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)];
|
|
563
863
|
const whitelistService2 = getWhitelistService();
|
|
564
864
|
const existing = await whitelistService2.getUsers();
|
|
565
865
|
const existingEmails = new Set(existing.map((u) => u.email));
|
|
566
866
|
let importedCount = 0;
|
|
567
|
-
for (const
|
|
568
|
-
if (existingEmails.has(
|
|
569
|
-
|
|
570
|
-
const finalRoles = strapiUser?.roles?.length ? strapiUser.roles.map((r) => String(r.id)) : user.roles;
|
|
571
|
-
await whitelistService2.registerUser(user.email, finalRoles);
|
|
867
|
+
for (const email of deduped) {
|
|
868
|
+
if (existingEmails.has(email)) continue;
|
|
869
|
+
await whitelistService2.registerUser(email);
|
|
572
870
|
importedCount++;
|
|
573
871
|
}
|
|
574
872
|
ctx.body = { importedCount };
|
|
575
873
|
}
|
|
576
874
|
async function syncUsers(ctx) {
|
|
577
875
|
const { users: rawUsers } = ctx.request.body;
|
|
578
|
-
const
|
|
876
|
+
const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter((e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e));
|
|
579
877
|
const whitelistService2 = getWhitelistService();
|
|
580
878
|
const currentUsers = await whitelistService2.getUsers();
|
|
581
|
-
|
|
582
|
-
const emailsToSync = users.map((u) => u.email);
|
|
583
|
-
const existingStrapiUsers = await strapi.query("admin::user").findMany({
|
|
584
|
-
where: { email: { $in: emailsToSync } },
|
|
585
|
-
populate: ["roles"]
|
|
586
|
-
});
|
|
587
|
-
const syncEmailSet = new Set(emailsToSync);
|
|
879
|
+
const syncEmailSet = new Set(emails);
|
|
588
880
|
const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
|
|
589
|
-
const strapiUsersByEmail = new Map(existingStrapiUsers.map((u) => [u.email, u]));
|
|
590
881
|
for (const currUser of currentUsers) {
|
|
591
882
|
if (!syncEmailSet.has(currUser.email)) {
|
|
592
|
-
await whitelistService2.removeUser(currUser.
|
|
883
|
+
await whitelistService2.removeUser(currUser.email);
|
|
593
884
|
}
|
|
594
885
|
}
|
|
595
|
-
for (const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (!currUser) {
|
|
599
|
-
const existingStrapiUser = strapiUsersByEmail.get(user.email);
|
|
600
|
-
if (existingStrapiUser?.roles) {
|
|
601
|
-
finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
|
|
602
|
-
matchedExistingUsersCount++;
|
|
603
|
-
}
|
|
604
|
-
await whitelistService2.registerUser(user.email, finalRoles);
|
|
605
|
-
} else {
|
|
606
|
-
await strapi.query("plugin::strapi-plugin-oidc.whitelists").update({
|
|
607
|
-
where: { id: currUser.id },
|
|
608
|
-
data: { roles: finalRoles }
|
|
609
|
-
});
|
|
886
|
+
for (const email of emails) {
|
|
887
|
+
if (!currentUsersByEmail.has(email)) {
|
|
888
|
+
await whitelistService2.registerUser(email);
|
|
610
889
|
}
|
|
611
890
|
}
|
|
612
|
-
ctx.body = { matchedExistingUsersCount };
|
|
891
|
+
ctx.body = { matchedExistingUsersCount: 0 };
|
|
613
892
|
}
|
|
614
893
|
const whitelist = {
|
|
615
894
|
info,
|
|
@@ -619,39 +898,99 @@ const whitelist = {
|
|
|
619
898
|
removeEmail,
|
|
620
899
|
deleteAll,
|
|
621
900
|
syncUsers,
|
|
622
|
-
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
|
|
623
951
|
};
|
|
624
952
|
const controllers = {
|
|
625
953
|
oidc,
|
|
626
954
|
role,
|
|
627
|
-
whitelist
|
|
955
|
+
whitelist,
|
|
956
|
+
auditLog
|
|
628
957
|
};
|
|
629
958
|
const rateLimitMap = /* @__PURE__ */ new Map();
|
|
630
959
|
const RATE_LIMIT_WINDOW = 6e4;
|
|
631
|
-
const MAX_REQUESTS =
|
|
632
|
-
|
|
960
|
+
const MAX_REQUESTS = 1e3;
|
|
961
|
+
function getRateLimitKey(ctx) {
|
|
633
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);
|
|
634
969
|
const now = Date.now();
|
|
635
970
|
const windowStart = now - RATE_LIMIT_WINDOW;
|
|
636
|
-
const requestStamps = (rateLimitMap.get(
|
|
971
|
+
const requestStamps = (rateLimitMap.get(key) || []).filter(
|
|
972
|
+
(timestamp) => timestamp > windowStart
|
|
973
|
+
);
|
|
637
974
|
if (requestStamps.length >= MAX_REQUESTS) {
|
|
638
975
|
ctx.status = 429;
|
|
639
976
|
ctx.body = "Too Many Requests";
|
|
640
977
|
return;
|
|
641
978
|
}
|
|
642
979
|
requestStamps.push(now);
|
|
643
|
-
rateLimitMap.set(
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
}
|
|
655
994
|
const routes = {
|
|
656
995
|
admin: {
|
|
657
996
|
type: "admin",
|
|
@@ -724,7 +1063,7 @@ const routes = {
|
|
|
724
1063
|
},
|
|
725
1064
|
{
|
|
726
1065
|
method: "DELETE",
|
|
727
|
-
path: "/whitelist/:
|
|
1066
|
+
path: "/whitelist/:email",
|
|
728
1067
|
handler: "whitelist.removeEmail",
|
|
729
1068
|
config: adminPolicies("update")
|
|
730
1069
|
},
|
|
@@ -733,6 +1072,30 @@ const routes = {
|
|
|
733
1072
|
path: "/whitelist",
|
|
734
1073
|
handler: "whitelist.deleteAll",
|
|
735
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"] }
|
|
736
1099
|
}
|
|
737
1100
|
]
|
|
738
1101
|
},
|
|
@@ -759,13 +1122,33 @@ const routes = {
|
|
|
759
1122
|
},
|
|
760
1123
|
{
|
|
761
1124
|
method: "DELETE",
|
|
762
|
-
path: "/whitelist/:
|
|
1125
|
+
path: "/whitelist/:email",
|
|
763
1126
|
handler: "whitelist.removeEmail"
|
|
764
1127
|
},
|
|
765
1128
|
{
|
|
766
1129
|
method: "DELETE",
|
|
767
1130
|
path: "/whitelist",
|
|
768
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"
|
|
769
1152
|
}
|
|
770
1153
|
]
|
|
771
1154
|
}
|
|
@@ -1136,14 +1519,14 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1136
1519
|
async getUsers() {
|
|
1137
1520
|
return getWhitelistQuery().findMany();
|
|
1138
1521
|
},
|
|
1139
|
-
async registerUser(email
|
|
1522
|
+
async registerUser(email) {
|
|
1140
1523
|
await getWhitelistQuery().create({
|
|
1141
|
-
data: { email
|
|
1524
|
+
data: { email }
|
|
1142
1525
|
});
|
|
1143
1526
|
},
|
|
1144
|
-
async removeUser(
|
|
1145
|
-
await
|
|
1146
|
-
where: {
|
|
1527
|
+
async removeUser(email) {
|
|
1528
|
+
await strapi2.db.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({
|
|
1529
|
+
where: { email }
|
|
1147
1530
|
});
|
|
1148
1531
|
},
|
|
1149
1532
|
async checkWhitelistForEmail(email) {
|
|
@@ -1161,10 +1544,73 @@ function whitelistService({ strapi: strapi2 }) {
|
|
|
1161
1544
|
}
|
|
1162
1545
|
};
|
|
1163
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
|
+
}
|
|
1164
1609
|
const services = {
|
|
1165
1610
|
oauth: oauthService,
|
|
1166
1611
|
role: roleService,
|
|
1167
|
-
whitelist: whitelistService
|
|
1612
|
+
whitelist: whitelistService,
|
|
1613
|
+
auditLog: auditLogService
|
|
1168
1614
|
};
|
|
1169
1615
|
const index = {
|
|
1170
1616
|
register: register$1,
|