@strapi/plugin-users-permissions 5.48.1 → 5.50.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.
Files changed (159) hide show
  1. package/.eslintrc.cjs +1 -0
  2. package/admin/src/pages/AdvancedSettings/index.jsx +1 -1
  3. package/admin/src/pages/Roles/pages/ListPage/index.jsx +1 -1
  4. package/admin/src/translations/ja.json +46 -7
  5. package/dist/admin/components/BoundRoute/index.js +12 -6
  6. package/dist/admin/components/BoundRoute/index.js.map +1 -1
  7. package/dist/admin/components/FormModal/Input/index.js +25 -21
  8. package/dist/admin/components/FormModal/Input/index.js.map +1 -1
  9. package/dist/admin/components/FormModal/index.js +14 -10
  10. package/dist/admin/components/FormModal/index.js.map +1 -1
  11. package/dist/admin/components/Permissions/PermissionRow/SubCategory.js +8 -3
  12. package/dist/admin/components/Permissions/PermissionRow/SubCategory.js.map +1 -1
  13. package/dist/admin/components/Permissions/PermissionRow/index.js +9 -4
  14. package/dist/admin/components/Permissions/PermissionRow/index.js.map +1 -1
  15. package/dist/admin/components/Policies/index.js +9 -3
  16. package/dist/admin/components/Policies/index.js.map +1 -1
  17. package/dist/admin/components/UsersPermissions/index.js +6 -2
  18. package/dist/admin/components/UsersPermissions/index.js.map +1 -1
  19. package/dist/admin/components/UsersPermissions/reducer.js +10 -4
  20. package/dist/admin/components/UsersPermissions/reducer.js.map +1 -1
  21. package/dist/admin/contexts/UsersPermissionsContext/index.js +6 -2
  22. package/dist/admin/contexts/UsersPermissionsContext/index.js.map +1 -1
  23. package/dist/admin/pages/AdvancedSettings/index.js +1 -1
  24. package/dist/admin/pages/AdvancedSettings/index.js.map +1 -1
  25. package/dist/admin/pages/AdvancedSettings/index.mjs +1 -1
  26. package/dist/admin/pages/AdvancedSettings/index.mjs.map +1 -1
  27. package/dist/admin/pages/AdvancedSettings/utils/schema.js +3 -2
  28. package/dist/admin/pages/AdvancedSettings/utils/schema.js.map +1 -1
  29. package/dist/admin/pages/EmailTemplates/components/EmailForm.js +17 -13
  30. package/dist/admin/pages/EmailTemplates/components/EmailForm.js.map +1 -1
  31. package/dist/admin/pages/EmailTemplates/components/EmailTable.js +6 -2
  32. package/dist/admin/pages/EmailTemplates/components/EmailTable.js.map +1 -1
  33. package/dist/admin/pages/EmailTemplates/index.js +3 -2
  34. package/dist/admin/pages/EmailTemplates/index.js.map +1 -1
  35. package/dist/admin/pages/EmailTemplates/utils/schema.js +3 -2
  36. package/dist/admin/pages/EmailTemplates/utils/schema.js.map +1 -1
  37. package/dist/admin/pages/Providers/index.js +7 -3
  38. package/dist/admin/pages/Providers/index.js.map +1 -1
  39. package/dist/admin/pages/Providers/utils/forms.js +3 -2
  40. package/dist/admin/pages/Providers/utils/forms.js.map +1 -1
  41. package/dist/admin/pages/Roles/constants.js +3 -2
  42. package/dist/admin/pages/Roles/constants.js.map +1 -1
  43. package/dist/admin/pages/Roles/pages/CreatePage.js +3 -2
  44. package/dist/admin/pages/Roles/pages/CreatePage.js.map +1 -1
  45. package/dist/admin/pages/Roles/pages/EditPage.js +3 -2
  46. package/dist/admin/pages/Roles/pages/EditPage.js.map +1 -1
  47. package/dist/admin/pages/Roles/pages/ListPage/components/TableBody.js +9 -5
  48. package/dist/admin/pages/Roles/pages/ListPage/components/TableBody.js.map +1 -1
  49. package/dist/admin/pages/Roles/pages/ListPage/index.js +1 -1
  50. package/dist/admin/pages/Roles/pages/ListPage/index.js.map +1 -1
  51. package/dist/admin/pages/Roles/pages/ListPage/index.mjs +1 -1
  52. package/dist/admin/pages/Roles/pages/ListPage/index.mjs.map +1 -1
  53. package/dist/admin/translations/ja.json.js +46 -7
  54. package/dist/admin/translations/ja.json.js.map +1 -1
  55. package/dist/admin/translations/ja.json.mjs +46 -7
  56. package/dist/admin/translations/ja.json.mjs.map +1 -1
  57. package/dist/admin/utils/cleanPermissions.js +6 -2
  58. package/dist/admin/utils/cleanPermissions.js.map +1 -1
  59. package/dist/admin/utils/formatPluginName.js +5 -1
  60. package/dist/admin/utils/formatPluginName.js.map +1 -1
  61. package/dist/server/bootstrap/index.js +7 -2
  62. package/dist/server/bootstrap/index.js.map +1 -1
  63. package/dist/server/controllers/auth.js +144 -117
  64. package/dist/server/controllers/auth.js.map +1 -1
  65. package/dist/server/controllers/auth.mjs +132 -113
  66. package/dist/server/controllers/auth.mjs.map +1 -1
  67. package/dist/server/controllers/content-manager-user.js +8 -3
  68. package/dist/server/controllers/content-manager-user.js.map +1 -1
  69. package/dist/server/controllers/permissions.js +5 -1
  70. package/dist/server/controllers/permissions.js.map +1 -1
  71. package/dist/server/controllers/role.js +7 -2
  72. package/dist/server/controllers/role.js.map +1 -1
  73. package/dist/server/controllers/settings.js +7 -2
  74. package/dist/server/controllers/settings.js.map +1 -1
  75. package/dist/server/controllers/user.js +7 -2
  76. package/dist/server/controllers/user.js.map +1 -1
  77. package/dist/server/controllers/validation/auth.js +5 -1
  78. package/dist/server/controllers/validation/auth.js.map +1 -1
  79. package/dist/server/controllers/validation/email-template.js +7 -2
  80. package/dist/server/controllers/validation/email-template.js.map +1 -1
  81. package/dist/server/controllers/validation/user.js +21 -10
  82. package/dist/server/controllers/validation/user.js.map +1 -1
  83. package/dist/server/controllers/validation/user.mjs +16 -9
  84. package/dist/server/controllers/validation/user.mjs.map +1 -1
  85. package/dist/server/graphql/mutations/auth/change-password.js +5 -1
  86. package/dist/server/graphql/mutations/auth/change-password.js.map +1 -1
  87. package/dist/server/graphql/mutations/auth/email-confirmation.js +5 -1
  88. package/dist/server/graphql/mutations/auth/email-confirmation.js.map +1 -1
  89. package/dist/server/graphql/mutations/auth/forgot-password.js +5 -1
  90. package/dist/server/graphql/mutations/auth/forgot-password.js.map +1 -1
  91. package/dist/server/graphql/mutations/auth/login.js +5 -1
  92. package/dist/server/graphql/mutations/auth/login.js.map +1 -1
  93. package/dist/server/graphql/mutations/auth/register.js +5 -1
  94. package/dist/server/graphql/mutations/auth/register.js.map +1 -1
  95. package/dist/server/graphql/mutations/auth/reset-password.js +5 -1
  96. package/dist/server/graphql/mutations/auth/reset-password.js.map +1 -1
  97. package/dist/server/graphql/mutations/crud/role/create-role.js +5 -1
  98. package/dist/server/graphql/mutations/crud/role/create-role.js.map +1 -1
  99. package/dist/server/graphql/mutations/crud/user/create-user.js +5 -1
  100. package/dist/server/graphql/mutations/crud/user/create-user.js.map +1 -1
  101. package/dist/server/graphql/mutations/crud/user/update-user.js +5 -1
  102. package/dist/server/graphql/mutations/crud/user/update-user.js.map +1 -1
  103. package/dist/server/graphql/utils.js +5 -1
  104. package/dist/server/graphql/utils.js.map +1 -1
  105. package/dist/server/middlewares/rateLimit.js +11 -4
  106. package/dist/server/middlewares/rateLimit.js.map +1 -1
  107. package/dist/server/register.js +7 -2
  108. package/dist/server/register.js.map +1 -1
  109. package/dist/server/routes/content-api/auth.js +16 -0
  110. package/dist/server/routes/content-api/auth.js.map +1 -1
  111. package/dist/server/routes/content-api/auth.mjs +16 -0
  112. package/dist/server/routes/content-api/auth.mjs.map +1 -1
  113. package/dist/server/routes/content-api/index.js +5 -1
  114. package/dist/server/routes/content-api/index.js.map +1 -1
  115. package/dist/server/routes/content-api/user.js +5 -1
  116. package/dist/server/routes/content-api/user.js.map +1 -1
  117. package/dist/server/routes/content-api/validation.js +36 -4
  118. package/dist/server/routes/content-api/validation.js.map +1 -1
  119. package/dist/server/routes/content-api/validation.mjs +29 -2
  120. package/dist/server/routes/content-api/validation.mjs.map +1 -1
  121. package/dist/server/services/jwt.js +15 -8
  122. package/dist/server/services/jwt.js.map +1 -1
  123. package/dist/server/services/jwt.mjs +8 -6
  124. package/dist/server/services/jwt.mjs.map +1 -1
  125. package/dist/server/services/providers-registry.js +13 -5
  126. package/dist/server/services/providers-registry.js.map +1 -1
  127. package/dist/server/services/providers.js +7 -2
  128. package/dist/server/services/providers.js.map +1 -1
  129. package/dist/server/services/role.js +7 -2
  130. package/dist/server/services/role.js.map +1 -1
  131. package/dist/server/services/user.js +13 -5
  132. package/dist/server/services/user.js.map +1 -1
  133. package/dist/server/services/users-permissions.js +19 -4
  134. package/dist/server/services/users-permissions.js.map +1 -1
  135. package/dist/server/services/users-permissions.mjs +8 -0
  136. package/dist/server/services/users-permissions.mjs.map +1 -1
  137. package/dist/server/strategies/users-permissions.js +15 -3
  138. package/dist/server/strategies/users-permissions.js.map +1 -1
  139. package/dist/server/strategies/users-permissions.mjs +8 -1
  140. package/dist/server/strategies/users-permissions.mjs.map +1 -1
  141. package/dist/server/utils/index.js +5 -1
  142. package/dist/server/utils/index.js.map +1 -1
  143. package/dist/server/utils/refresh-cookie-options.js +27 -0
  144. package/dist/server/utils/refresh-cookie-options.js.map +1 -0
  145. package/dist/server/utils/refresh-cookie-options.mjs +25 -0
  146. package/dist/server/utils/refresh-cookie-options.mjs.map +1 -0
  147. package/dist/server/utils/sanitize/sanitizers.js +7 -2
  148. package/dist/server/utils/sanitize/sanitizers.js.map +1 -1
  149. package/lint-staged.config.mjs +1 -0
  150. package/package.json +5 -5
  151. package/rollup.config.mjs +3 -3
  152. package/server/controllers/auth.js +162 -141
  153. package/server/controllers/validation/user.js +22 -9
  154. package/server/routes/content-api/auth.js +12 -0
  155. package/server/routes/content-api/validation.js +32 -2
  156. package/server/services/jwt.js +4 -3
  157. package/server/services/users-permissions.js +2 -0
  158. package/server/strategies/users-permissions.js +6 -1
  159. package/server/utils/refresh-cookie-options.js +20 -0
