@strapi/plugin-users-permissions 5.49.0 → 5.50.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/.eslintrc.cjs +1 -0
- package/admin/src/pages/AdvancedSettings/index.jsx +1 -1
- package/admin/src/pages/Roles/pages/ListPage/index.jsx +1 -1
- package/admin/src/translations/ja.json +46 -7
- package/dist/admin/pages/AdvancedSettings/index.js +1 -1
- package/dist/admin/pages/AdvancedSettings/index.js.map +1 -1
- package/dist/admin/pages/AdvancedSettings/index.mjs +1 -1
- package/dist/admin/pages/AdvancedSettings/index.mjs.map +1 -1
- package/dist/admin/pages/Roles/pages/ListPage/index.js +1 -1
- package/dist/admin/pages/Roles/pages/ListPage/index.js.map +1 -1
- package/dist/admin/pages/Roles/pages/ListPage/index.mjs +1 -1
- package/dist/admin/pages/Roles/pages/ListPage/index.mjs.map +1 -1
- package/dist/admin/translations/ja.json.js +46 -7
- package/dist/admin/translations/ja.json.js.map +1 -1
- package/dist/admin/translations/ja.json.mjs +46 -7
- package/dist/admin/translations/ja.json.mjs.map +1 -1
- package/dist/server/controllers/auth.js +133 -114
- package/dist/server/controllers/auth.js.map +1 -1
- package/dist/server/controllers/auth.mjs +132 -113
- package/dist/server/controllers/auth.mjs.map +1 -1
- package/dist/server/controllers/validation/auth.js +6 -6
- package/dist/server/controllers/validation/auth.js.map +1 -1
- package/dist/server/controllers/validation/auth.mjs +6 -6
- package/dist/server/controllers/validation/auth.mjs.map +1 -1
- package/dist/server/controllers/validation/user.js +16 -9
- package/dist/server/controllers/validation/user.js.map +1 -1
- package/dist/server/controllers/validation/user.mjs +16 -9
- package/dist/server/controllers/validation/user.mjs.map +1 -1
- package/dist/server/routes/content-api/auth.js +16 -0
- package/dist/server/routes/content-api/auth.js.map +1 -1
- package/dist/server/routes/content-api/auth.mjs +16 -0
- package/dist/server/routes/content-api/auth.mjs.map +1 -1
- package/dist/server/routes/content-api/validation.js +29 -2
- package/dist/server/routes/content-api/validation.js.map +1 -1
- package/dist/server/routes/content-api/validation.mjs +29 -2
- package/dist/server/routes/content-api/validation.mjs.map +1 -1
- package/dist/server/services/jwt.js +8 -6
- package/dist/server/services/jwt.js.map +1 -1
- package/dist/server/services/jwt.mjs +8 -6
- package/dist/server/services/jwt.mjs.map +1 -1
- package/dist/server/services/users-permissions.js +8 -0
- package/dist/server/services/users-permissions.js.map +1 -1
- package/dist/server/services/users-permissions.mjs +8 -0
- package/dist/server/services/users-permissions.mjs.map +1 -1
- package/dist/server/strategies/users-permissions.js +8 -1
- package/dist/server/strategies/users-permissions.js.map +1 -1
- package/dist/server/strategies/users-permissions.mjs +8 -1
- package/dist/server/strategies/users-permissions.mjs.map +1 -1
- package/dist/server/utils/refresh-cookie-options.js +27 -0
- package/dist/server/utils/refresh-cookie-options.js.map +1 -0
- package/dist/server/utils/refresh-cookie-options.mjs +25 -0
- package/dist/server/utils/refresh-cookie-options.mjs.map +1 -0
- package/lint-staged.config.mjs +1 -0
- package/package.json +5 -5
- package/rollup.config.mjs +3 -3
- package/server/controllers/auth.js +162 -141
- package/server/controllers/validation/auth.js +6 -6
- package/server/controllers/validation/user.js +22 -9
- package/server/routes/content-api/auth.js +12 -0
- package/server/routes/content-api/validation.js +32 -2
- package/server/services/jwt.js +4 -3
- package/server/services/users-permissions.js +2 -0
- package/server/strategies/users-permissions.js +6 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
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, {
|
|
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')
|
|
@@ -14,7 +14,7 @@ const createRegisterSchema = (config) =>
|
|
|
14
14
|
password: yup
|
|
15
15
|
.string()
|
|
16
16
|
.required()
|
|
17
|
-
.test(function (value) {
|
|
17
|
+
.test(function validateRegisterPasswordMaxLength(value) {
|
|
18
18
|
if (!value) return true;
|
|
19
19
|
const isValid = new TextEncoder().encode(value).length <= 72;
|
|
20
20
|
if (!isValid) {
|
|
@@ -22,7 +22,7 @@ const createRegisterSchema = (config) =>
|
|
|
22
22
|
}
|
|
23
23
|
return true;
|
|
24
24
|
})
|
|
25
|
-
.test(async function (value) {
|
|
25
|
+
.test(async function validateRegisterPassword(value) {
|
|
26
26
|
if (typeof config?.validatePassword === 'function') {
|
|
27
27
|
try {
|
|
28
28
|
const isValid = await config.validatePassword(value);
|
|
@@ -57,7 +57,7 @@ const createResetPasswordSchema = (config) =>
|
|
|
57
57
|
password: yup
|
|
58
58
|
.string()
|
|
59
59
|
.required()
|
|
60
|
-
.test(function (value) {
|
|
60
|
+
.test(function validateResetPasswordMaxLength(value) {
|
|
61
61
|
if (!value) return true;
|
|
62
62
|
const isValid = new TextEncoder().encode(value).length <= 72;
|
|
63
63
|
if (!isValid) {
|
|
@@ -65,7 +65,7 @@ const createResetPasswordSchema = (config) =>
|
|
|
65
65
|
}
|
|
66
66
|
return true;
|
|
67
67
|
})
|
|
68
|
-
.test(async function (value) {
|
|
68
|
+
.test(async function validateResetPassword(value) {
|
|
69
69
|
if (typeof config?.validatePassword === 'function') {
|
|
70
70
|
try {
|
|
71
71
|
const isValid = await config.validatePassword(value);
|
|
@@ -93,7 +93,7 @@ const createChangePasswordSchema = (config) =>
|
|
|
93
93
|
password: yup
|
|
94
94
|
.string()
|
|
95
95
|
.required()
|
|
96
|
-
.test(function (value) {
|
|
96
|
+
.test(function validateChangePasswordMaxLength(value) {
|
|
97
97
|
if (!value) return true;
|
|
98
98
|
const isValid = new TextEncoder().encode(value).length <= 72;
|
|
99
99
|
if (!isValid) {
|
|
@@ -101,7 +101,7 @@ const createChangePasswordSchema = (config) =>
|
|
|
101
101
|
}
|
|
102
102
|
return true;
|
|
103
103
|
})
|
|
104
|
-
.test(async function (value) {
|
|
104
|
+
.test(async function validateChangePassword(value) {
|
|
105
105
|
if (typeof config?.validatePassword === 'function') {
|
|
106
106
|
try {
|
|
107
107
|
const isValid = await config.validatePassword(value);
|
|
@@ -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(
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
.
|
|
54
|
-
|
|
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:
|
|
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:
|
|
243
|
+
role: roleRelationInput.optional(),
|
|
214
244
|
});
|
|
215
245
|
}
|
|
216
246
|
|
package/server/services/jwt.js
CHANGED
|
@@ -80,17 +80,18 @@ module.exports = ({ strapi }) => ({
|
|
|
80
80
|
throw new Error('Invalid token.');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|