strapi-plugin-oidc 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -121,7 +121,12 @@ Role names are the **display names** shown in **Settings → Roles** (e.g. `"Edi
121
121
  1. **User's OIDC groups match `OIDC_GROUP_ROLE_MAP`** → use the mapped Strapi roles
122
122
  2. **No group match or no mapping configured** → use the default OIDC roles
123
123
 
124
- > **Note:** Existing users' roles are updated on every login to reflect current group membership.
124
+ ### Role updates on subsequent logins
125
+
126
+ - **New users** — OIDC roles are always assigned on first login.
127
+ - **Existing users with manually unchanged roles** — If a user's current roles still match the roles assigned by OIDC on their previous login (i.e., an administrator has not manually changed their roles), their roles are updated to reflect the current group mapping. This ensures that when the group-to-role mapping changes, returning users pick up the new roles automatically.
128
+ - **Existing users with manually changed roles** — If an administrator has manually assigned the user different roles since their last OIDC login, the user's roles are left unchanged. OIDC will not overwrite a manual role assignment.
129
+ - **Mapping removed or user's groups don't map** — If the `OIDC_GROUP_ROLE_MAP` is removed, a user's groups no longer match any mapping, or there are no default OIDC roles configured, the user keeps their last known roles.
125
130
 
126
131
  ## Whitelist API
127
132
 
@@ -140,14 +145,10 @@ API calls write directly to the database — there is no unsaved state.
140
145
 
141
146
  ### Import format
142
147
 
143
- Accepted by both the API import endpoint and the Admin UI import button. `roles` is optional and accepts role **names** (recommended) or numeric IDs. If the email already exists as a Strapi admin user, their current roles are used automatically.
148
+ Accepted by both the API import endpoint and the Admin UI import button. If the email already exists as a Strapi admin user, their current roles are used automatically.
144
149
 
145
150
  ```json
146
- [
147
- { "email": "alice@example.com", "roles": ["Editor"] },
148
- { "email": "bob@example.com", "roles": ["Editor", "Author"] },
149
- { "email": "carol@example.com" }
150
- ]
151
+ [{ "email": "alice@example.com" }, { "email": "bob@example.com" }]
151
152
  ```
152
153
 
153
154
  Duplicate emails within the payload and emails already in the whitelist are silently skipped.
@@ -166,12 +167,12 @@ curl -H "Authorization: Bearer <token>" \
166
167
 
167
168
  # Add
168
169
  curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
169
- -d '{"email": "user@example.com", "roles": ["Editor"]}' \
170
+ -d '{"email": "user@example.com"}' \
170
171
  http://localhost:1337/api/strapi-plugin-oidc/whitelist
171
172
 
172
173
  # Bulk import
173
174
  curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
174
- -d '{"users": [{"email": "a@example.com", "roles": ["Editor"]}, {"email": "b@example.com"}]}' \
175
+ -d '{"users": [{"email": "a@example.com"}, {"email": "b@example.com"}]}' \
175
176
  http://localhost:1337/api/strapi-plugin-oidc/whitelist/import
176
177
 
177
178
  # Delete one (by email)
@@ -494,6 +494,30 @@ async function registerNewUser(oauthService2, email, userResponseData, config2,
494
494
  await oauthService2.triggerWebHook(activateUser);
495
495
  return activateUser;
496
496
  }
497
+ function rolesChanged(current, next) {
498
+ return current.size !== next.size || [...next].some((id) => !current.has(id));
499
+ }
500
+ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
501
+ try {
502
+ strapi.log.info(
503
+ `[OIDC] Roles updated for user ${user.id}: [${[...currentRoleIds].join(",")}] -> [${newRoleIds.join(",")}]`
504
+ );
505
+ await strapi.db.query("admin::user").update({
506
+ where: { id: user.id },
507
+ data: { roles: newRoleIds }
508
+ });
509
+ } catch (updateErr) {
510
+ strapi.log.error({
511
+ code: errorCodes.ROLE_UPDATE_FAILED,
512
+ userId: user.id,
513
+ detail: getErrorDetail("role_update_failed", {
514
+ userId: user.id,
515
+ error: updateErr.message
516
+ })
517
+ });
518
+ throw updateErr;
519
+ }
520
+ }
497
521
  async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
498
522
  const rawEmail = String(userResponseData.email ?? "");
499
523
  const email = rawEmail.toLowerCase();
@@ -506,40 +530,26 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
506
530
  const resolvedRoleNames = allRoles.filter((r) => roles2.includes(String(r.id))).map((r) => r.name);
507
531
  let userCreated = false;
508
532
  let rolesUpdated = false;
509
- let activateUser = await userService.findOneByEmail(email);
510
- if (!activateUser) {
511
- activateUser = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
533
+ let user = await userService.findOneByEmail(email, ["roles"]);
534
+ if (!user) {
535
+ user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
512
536
  userCreated = true;
537
+ rolesUpdated = true;
513
538
  } else if (roles2.length > 0) {
514
- const currentRoleIds = new Set((activateUser.roles ?? []).map((r) => String(r.id)));
539
+ const defaultRoleIds = new Set(user.roles.map((r) => String(r.id)));
540
+ const currentRoleIds = new Set(user.roles.map((r) => String(r.id)));
515
541
  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
- });
542
+ if (rolesChanged(currentRoleIds, newRoleIds)) {
543
+ const isOnDefaultRoles = currentRoleIds.size === defaultRoleIds.size && [...currentRoleIds].every((id) => defaultRoleIds.has(id));
544
+ if (isOnDefaultRoles) {
545
+ await updateUserRoles(user, currentRoleIds, roles2);
526
546
  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
547
  }
538
548
  }
539
549
  }
540
- const jwtToken = await oauthService2.generateToken(activateUser, ctx);
541
- oauthService2.triggerSignInSuccess(activateUser);
542
- return { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
550
+ const jwtToken = await oauthService2.generateToken(user, ctx);
551
+ oauthService2.triggerSignInSuccess(user);
552
+ return { activateUser: user, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
543
553
  }
544
554
  function classifyOidcError(msg, userInfo) {
545
555
  const errorMap = [
@@ -834,9 +844,9 @@ async function register(ctx) {
834
844
  ctx.body = { matchedExistingUsersCount };
835
845
  }
836
846
  async function removeEmail(ctx) {
837
- const { id } = ctx.params;
847
+ const { email } = ctx.params;
838
848
  const whitelistService2 = getWhitelistService();
839
- await whitelistService2.removeUser(id);
849
+ await whitelistService2.removeUser(email);
840
850
  ctx.body = {};
841
851
  }
842
852
  async function deleteAll(ctx) {
@@ -880,7 +890,7 @@ async function syncUsers(ctx) {
880
890
  const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
881
891
  for (const currUser of currentUsers) {
882
892
  if (!syncEmailSet.has(currUser.email)) {
883
- await whitelistService2.removeUser(currUser.id);
893
+ await whitelistService2.removeUser(currUser.email);
884
894
  }
885
895
  }
886
896
  for (const email of emails) {
@@ -1063,7 +1073,7 @@ const routes = {
1063
1073
  },
1064
1074
  {
1065
1075
  method: "DELETE",
1066
- path: "/whitelist/:id",
1076
+ path: "/whitelist/:email",
1067
1077
  handler: "whitelist.removeEmail",
1068
1078
  config: adminPolicies("update")
1069
1079
  },
@@ -1122,7 +1132,7 @@ const routes = {
1122
1132
  },
1123
1133
  {
1124
1134
  method: "DELETE",
1125
- path: "/whitelist/:id",
1135
+ path: "/whitelist/:email",
1126
1136
  handler: "whitelist.removeEmail"
1127
1137
  },
1128
1138
  {
@@ -1524,9 +1534,9 @@ function whitelistService({ strapi: strapi2 }) {
1524
1534
  data: { email }
1525
1535
  });
1526
1536
  },
1527
- async removeUser(id) {
1528
- await getWhitelistQuery().delete({
1529
- where: { id }
1537
+ async removeUser(email) {
1538
+ await getWhitelistQuery().deleteMany({
1539
+ where: { email }
1530
1540
  });
1531
1541
  },
1532
1542
  async checkWhitelistForEmail(email) {
@@ -488,6 +488,30 @@ async function registerNewUser(oauthService2, email, userResponseData, config2,
488
488
  await oauthService2.triggerWebHook(activateUser);
489
489
  return activateUser;
490
490
  }
491
+ function rolesChanged(current, next) {
492
+ return current.size !== next.size || [...next].some((id) => !current.has(id));
493
+ }
494
+ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
495
+ try {
496
+ strapi.log.info(
497
+ `[OIDC] Roles updated for user ${user.id}: [${[...currentRoleIds].join(",")}] -> [${newRoleIds.join(",")}]`
498
+ );
499
+ await strapi.db.query("admin::user").update({
500
+ where: { id: user.id },
501
+ data: { roles: newRoleIds }
502
+ });
503
+ } catch (updateErr) {
504
+ strapi.log.error({
505
+ code: errorCodes.ROLE_UPDATE_FAILED,
506
+ userId: user.id,
507
+ detail: getErrorDetail("role_update_failed", {
508
+ userId: user.id,
509
+ error: updateErr.message
510
+ })
511
+ });
512
+ throw updateErr;
513
+ }
514
+ }
491
515
  async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
492
516
  const rawEmail = String(userResponseData.email ?? "");
493
517
  const email = rawEmail.toLowerCase();
@@ -500,40 +524,26 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
500
524
  const resolvedRoleNames = allRoles.filter((r) => roles2.includes(String(r.id))).map((r) => r.name);
501
525
  let userCreated = false;
502
526
  let rolesUpdated = false;
503
- let activateUser = await userService.findOneByEmail(email);
504
- if (!activateUser) {
505
- activateUser = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
527
+ let user = await userService.findOneByEmail(email, ["roles"]);
528
+ if (!user) {
529
+ user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
506
530
  userCreated = true;
531
+ rolesUpdated = true;
507
532
  } else if (roles2.length > 0) {
508
- const currentRoleIds = new Set((activateUser.roles ?? []).map((r) => String(r.id)));
533
+ const defaultRoleIds = new Set(user.roles.map((r) => String(r.id)));
534
+ const currentRoleIds = new Set(user.roles.map((r) => String(r.id)));
509
535
  const newRoleIds = new Set(roles2);
510
- const rolesChanged = currentRoleIds.size !== newRoleIds.size || [...newRoleIds].some((id) => !currentRoleIds.has(id));
511
- if (rolesChanged) {
512
- try {
513
- strapi.log.info(
514
- `[OIDC] Roles updated for user ${activateUser.id}: [${[...currentRoleIds].join(",")}] -> [${roles2.join(",")}]`
515
- );
516
- await strapi.db.query("admin::user").update({
517
- where: { id: activateUser.id },
518
- data: { roles: roles2 }
519
- });
536
+ if (rolesChanged(currentRoleIds, newRoleIds)) {
537
+ const isOnDefaultRoles = currentRoleIds.size === defaultRoleIds.size && [...currentRoleIds].every((id) => defaultRoleIds.has(id));
538
+ if (isOnDefaultRoles) {
539
+ await updateUserRoles(user, currentRoleIds, roles2);
520
540
  rolesUpdated = true;
521
- } catch (updateErr) {
522
- strapi.log.error({
523
- code: errorCodes.ROLE_UPDATE_FAILED,
524
- userId: activateUser.id,
525
- detail: getErrorDetail("role_update_failed", {
526
- userId: activateUser.id,
527
- error: updateErr.message
528
- })
529
- });
530
- throw updateErr;
531
541
  }
532
542
  }
533
543
  }
534
- const jwtToken = await oauthService2.generateToken(activateUser, ctx);
535
- oauthService2.triggerSignInSuccess(activateUser);
536
- return { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
544
+ const jwtToken = await oauthService2.generateToken(user, ctx);
545
+ oauthService2.triggerSignInSuccess(user);
546
+ return { activateUser: user, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
537
547
  }
538
548
  function classifyOidcError(msg, userInfo) {
539
549
  const errorMap = [
@@ -828,9 +838,9 @@ async function register(ctx) {
828
838
  ctx.body = { matchedExistingUsersCount };
829
839
  }
830
840
  async function removeEmail(ctx) {
831
- const { id } = ctx.params;
841
+ const { email } = ctx.params;
832
842
  const whitelistService2 = getWhitelistService();
833
- await whitelistService2.removeUser(id);
843
+ await whitelistService2.removeUser(email);
834
844
  ctx.body = {};
835
845
  }
836
846
  async function deleteAll(ctx) {
@@ -874,7 +884,7 @@ async function syncUsers(ctx) {
874
884
  const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
875
885
  for (const currUser of currentUsers) {
876
886
  if (!syncEmailSet.has(currUser.email)) {
877
- await whitelistService2.removeUser(currUser.id);
887
+ await whitelistService2.removeUser(currUser.email);
878
888
  }
879
889
  }
880
890
  for (const email of emails) {
@@ -1057,7 +1067,7 @@ const routes = {
1057
1067
  },
1058
1068
  {
1059
1069
  method: "DELETE",
1060
- path: "/whitelist/:id",
1070
+ path: "/whitelist/:email",
1061
1071
  handler: "whitelist.removeEmail",
1062
1072
  config: adminPolicies("update")
1063
1073
  },
@@ -1116,7 +1126,7 @@ const routes = {
1116
1126
  },
1117
1127
  {
1118
1128
  method: "DELETE",
1119
- path: "/whitelist/:id",
1129
+ path: "/whitelist/:email",
1120
1130
  handler: "whitelist.removeEmail"
1121
1131
  },
1122
1132
  {
@@ -1518,9 +1528,9 @@ function whitelistService({ strapi: strapi2 }) {
1518
1528
  data: { email }
1519
1529
  });
1520
1530
  },
1521
- async removeUser(id) {
1522
- await getWhitelistQuery().delete({
1523
- where: { id }
1531
+ async removeUser(email) {
1532
+ await getWhitelistQuery().deleteMany({
1533
+ where: { email }
1524
1534
  });
1525
1535
  },
1526
1536
  async checkWhitelistForEmail(email) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-oidc",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "A Strapi plugin that provides OpenID Connect (OIDC) authentication functionality for the Strapi Admin Panel.",
5
5
  "strapi": {
6
6
  "displayName": "OIDC Plugin",