@@ -12,6 +12,7 @@ const _ = require('lodash');
12
12
  const { concat, compact, isArray } = require('lodash/fp');
13
13
  const utils = require('@strapi/utils');
14
14
  const { getService } = require('../utils');
15
+ const { buildRefreshCookieOptions } = require('../utils/refresh-cookie-options');
15
16
  const {
16
17
  validateCallbackBody,
17
18
  validateRegisterBody,
@@ -23,6 +24,7 @@ const {
23
24
  } = require('./validation/auth');
24
25
 
25
26
  const { ApplicationError, ValidationError, ForbiddenError } = utils.errors;
27
+ const { buildSessionMetadata, sanitizeSessionEntry, sortSessionsForDisplay } = utils;
26
28
 
27
29
  const sanitizeUser = (user, ctx) => {
28
30
  const { auth } = ctx.state;
@@ -37,6 +39,102 @@ const extractDeviceId = (requestBody) => {
37
39
  return typeof deviceId === 'string' && deviceId.length > 0 ? deviceId : undefined;
38
40
  };
39
41
 
42
+ const buildSessionMetadataFromContext = (ctx) =>
43
+ buildSessionMetadata({
44
+ userAgent: ctx.request.headers['user-agent'],
45
+ });
46
+
47
+ const sendRefreshAuthResponse = async (strapi, ctx, user, { metadata } = {}) => {
48
+ const deviceId = extractDeviceId(ctx.request.body);
49
+ const tokenOptions = { type: 'refresh', ...(metadata ? { metadata } : {}) };
50
+
51
+ const refresh = await strapi
52
+ .sessionManager('users-permissions')
53
+ .generateRefreshToken(String(user.id), deviceId, tokenOptions);
54
+
55
+ const access = await strapi
56
+ .sessionManager('users-permissions')
57
+ .generateAccessToken(refresh.token);
58
+ if ('error' in access) {
59
+ throw new ApplicationError('Invalid credentials');
60
+ }
61
+
62
+ const upSessions = strapi.config.get('plugin::users-permissions.sessions');
63
+ const requestHttpOnly = ctx.request.header['x-strapi-refresh-cookie'] === 'httpOnly';
64
+
65
+ if (upSessions?.httpOnly || requestHttpOnly) {
66
+ const cookieName = upSessions.cookie?.name || 'strapi_up_refresh';
67
+ const isProduction = process.env.NODE_ENV === 'production';
68
+ ctx.cookies.set(cookieName, refresh.token, buildRefreshCookieOptions(upSessions, isProduction));
69
+ return ctx.send({ jwt: access.token, user: await sanitizeUser(user, ctx) });
70
+ }
71
+
72
+ return ctx.send({
73
+ jwt: access.token,
74
+ refreshToken: refresh.token,
75
+ user: await sanitizeUser(user, ctx),
76
+ });
77
+ };
78
+
79
+ const reissueTokensAfterPasswordChange = async (strapi, ctx, user) => {
80
+ const deviceId = extractDeviceId(ctx.request.body);
81
+
82
+ await strapi.sessionManager('users-permissions').invalidateRefreshToken(String(user.id));
83
+
84
+ const newDeviceId = deviceId || crypto.randomUUID();
85
+ const refresh = await strapi
86
+ .sessionManager('users-permissions')
87
+ .generateRefreshToken(String(user.id), newDeviceId, { type: 'refresh' });
88
+
89
+ const access = await strapi
90
+ .sessionManager('users-permissions')
91
+ .generateAccessToken(refresh.token);
92
+ if ('error' in access) {
93
+ throw new ApplicationError('Invalid credentials');
94
+ }
95
+
96
+ return ctx.send({
97
+ jwt: access.token,
98
+ refreshToken: refresh.token,
99
+ user: await sanitizeUser(user, ctx),
100
+ });
101
+ };
102
+
103
+ const revokeLogoutSessions = async (strapi, ctx, userId, { scope, deviceId, body }) => {
104
+ const sessionManager = strapi.sessionManager('users-permissions');
105
+ const upSessions = strapi.config.get('plugin::users-permissions.sessions');
106
+
107
+ if (scope === 'all') {
108
+ await sessionManager.invalidateRefreshToken(userId);
109
+ return;
110
+ }
111
+
112
+ if (deviceId) {
113
+ await sessionManager.invalidateRefreshToken(userId, deviceId);
114
+ return;
115
+ }
116
+
117
+ let currentSessionId = ctx.state.session?.id;
118
+ const cookieName = upSessions?.cookie?.name || 'strapi_up_refresh';
119
+ const refreshToken =
120
+ ctx.cookies.get(cookieName) ||
121
+ (typeof body.refreshToken === 'string' ? body.refreshToken : undefined);
122
+
123
+ if (refreshToken) {
124
+ const validation = await sessionManager.validateRefreshToken(refreshToken);
125
+ if (validation.isValid) {
126
+ currentSessionId = validation.sessionId;
127
+ }
128
+ }
129
+
130
+ if (currentSessionId) {
131
+ await sessionManager.revokeSessionById(userId, currentSessionId);
132
+ return;
133
+ }
134
+
135
+ await sessionManager.invalidateRefreshToken(userId);
136
+ };
137
+
40
138
  module.exports = ({ strapi }) => ({
41
139
  async callback(ctx) {
42
140
  const provider = ctx.params.provider || 'local';
@@ -94,46 +192,8 @@ module.exports = ({ strapi }) => ({
94
192
 
95
193
  const mode = strapi.config.get('plugin::users-permissions.jwtManagement', 'legacy-support');
96
194
  if (mode === 'refresh') {
97
- const deviceId = extractDeviceId(ctx.request.body);
98
-
99
- const refresh = await strapi
100
- .sessionManager('users-permissions')
101
- .generateRefreshToken(String(user.id), deviceId, { type: 'refresh' });
102
-
103
- const access = await strapi
104
- .sessionManager('users-permissions')
105
- .generateAccessToken(refresh.token);
106
- if ('error' in access) {
107
- throw new ApplicationError('Invalid credentials');
108
- }
109
-
110
- const upSessions = strapi.config.get('plugin::users-permissions.sessions');
111
- const requestHttpOnly = ctx.request.header['x-strapi-refresh-cookie'] === 'httpOnly';
112
- if (upSessions?.httpOnly || requestHttpOnly) {
113
- const cookieName = upSessions.cookie?.name || 'strapi_up_refresh';
114
- const isProduction = process.env.NODE_ENV === 'production';
115
- const isSecure =
116
- typeof upSessions.cookie?.secure === 'boolean'
117
- ? upSessions.cookie?.secure
118
- : isProduction;
119
-
120
- const cookieOptions = {
121
- httpOnly: true,
122
- secure: isSecure,
123
- sameSite: upSessions.cookie?.sameSite ?? 'lax',
124
- path: upSessions.cookie?.path ?? '/',
125
- domain: upSessions.cookie?.domain,
126
- overwrite: true,
127
- };
128
-
129
- ctx.cookies.set(cookieName, refresh.token, cookieOptions);
130
- return ctx.send({ jwt: access.token, user: await sanitizeUser(user, ctx) });
131
- }
132
-
133
- return ctx.send({
134
- jwt: access.token,
135
- refreshToken: refresh.token,
136
- user: await sanitizeUser(user, ctx),
195
+ return sendRefreshAuthResponse(strapi, ctx, user, {
196
+ metadata: buildSessionMetadataFromContext(ctx),
137
197
  });
138
198
  }
139
199
 
@@ -153,44 +213,8 @@ module.exports = ({ strapi }) => ({
153
213
 
154
214
  const mode = strapi.config.get('plugin::users-permissions.jwtManagement', 'legacy-support');
155
215
  if (mode === 'refresh') {
156
- const deviceId = extractDeviceId(ctx.request.body);
157
-
158
- const refresh = await strapi
159
- .sessionManager('users-permissions')
160
- .generateRefreshToken(String(user.id), deviceId, { type: 'refresh' });
161
-
162
- const access = await strapi
163
- .sessionManager('users-permissions')
164
- .generateAccessToken(refresh.token);
165
- if ('error' in access) {
166
- throw new ApplicationError('Invalid credentials');
167
- }
168
-
169
- const upSessions = strapi.config.get('plugin::users-permissions.sessions');
170
- const requestHttpOnly = ctx.request.header['x-strapi-refresh-cookie'] === 'httpOnly';
171
- if (upSessions?.httpOnly || requestHttpOnly) {
172
- const cookieName = upSessions.cookie?.name || 'strapi_up_refresh';
173
- const isProduction = process.env.NODE_ENV === 'production';
174
- const isSecure =
175
- typeof upSessions.cookie?.secure === 'boolean'
176
- ? upSessions.cookie?.secure
177
- : isProduction;
178
-
179
- const cookieOptions = {
180
- httpOnly: true,
181
- secure: isSecure,
182
- sameSite: upSessions.cookie?.sameSite ?? 'lax',
183
- path: upSessions.cookie?.path ?? '/',
184
- domain: upSessions.cookie?.domain,
185
- overwrite: true,
186
- };
187
- ctx.cookies.set(cookieName, refresh.token, cookieOptions);
188
- return ctx.send({ jwt: access.token, user: await sanitizeUser(user, ctx) });
189
- }
190
- return ctx.send({
191
- jwt: access.token,
192
- refreshToken: refresh.token,
193
- user: await sanitizeUser(user, ctx),
216
+ return sendRefreshAuthResponse(strapi, ctx, user, {
217
+ metadata: buildSessionMetadataFromContext(ctx),
194
218
  });
195
219
  }
196
220
 
@@ -233,28 +257,7 @@ module.exports = ({ strapi }) => ({
233
257
 
234
258
  const mode = strapi.config.get('plugin::users-permissions.jwtManagement', 'legacy-support');
235
259
  if (mode === 'refresh') {
236
- const deviceId = extractDeviceId(ctx.request.body);
237
-
238
- // Invalidate all sessions when password changes for security
239
- await strapi.sessionManager('users-permissions').invalidateRefreshToken(String(user.id));
240
-
241
- const newDeviceId = deviceId || crypto.randomUUID();
242
- const refresh = await strapi
243
- .sessionManager('users-permissions')
244
- .generateRefreshToken(String(user.id), newDeviceId, { type: 'refresh' });
245
-
246
- const access = await strapi
247
- .sessionManager('users-permissions')
248
- .generateAccessToken(refresh.token);
249
- if ('error' in access) {
250
- throw new ApplicationError('Invalid credentials');
251
- }
252
-
253
- return ctx.send({
254
- jwt: access.token,
255
- refreshToken: refresh.token,
256
- user: await sanitizeUser(user, ctx),
257
- });
260
+ return reissueTokensAfterPasswordChange(strapi, ctx, user);
258
261
  }
259
262
 
260
263
  return ctx.send({
@@ -290,28 +293,7 @@ module.exports = ({ strapi }) => ({
290
293
 
291
294
  const mode = strapi.config.get('plugin::users-permissions.jwtManagement', 'legacy-support');
292
295
  if (mode === 'refresh') {
293
- const deviceId = extractDeviceId(ctx.request.body);
294
-
295
- // Invalidate all sessions when password is reset for security
296
- await strapi.sessionManager('users-permissions').invalidateRefreshToken(String(user.id));
297
-
298
- const newDeviceId = deviceId || crypto.randomUUID();
299
- const refresh = await strapi
300
- .sessionManager('users-permissions')
301
- .generateRefreshToken(String(user.id), newDeviceId, { type: 'refresh' });
302
-
303
- const access = await strapi
304
- .sessionManager('users-permissions')
305
- .generateAccessToken(refresh.token);
306
- if ('error' in access) {
307
- throw new ApplicationError('Invalid credentials');
308
- }
309
-
310
- return ctx.send({
311
- jwt: access.token,
312
- refreshToken: refresh.token,
313
- user: await sanitizeUser(user, ctx),
314
- });
296
+ return reissueTokensAfterPasswordChange(strapi, ctx, user);
315
297
  }
316
298
 
317
299
  return ctx.send({
@@ -355,18 +337,11 @@ module.exports = ({ strapi }) => ({
355
337
  const requestHttpOnly = ctx.request.header['x-strapi-refresh-cookie'] === 'httpOnly';
356
338
  if (upSessions?.httpOnly || requestHttpOnly) {
357
339
  const isProduction = process.env.NODE_ENV === 'production';
358
- const isSecure =
359
- typeof upSessions.cookie?.secure === 'boolean' ? upSessions.cookie?.secure : isProduction;
360
-
361
- const cookieOptions = {
362
- httpOnly: true,
363
- secure: isSecure,
364
- sameSite: upSessions.cookie?.sameSite ?? 'lax',
365
- path: upSessions.cookie?.path ?? '/',
366
- domain: upSessions.cookie?.domain,
367
- overwrite: true,
368
- };
369
- ctx.cookies.set(cookieName, rotation.token, cookieOptions);
340
+ ctx.cookies.set(
341
+ cookieName,
342
+ rotation.token,
343
+ buildRefreshCookieOptions(upSessions, isProduction)
344
+ );
370
345
  return ctx.send({ jwt: result.token });
371
346
  }
372
347
  return ctx.send({ jwt: result.token, refreshToken: rotation.token });
@@ -377,21 +352,22 @@ module.exports = ({ strapi }) => ({
377
352
  return ctx.notFound();
378
353
  }
379
354
 
380
- // Invalidate all sessions for the authenticated user, or by deviceId if provided
381
355
  if (!ctx.state.user) {
382
356
  return ctx.unauthorized('Missing authentication');
383
357
  }
384
358
 
385
- const deviceId = extractDeviceId(ctx.request.body);
359
+ const userId = String(ctx.state.user.id);
360
+ const upSessions = strapi.config.get('plugin::users-permissions.sessions');
361
+ const body = ctx.request.body || {};
362
+ const scope = typeof body.scope === 'string' ? body.scope : undefined;
363
+ const deviceId = extractDeviceId(body);
364
+
386
365
  try {
387
- await strapi
388
- .sessionManager('users-permissions')
389
- .invalidateRefreshToken(String(ctx.state.user.id), deviceId);
366
+ await revokeLogoutSessions(strapi, ctx, userId, { scope, deviceId, body });
390
367
  } catch (err) {
391
368
  strapi.log.error('UP logout failed', err);
392
369
  }
393
370
 
394
- const upSessions = strapi.config.get('plugin::users-permissions.sessions');
395
371
  const requestHttpOnly = ctx.request.header['x-strapi-refresh-cookie'] === 'httpOnly';
396
372
  if (upSessions?.httpOnly || requestHttpOnly) {
397
373
  const cookieName = upSessions.cookie?.name || 'strapi_up_refresh';
@@ -399,6 +375,48 @@ module.exports = ({ strapi }) => ({
399
375
  }
400
376
  return ctx.send({ ok: true });
401
377
  },
378
+ async getSessions(ctx) {
379
+ const mode = strapi.config.get('plugin::users-permissions.jwtManagement', 'legacy-support');
380
+ if (mode !== 'refresh') {
381
+ return ctx.notFound();
382
+ }
383
+
384
+ if (!ctx.state.user) {
385
+ return ctx.unauthorized('Missing authentication');
386
+ }
387
+
388
+ const currentSessionId = ctx.state.session?.id;
389
+ const sessions = await strapi
390
+ .sessionManager('users-permissions')
391
+ .listSessions(String(ctx.state.user.id));
392
+
393
+ const data = sortSessionsForDisplay(
394
+ sessions.map((session) => sanitizeSessionEntry(session, currentSessionId))
395
+ );
396
+
397
+ return ctx.send({ data });
398
+ },
399
+ async revokeSession(ctx) {
400
+ const mode = strapi.config.get('plugin::users-permissions.jwtManagement', 'legacy-support');
401
+ if (mode !== 'refresh') {
402
+ return ctx.notFound();
403
+ }
404
+
405
+ if (!ctx.state.user) {
406
+ return ctx.unauthorized('Missing authentication');
407
+ }
408
+
409
+ const { sessionId } = ctx.params;
410
+ const revoked = await strapi
411
+ .sessionManager('users-permissions')
412
+ .revokeSessionById(String(ctx.state.user.id), sessionId);
413
+
414
+ if (!revoked) {
415
+ return ctx.notFound('Session not found');
416
+ }
417
+
418
+ return ctx.send({ data: {} });
419
+ },
402
420
  async connect(ctx, next) {
403
421
  const grant = require('grant').koa();
404
422
 
@@ -617,7 +635,10 @@ module.exports = ({ strapi }) => ({
617
635
 
618
636
  const refresh = await strapi
619
637
  .sessionManager('users-permissions')
620
- .generateRefreshToken(String(user.id), deviceId, { type: 'refresh' });
638
+ .generateRefreshToken(String(user.id), deviceId, {
639
+ type: 'refresh',
640
+ metadata: buildSessionMetadataFromContext(ctx),
641
+ });
621
642
 
622
643
  const access = await strapi
623
644
  .sessionManager('users-permissions')
@@ -6,6 +6,17 @@ const deleteRoleSchema = yup.object().shape({
6
6
  role: yup.strapiID().required(),
7
7
  });
8
8
 
9
+ // A role relation entry may be referenced by numeric `id` (legacy) or by
10
+ // `documentId` (the v5 default), but must carry at least one of them.
11
+ const roleRelationEntrySchema = yup
12
+ .object()
13
+ .shape({ id: yup.strapiID(), documentId: yup.strapiID() })
14
+ .test(
15
+ 'id-or-documentId',
16
+ 'Relation entry must include an id or documentId',
17
+ (entry) => !!entry && (entry.id != null || entry.documentId != null)
18
+ );
19
+
9
20
  const createUserBodySchema = yup.object().shape({
10
21
  email: yup.string().email().required(),
11
22
  username: yup.string().min(1).required(),
@@ -17,7 +28,7 @@ const createUserBodySchema = yup.object().shape({
17
28
  .shape({
18
29
  connect: yup
19
30
  .array()
20
- .of(yup.object().shape({ id: yup.strapiID().required() }))
31
+ .of(roleRelationEntrySchema)
21
32
  .min(1, 'Users must have a role')
22
33
  .required(),
23
34
  })
@@ -44,20 +55,22 @@ const updateUserBodySchema = yup.object().shape({
44
55
  role: yup.lazy((value) =>
45
56
  typeof value === 'object'
46
57
  ? yup.object().shape({
47
- connect: yup
48
- .array()
49
- .of(yup.object().shape({ id: yup.strapiID().required() }))
50
- .required(),
58
+ // connect/disconnect are each optional (matching core relation inputs),
59
+ // but a role must remain: reject disconnecting every role without
60
+ // connecting a replacement.
61
+ connect: yup.array().of(roleRelationEntrySchema),
51
62
  disconnect: yup
52
63
  .array()
53
- .test('CheckDisconnect', 'Cannot remove role', function test(disconnectValue) {
54
- if (value.connect.length === 0 && disconnectValue.length > 0) {
64
+ .of(roleRelationEntrySchema)
65
+ .test('CheckDisconnect', 'Cannot remove role', function test(disconnect) {
66
+ const connectValue = value.connect || [];
67
+ const disconnectValue = disconnect || [];
68
+ if (connectValue.length === 0 && disconnectValue.length > 0) {
55
69
  return false;
56
70
  }
57
71
 
58
72
  return true;
59
- })
60
- .required(),
73
+ }),
61
74
  })
62
75
  : yup.strapiID()
63
76
  ),
@@ -127,5 +127,17 @@ module.exports = (strapi) => {
127
127
  handler: 'auth.logout',
128
128
  config: { prefix: '' },
129
129
  },
130
+ {
131
+ method: 'GET',
132
+ path: '/auth/sessions',
133
+ handler: 'auth.getSessions',
134
+ config: { prefix: '' },
135
+ },
136
+ {
137
+ method: 'DELETE',
138
+ path: '/auth/sessions/:sessionId',
139
+ handler: 'auth.revokeSession',
140
+ config: { prefix: '' },
141
+ },
130
142
  ];
131
143
  };
@@ -3,6 +3,36 @@
3
3
  const { AbstractRouteValidator } = require('@strapi/utils');
4
4
  const z = require('zod/v4');
5
5
 
6
+ // A single role relation entry, referenced by numeric `id` (legacy) or by
7
+ // `documentId` (the v5 default), but must carry at least one of them. The
8
+ // detailed rules (min one role on create, cannot remove the last role on
9
+ // update) stay in the Yup controller validator.
10
+ const roleRelationEntry = z
11
+ .object({
12
+ id: z.union([z.number(), z.string()]).optional(),
13
+ documentId: z.string().optional(),
14
+ })
15
+ .refine((entry) => entry.id != null || entry.documentId != null, {
16
+ message: 'Relation entry must include an id or documentId',
17
+ });
18
+
19
+ // The `role` relation input: shorthand scalar (numeric id or documentId) or the
20
+ // longhand connect/disconnect object, matching every other v5 relation. The
21
+ // object form must carry at least one of connect/disconnect (an empty object is
22
+ // a no-op and almost certainly a mistake).
23
+ const roleRelationInput = z.union([
24
+ z.number(),
25
+ z.string(),
26
+ z
27
+ .object({
28
+ connect: z.array(roleRelationEntry).optional(),
29
+ disconnect: z.array(roleRelationEntry).optional(),
30
+ })
31
+ .refine((input) => input.connect != null || input.disconnect != null, {
32
+ message: 'Relation input must include connect or disconnect',
33
+ }),
34
+ ]);
35
+
6
36
  class UsersPermissionsRouteValidator extends AbstractRouteValidator {
7
37
  constructor(strapi) {
8
38
  super();
@@ -201,7 +231,7 @@ class UsersPermissionsRouteValidator extends AbstractRouteValidator {
201
231
  username: z.string(),
202
232
  email: z.email(),
203
233
  password: z.string(),
204
- role: z.number().optional(),
234
+ role: roleRelationInput.optional(),
205
235
  });
206
236
  }
207
237
 
@@ -210,7 +240,7 @@ class UsersPermissionsRouteValidator extends AbstractRouteValidator {
210
240
  username: z.string().optional(),
211
241
  email: z.email().optional(),
212
242
  password: z.string().optional(),
213
- role: z.number().optional(),
243
+ role: roleRelationInput.optional(),
214
244
  });
215
245
  }
216
246
 
@@ -80,17 +80,18 @@ module.exports = ({ strapi }) => ({
80
80
  throw new Error('Invalid token.');
81
81
  }
82
82
 
83
- return { id: user.id };
83
+ // Surface the sessionId so the strategy can flag the "current" session.
84
+ return { id: user.id, sessionId: result.payload.sessionId };
84
85
  }
85
86
 
86
87
  return new Promise((resolve, reject) => {
87
88
  const jwtConfig = strapi.config.get('plugin::users-permissions.jwt', {});
88
- const algorithms = jwtConfig && jwtConfig.algorithm ? [jwtConfig.algorithm] : undefined;
89
+ const algorithms = [jwtConfig?.algorithm || 'HS256'];
89
90
 
90
91
  jwt.verify(
91
92
  token,
92
93
  strapi.config.get('plugin::users-permissions.jwtSecret'),
93
- algorithms ? { algorithms } : {},
94
+ { algorithms },
94
95
  (err, tokenPayload = {}) => {
95
96
  if (err) {
96
97
  return reject(new Error('Invalid token.'));
@@ -22,6 +22,8 @@ const DEFAULT_PERMISSIONS = [
22
22
  { action: 'plugin::users-permissions.auth.sendEmailConfirmation', roleType: 'public' },
23
23
  { action: 'plugin::users-permissions.auth.refresh', roleType: 'public' },
24
24
  { action: 'plugin::users-permissions.auth.logout', roleType: 'authenticated' },
25
+ { action: 'plugin::users-permissions.auth.getSessions', roleType: 'authenticated' },
26
+ { action: 'plugin::users-permissions.auth.revokeSession', roleType: 'authenticated' },
25
27
  { action: 'plugin::users-permissions.user.me', roleType: 'authenticated' },
26
28
  { action: 'plugin::users-permissions.auth.changePassword', roleType: 'authenticated' },
27
29
  ];
@@ -14,7 +14,7 @@ const authenticate = async (ctx) => {
14
14
  const token = await getService('jwt').getToken(ctx);
15
15
 
16
16
  if (token) {
17
- const { id } = token;
17
+ const { id, sessionId } = token;
18
18
 
19
19
  // Invalid token
20
20
  if (id === undefined) {
@@ -49,6 +49,11 @@ const authenticate = async (ctx) => {
49
49
  const ability = await strapi.contentAPI.permissions.engine.generateAbility(permissions);
50
50
 
51
51
  ctx.state.user = user;
52
+ // Expose the session backing this request (refresh mode only) so endpoints
53
+ // can flag the "current" session when listing active sessions.
54
+ if (sessionId) {
55
+ ctx.state.session = { id: sessionId };
56
+ }
52
57
 
53
58
  return {
54
59
  authenticated: true,
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const buildRefreshCookieOptions = (upSessions, isProduction) => {
4
+ const isSecure =
5
+ typeof upSessions.cookie?.secure === 'boolean' ? upSessions.cookie?.secure : isProduction;
6
+
7
+ return {
8
+ httpOnly: true,
9
+ secure: isSecure,
10
+ sameSite: upSessions.cookie?.sameSite ?? 'lax',
11
+ path: upSessions.cookie?.path ?? '/',
12
+ domain: upSessions.cookie?.domain,
13
+ maxAge: upSessions.cookie?.maxAge,
14
+ overwrite: true,
15
+ };
16
+ };
17
+
18
+ module.exports = {
19
+ buildRefreshCookieOptions,
20
+ };