@strapi/plugin-users-permissions 5.52.2 → 5.53.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 (66) hide show
  1. package/dist/server/controllers/auth.js +8 -2
  2. package/dist/server/controllers/auth.js.map +1 -1
  3. package/dist/server/controllers/auth.mjs +8 -2
  4. package/dist/server/controllers/auth.mjs.map +1 -1
  5. package/dist/server/graphql/mutations/auth/change-password.js +6 -3
  6. package/dist/server/graphql/mutations/auth/change-password.js.map +1 -1
  7. package/dist/server/graphql/mutations/auth/change-password.mjs +6 -3
  8. package/dist/server/graphql/mutations/auth/change-password.mjs.map +1 -1
  9. package/dist/server/graphql/mutations/auth/forgot-password.js +6 -3
  10. package/dist/server/graphql/mutations/auth/forgot-password.js.map +1 -1
  11. package/dist/server/graphql/mutations/auth/forgot-password.mjs +6 -3
  12. package/dist/server/graphql/mutations/auth/forgot-password.mjs.map +1 -1
  13. package/dist/server/graphql/mutations/auth/login.js +9 -6
  14. package/dist/server/graphql/mutations/auth/login.js.map +1 -1
  15. package/dist/server/graphql/mutations/auth/login.mjs +9 -6
  16. package/dist/server/graphql/mutations/auth/login.mjs.map +1 -1
  17. package/dist/server/graphql/mutations/auth/rate-limit.js +63 -0
  18. package/dist/server/graphql/mutations/auth/rate-limit.js.map +1 -0
  19. package/dist/server/graphql/mutations/auth/rate-limit.mjs +61 -0
  20. package/dist/server/graphql/mutations/auth/rate-limit.mjs.map +1 -0
  21. package/dist/server/graphql/mutations/auth/register.js +6 -3
  22. package/dist/server/graphql/mutations/auth/register.js.map +1 -1
  23. package/dist/server/graphql/mutations/auth/register.mjs +6 -3
  24. package/dist/server/graphql/mutations/auth/register.mjs.map +1 -1
  25. package/dist/server/graphql/mutations/auth/reset-password.js +6 -3
  26. package/dist/server/graphql/mutations/auth/reset-password.js.map +1 -1
  27. package/dist/server/graphql/mutations/auth/reset-password.mjs +6 -3
  28. package/dist/server/graphql/mutations/auth/reset-password.mjs.map +1 -1
  29. package/dist/server/middlewares/rateLimit.js +1 -1
  30. package/dist/server/middlewares/rateLimit.js.map +1 -1
  31. package/dist/server/middlewares/rateLimit.mjs +1 -1
  32. package/dist/server/middlewares/rateLimit.mjs.map +1 -1
  33. package/dist/server/services/providers-registry.js +80 -19
  34. package/dist/server/services/providers-registry.js.map +1 -1
  35. package/dist/server/services/providers-registry.mjs +80 -19
  36. package/dist/server/services/providers-registry.mjs.map +1 -1
  37. package/dist/server/services/providers.js +12 -8
  38. package/dist/server/services/providers.js.map +1 -1
  39. package/dist/server/services/providers.mjs +12 -8
  40. package/dist/server/services/providers.mjs.map +1 -1
  41. package/dist/server/services/user.js +7 -7
  42. package/dist/server/services/user.js.map +1 -1
  43. package/dist/server/services/user.mjs +7 -7
  44. package/dist/server/services/user.mjs.map +1 -1
  45. package/dist/server/utils/oauth-connect/index.js +11 -6
  46. package/dist/server/utils/oauth-connect/index.js.map +1 -1
  47. package/dist/server/utils/oauth-connect/index.mjs +11 -6
  48. package/dist/server/utils/oauth-connect/index.mjs.map +1 -1
  49. package/dist/server/utils/trim-grant-session-response.js +67 -0
  50. package/dist/server/utils/trim-grant-session-response.js.map +1 -0
  51. package/dist/server/utils/trim-grant-session-response.mjs +65 -0
  52. package/dist/server/utils/trim-grant-session-response.mjs.map +1 -0
  53. package/package.json +4 -4
  54. package/server/src/controllers/auth.js +13 -2
  55. package/server/src/graphql/mutations/auth/change-password.js +5 -5
  56. package/server/src/graphql/mutations/auth/forgot-password.js +5 -5
  57. package/server/src/graphql/mutations/auth/login.js +10 -6
  58. package/server/src/graphql/mutations/auth/rate-limit.js +62 -0
  59. package/server/src/graphql/mutations/auth/register.js +5 -5
  60. package/server/src/graphql/mutations/auth/reset-password.js +5 -5
  61. package/server/src/middlewares/rateLimit.js +1 -1
  62. package/server/src/services/providers-registry.js +99 -23
  63. package/server/src/services/providers.js +9 -7
  64. package/server/src/services/user.js +15 -5
  65. package/server/src/utils/oauth-connect/index.js +12 -6
  66. package/server/src/utils/trim-grant-session-response.js +70 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trim-grant-session-response.mjs","sources":["../../../server/src/utils/trim-grant-session-response.js"],"sourcesContent":["'use strict';\n\nconst compact = (value) => {\n if (value === null || value === undefined) {\n return undefined;\n }\n\n if (typeof value !== 'object' || Array.isArray(value)) {\n return value;\n }\n\n return Object.fromEntries(\n Object.entries(value)\n .map(([key, entry]) => [key, compact(entry)])\n .filter(([, entry]) => entry !== undefined)\n );\n};\n\n/**\n * Compact grant's OAuth token response before it is written to the signed\n * koa-session cookie. The full token exchange payload (especially Cognito's\n * dual JWTs plus a duplicated raw blob) can exceed the browser's ~4 KB\n * per-cookie limit and silently break the auth callback.\n *\n * Keep only fields that auth.callback / providers-registry actually read.\n */\nconst trimGrantSessionResponse = (grantResponse, provider) => {\n if (!grantResponse || typeof grantResponse !== 'object') {\n return grantResponse;\n }\n\n switch (provider) {\n case 'cognito':\n return compact({\n id_token: grantResponse.id_token,\n });\n\n case 'twitter':\n return compact({\n access_token: grantResponse.access_token,\n access_secret: grantResponse.access_secret,\n raw: grantResponse.raw?.screen_name\n ? { screen_name: grantResponse.raw.screen_name }\n : undefined,\n });\n\n case 'vk':\n return compact({\n access_token: grantResponse.access_token,\n raw:\n grantResponse.raw?.email != null && grantResponse.raw?.user_id != null\n ? {\n email: grantResponse.raw.email,\n user_id: grantResponse.raw.user_id,\n }\n : undefined,\n });\n\n default:\n return compact({\n access_token: grantResponse.access_token,\n oauth_token: grantResponse.oauth_token,\n id_token: grantResponse.id_token,\n });\n }\n};\n\nmodule.exports = {\n trimGrantSessionResponse,\n};\n"],"names":["compact","value","undefined","Array","isArray","Object","fromEntries","entries","map","key","entry","filter","trimGrantSessionResponse","grantResponse","provider","id_token","access_token","access_secret","raw","screen_name","email","user_id","oauth_token","trimGrantSessionResponse_1"],"mappings":";;;;;AAEA,IAAA,MAAMA,UAAU,CAACC,KAAAA,GAAAA;QACf,IAAIA,KAAAA,KAAU,IAAA,IAAQA,KAAAA,KAAUC,SAAAA,EAAW;YACzC,OAAOA,SAAAA;AACX,QAAA;AAEE,QAAA,IAAI,OAAOD,KAAAA,KAAU,QAAA,IAAYE,KAAAA,CAAMC,OAAO,CAACH,KAAAA,CAAAA,EAAQ;YACrD,OAAOA,KAAAA;AACX,QAAA;AAEE,QAAA,OAAOI,MAAAA,CAAOC,WAAW,CACvBD,MAAAA,CAAOE,OAAO,CAACN,KAAAA,CAAAA,CACZO,GAAG,CAAC,CAAC,CAACC,GAAAA,EAAKC,MAAM,GAAK;AAACD,gBAAAA,GAAAA;gBAAKT,OAAAA,CAAQU,KAAAA;AAAO,aAAA,CAAA,CAC3CC,MAAM,CAAC,CAAC,GAAGD,KAAAA,CAAM,GAAKA,KAAAA,KAAUR,SAAAA,CAAAA,CAAAA;AAEvC,IAAA,CAAA;AAEA;;;;;;;KAQA,MAAMU,wBAAAA,GAA2B,CAACC,aAAAA,EAAeC,QAAAA,GAAAA;AAC/C,QAAA,IAAI,CAACD,aAAAA,IAAiB,OAAOA,aAAAA,KAAkB,QAAA,EAAU;YACvD,OAAOA,aAAAA;AACX,QAAA;QAEE,OAAQC,QAAAA;YACN,KAAK,SAAA;AACH,gBAAA,OAAOd,OAAAA,CAAQ;AACbe,oBAAAA,QAAAA,EAAUF,cAAcE;AAChC,iBAAA,CAAA;YAEI,KAAK,SAAA;AACH,gBAAA,OAAOf,OAAAA,CAAQ;AACbgB,oBAAAA,YAAAA,EAAcH,cAAcG,YAAY;AACxCC,oBAAAA,aAAAA,EAAeJ,cAAcI,aAAa;oBAC1CC,GAAAA,EAAKL,aAAAA,CAAcK,GAAG,EAAEC,WAAAA,GACpB;wBAAEA,WAAAA,EAAaN,aAAAA,CAAcK,GAAG,CAACC;qBAAW,GAC5CjB;AACZ,iBAAA,CAAA;YAEI,KAAK,IAAA;AACH,gBAAA,OAAOF,OAAAA,CAAQ;AACbgB,oBAAAA,YAAAA,EAAcH,cAAcG,YAAY;oBACxCE,GAAAA,EACEL,aAAAA,CAAcK,GAAG,EAAEE,KAAAA,IAAS,QAAQP,aAAAA,CAAcK,GAAG,EAAEG,OAAAA,IAAW,IAAA,GAC9D;wBACED,KAAAA,EAAOP,aAAAA,CAAcK,GAAG,CAACE,KAAK;wBAC9BC,OAAAA,EAASR,aAAAA,CAAcK,GAAG,CAACG;qBAC3C,GACcnB;AACd,iBAAA,CAAA;AAEI,YAAA;AACE,gBAAA,OAAOF,OAAAA,CAAQ;AACbgB,oBAAAA,YAAAA,EAAcH,cAAcG,YAAY;AACxCM,oBAAAA,WAAAA,EAAaT,cAAcS,WAAW;AACtCP,oBAAAA,QAAAA,EAAUF,cAAcE;AAChC,iBAAA,CAAA;AACA;AACA,IAAA,CAAA;IAEAQ,0BAAAA,GAAiB;AACfX,QAAAA;AACF,KAAA;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/plugin-users-permissions",
3
- "version": "5.52.2",
3
+ "version": "5.53.0",
4
4
  "description": "Protect your API with a full-authentication process based on JWT",
5
5
  "homepage": "https://strapi.io",
6
6
  "bugs": {
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@strapi/design-system": "2.2.4",
57
57
  "@strapi/icons": "2.2.4",
58
- "@strapi/utils": "5.52.2",
58
+ "@strapi/utils": "5.53.0",
59
59
  "bcryptjs": "2.4.3",
60
60
  "formik": "2.4.9",
61
61
  "immer": "9.0.21",
@@ -72,8 +72,8 @@
72
72
  "zod": "4.4.3"
73
73
  },
74
74
  "devDependencies": {
75
- "@strapi/admin": "5.52.2",
76
- "@strapi/strapi": "5.52.2",
75
+ "@strapi/admin": "5.53.0",
76
+ "@strapi/strapi": "5.53.0",
77
77
  "@testing-library/dom": "10.4.1",
78
78
  "@testing-library/jest-dom": "6.9.1",
79
79
  "@testing-library/react": "16.3.2",
@@ -205,7 +205,15 @@ module.exports = ({ strapi }) => ({
205
205
 
206
206
  // Connect the user with the third-party provider.
207
207
  try {
208
- const user = await getService('providers').connect(provider, ctx.query);
208
+ const grantResponse = _.get(ctx, 'session.grant.response');
209
+
210
+ if (!grantResponse) {
211
+ throw new ApplicationError('OAuth authentication requires a completed provider session');
212
+ }
213
+
214
+ const user = await getService('providers').connect(provider, grantResponse, {
215
+ grantResponse,
216
+ });
209
217
 
210
218
  if (user.blocked) {
211
219
  throw new ForbiddenError('Your account has been blocked by an administrator');
@@ -373,7 +381,10 @@ module.exports = ({ strapi }) => ({
373
381
  const cookieName = upSessions.cookie?.name || 'strapi_up_refresh';
374
382
  const isProduction = process.env.NODE_ENV === 'production';
375
383
 
376
- const { maxAge, ...cookieOptions } = buildRefreshCookieOptions(upSessions, isProduction);
384
+ const { maxAge: _maxAge, ...cookieOptions } = buildRefreshCookieOptions(
385
+ upSessions,
386
+ isProduction
387
+ );
377
388
 
378
389
  ctx.cookies.set(cookieName, '', { ...cookieOptions, expires: new Date(0) });
379
390
  }
@@ -3,9 +3,11 @@
3
3
  const { toPlainObject } = require('lodash/fp');
4
4
 
5
5
  const { checkBadRequest } = require('../../utils');
6
+ const { createRateLimitRunner } = require('./rate-limit');
6
7
 
7
8
  module.exports = ({ nexus, strapi }) => {
8
9
  const { nonNull } = nexus;
10
+ const runRateLimit = createRateLimitRunner(strapi, '/auth/change-password');
9
11
 
10
12
  return {
11
13
  type: 'UsersPermissionsLoginPayload',
@@ -21,11 +23,9 @@ module.exports = ({ nexus, strapi }) => {
21
23
  async resolve(parent, args, context) {
22
24
  const { koaContext } = context;
23
25
 
24
- koaContext.request.body = toPlainObject(args);
25
-
26
- await strapi.plugin('users-permissions').controller('auth').changePassword(koaContext);
27
-
28
- const output = koaContext.body;
26
+ const output = await runRateLimit(koaContext, { body: toPlainObject(args) }, (ctx) =>
27
+ strapi.plugin('users-permissions').controller('auth').changePassword(ctx)
28
+ );
29
29
 
30
30
  checkBadRequest(output);
31
31
 
@@ -3,9 +3,11 @@
3
3
  const { toPlainObject } = require('lodash/fp');
4
4
 
5
5
  const { checkBadRequest } = require('../../utils');
6
+ const { createRateLimitRunner } = require('./rate-limit');
6
7
 
7
8
  module.exports = ({ nexus, strapi }) => {
8
9
  const { nonNull } = nexus;
10
+ const runRateLimit = createRateLimitRunner(strapi, '/auth/forgot-password');
9
11
 
10
12
  return {
11
13
  type: 'UsersPermissionsPasswordPayload',
@@ -19,11 +21,9 @@ module.exports = ({ nexus, strapi }) => {
19
21
  async resolve(parent, args, context) {
20
22
  const { koaContext } = context;
21
23
 
22
- koaContext.request.body = toPlainObject(args);
23
-
24
- await strapi.plugin('users-permissions').controller('auth').forgotPassword(koaContext);
25
-
26
- const output = koaContext.body;
24
+ const output = await runRateLimit(koaContext, { body: toPlainObject(args) }, (ctx) =>
25
+ strapi.plugin('users-permissions').controller('auth').forgotPassword(ctx)
26
+ );
27
27
 
28
28
  checkBadRequest(output);
29
29
 
@@ -3,9 +3,11 @@
3
3
  const { toPlainObject } = require('lodash/fp');
4
4
 
5
5
  const { checkBadRequest } = require('../../utils');
6
+ const { createRateLimitRunner } = require('./rate-limit');
6
7
 
7
8
  module.exports = ({ nexus, strapi }) => {
8
9
  const { nonNull } = nexus;
10
+ const runRateLimit = createRateLimitRunner(strapi, '/auth/local');
9
11
 
10
12
  return {
11
13
  type: nonNull('UsersPermissionsLoginPayload'),
@@ -17,12 +19,14 @@ module.exports = ({ nexus, strapi }) => {
17
19
  async resolve(parent, args, context) {
18
20
  const { koaContext } = context;
19
21
 
20
- koaContext.params = { provider: args.input.provider };
21
- koaContext.request.body = toPlainObject(args.input);
22
-
23
- await strapi.plugin('users-permissions').controller('auth').callback(koaContext);
24
-
25
- const output = koaContext.body;
22
+ const output = await runRateLimit(
23
+ koaContext,
24
+ {
25
+ body: toPlainObject(args.input),
26
+ params: { provider: args.input.provider },
27
+ },
28
+ (ctx) => strapi.plugin('users-permissions').controller('auth').callback(ctx)
29
+ );
26
30
 
27
31
  checkBadRequest(output);
28
32
 
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const RATE_LIMIT_MIDDLEWARE = 'plugin::users-permissions.rateLimit';
4
+ const contextQueues = new WeakMap();
5
+
6
+ const getRateLimitPath = (strapi, routeSuffix) => {
7
+ const configuredPrefix = strapi.config.get('api.rest.prefix', '/api');
8
+ const prefix = typeof configuredPrefix === 'string' ? configuredPrefix : '/api';
9
+ const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, '');
10
+ const normalizedSuffix = routeSuffix.replace(/^\/+/, '');
11
+
12
+ return `/${[normalizedPrefix, normalizedSuffix].filter(Boolean).join('/')}`;
13
+ };
14
+
15
+ const enqueueContextOperation = (koaContext, operation) => {
16
+ const previous = contextQueues.get(koaContext) || Promise.resolve();
17
+ const result = previous.catch(() => undefined).then(operation);
18
+
19
+ contextQueues.set(
20
+ koaContext,
21
+ result.catch(() => undefined)
22
+ );
23
+
24
+ return result;
25
+ };
26
+
27
+ const createRateLimitRunner = (strapi, routeSuffix) => {
28
+ const rateLimit = strapi.middleware(RATE_LIMIT_MIDDLEWARE)({}, { strapi });
29
+ const routePath = getRateLimitPath(strapi, routeSuffix);
30
+
31
+ return (koaContext, { body, params }, controller) =>
32
+ enqueueContextOperation(koaContext, async () => {
33
+ const originalBody = koaContext.request.body;
34
+ const originalPath = koaContext.request.path;
35
+ const originalParams = koaContext.params;
36
+ const originalResponseBody = koaContext.body;
37
+ let responseStatus = 200;
38
+
39
+ koaContext.request.path = routePath;
40
+ koaContext.request.body = body;
41
+ if (params) {
42
+ koaContext.params = params;
43
+ }
44
+
45
+ try {
46
+ await rateLimit(koaContext, () => controller(koaContext));
47
+ responseStatus = koaContext.status;
48
+ return koaContext.body;
49
+ } finally {
50
+ koaContext.request.path = originalPath;
51
+ koaContext.request.body = originalBody;
52
+ koaContext.params = originalParams;
53
+ koaContext.body = originalResponseBody;
54
+ koaContext.status = responseStatus;
55
+ }
56
+ });
57
+ };
58
+
59
+ module.exports = {
60
+ createRateLimitRunner,
61
+ getRateLimitPath,
62
+ };
@@ -3,9 +3,11 @@
3
3
  const { toPlainObject } = require('lodash/fp');
4
4
 
5
5
  const { checkBadRequest } = require('../../utils');
6
+ const { createRateLimitRunner } = require('./rate-limit');
6
7
 
7
8
  module.exports = ({ nexus, strapi }) => {
8
9
  const { nonNull } = nexus;
10
+ const runRateLimit = createRateLimitRunner(strapi, '/auth/local/register');
9
11
 
10
12
  return {
11
13
  type: nonNull('UsersPermissionsLoginPayload'),
@@ -19,11 +21,9 @@ module.exports = ({ nexus, strapi }) => {
19
21
  async resolve(parent, args, context) {
20
22
  const { koaContext } = context;
21
23
 
22
- koaContext.request.body = toPlainObject(args.input);
23
-
24
- await strapi.plugin('users-permissions').controller('auth').register(koaContext);
25
-
26
- const output = koaContext.body;
24
+ const output = await runRateLimit(koaContext, { body: toPlainObject(args.input) }, (ctx) =>
25
+ strapi.plugin('users-permissions').controller('auth').register(ctx)
26
+ );
27
27
 
28
28
  checkBadRequest(output);
29
29
 
@@ -3,9 +3,11 @@
3
3
  const { toPlainObject } = require('lodash/fp');
4
4
 
5
5
  const { checkBadRequest } = require('../../utils');
6
+ const { createRateLimitRunner } = require('./rate-limit');
6
7
 
7
8
  module.exports = ({ nexus, strapi }) => {
8
9
  const { nonNull } = nexus;
10
+ const runRateLimit = createRateLimitRunner(strapi, '/auth/reset-password');
9
11
 
10
12
  return {
11
13
  type: 'UsersPermissionsLoginPayload',
@@ -21,11 +23,9 @@ module.exports = ({ nexus, strapi }) => {
21
23
  async resolve(parent, args, context) {
22
24
  const { koaContext } = context;
23
25
 
24
- koaContext.request.body = toPlainObject(args);
25
-
26
- await strapi.plugin('users-permissions').controller('auth').resetPassword(koaContext);
27
-
28
- const output = koaContext.body;
26
+ const output = await runRateLimit(koaContext, { body: toPlainObject(args) }, (ctx) =>
27
+ strapi.plugin('users-permissions').controller('auth').resetPassword(ctx)
28
+ );
29
29
 
30
30
  checkBadRequest(output);
31
31
 
@@ -46,7 +46,7 @@ const routeUsesEmailIdentifier = (requestPath) => {
46
46
  * `/api/auth/local/` share one bucket.
47
47
  */
48
48
  const normalizeRequestPathForRateLimit = (requestPath) => {
49
- const normalized = path.normalize(requestPath);
49
+ const normalized = path.posix.normalize(requestPath);
50
50
  const lower = toLower(normalized);
51
51
  return lower.replace(/\/+$/, '') || '/';
52
52
  };
@@ -23,6 +23,9 @@ const initProviders = ({ baseURL }) => ({
23
23
  },
24
24
  async authCallback({ accessToken }) {
25
25
  const { body } = await bearerGet('https://discord.com/api/users/@me', accessToken);
26
+ if (body.verified !== true) {
27
+ throw new Error('Email not verified by Discord');
28
+ }
26
29
  const username =
27
30
  body.discriminator && body.discriminator !== '0'
28
31
  ? `${body.username}#${body.discriminator}`
@@ -46,6 +49,9 @@ const initProviders = ({ baseURL }) => ({
46
49
  const { body } = await bearerGet('https://graph.facebook.com/me', accessToken, {
47
50
  qs: { fields: 'name,email' },
48
51
  });
52
+ if (!body.email) {
53
+ throw new Error('Email not verified by Facebook');
54
+ }
49
55
  return {
50
56
  username: body.name,
51
57
  email: body.email,
@@ -65,6 +71,10 @@ const initProviders = ({ baseURL }) => ({
65
71
  const { body } = await fetchJson(
66
72
  `https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(accessToken)}`
67
73
  );
74
+ const verified = body.verified_email ?? body.email_verified;
75
+ if (verified !== true && verified !== 'true') {
76
+ throw new Error('Email not verified by Google');
77
+ }
68
78
  return {
69
79
  username: body.email.split('@')[0],
70
80
  email: body.email,
@@ -85,13 +95,6 @@ const initProviders = ({ baseURL }) => ({
85
95
  headers: { 'user-agent': 'strapi' },
86
96
  });
87
97
 
88
- if (userBody.email) {
89
- return {
90
- username: userBody.login,
91
- email: userBody.email,
92
- };
93
- }
94
-
95
98
  const { body: emailBody } = await bearerGet(
96
99
  'https://api.github.com/user/emails',
97
100
  accessToken,
@@ -100,11 +103,32 @@ const initProviders = ({ baseURL }) => ({
100
103
  }
101
104
  );
102
105
 
106
+ if (userBody.email) {
107
+ const verifiedPublicEmail = Array.isArray(emailBody)
108
+ ? emailBody.find((entry) => entry.email === userBody.email && entry.verified === true)
109
+ : null;
110
+
111
+ if (!verifiedPublicEmail) {
112
+ throw new Error('Email not verified by GitHub');
113
+ }
114
+
115
+ return {
116
+ username: userBody.login,
117
+ email: userBody.email,
118
+ };
119
+ }
120
+
121
+ const primaryEmail = Array.isArray(emailBody)
122
+ ? emailBody.find((email) => email.primary === true)
123
+ : null;
124
+
125
+ if (!primaryEmail || primaryEmail.verified !== true) {
126
+ throw new Error('Email not verified by GitHub');
127
+ }
128
+
103
129
  return {
104
130
  username: userBody.login,
105
- email: Array.isArray(emailBody)
106
- ? emailBody.find((email) => email.primary === true).email
107
- : null,
131
+ email: primaryEmail.email,
108
132
  };
109
133
  },
110
134
  },
@@ -134,17 +158,28 @@ const initProviders = ({ baseURL }) => ({
134
158
  secret: '',
135
159
  callbackUrl: `${baseURL}/twitter/callback`,
136
160
  },
137
- async authCallback({ accessToken, query, providers }) {
161
+ async authCallback({ accessToken, providers, grantResponse }) {
162
+ const accessSecret = grantResponse?.access_secret;
163
+ const screenName = grantResponse?.raw?.screen_name;
164
+
165
+ if (!accessSecret) {
166
+ throw new Error('Twitter authentication requires a completed OAuth session');
167
+ }
168
+
138
169
  const { twitterGet } = require('../utils/oauth-connect/oauth1');
139
170
  const { body } = await twitterGet({
140
171
  url: 'https://api.twitter.com/1.1/account/verify_credentials.json',
141
172
  accessToken,
142
- accessCredential: query.access_secret,
173
+ accessCredential: accessSecret,
143
174
  consumerKey: providers.twitter.key,
144
175
  clientCredential: providers.twitter.secret,
145
- qs: { include_email: 'true', screen_name: query['raw[screen_name]'] },
176
+ qs: { include_email: 'true', screen_name: screenName },
146
177
  });
147
178
 
179
+ if (!body.email) {
180
+ throw new Error('Email not verified by Twitter');
181
+ }
182
+
148
183
  return {
149
184
  username: body.screen_name,
150
185
  email: body.email,
@@ -179,13 +214,25 @@ const initProviders = ({ baseURL }) => ({
179
214
  callbackUrl: `${baseURL}/vk/callback`,
180
215
  scope: ['email'],
181
216
  },
182
- async authCallback({ accessToken, query }) {
217
+ async authCallback({ accessToken, grantResponse }) {
218
+ const email = grantResponse?.raw?.email;
219
+ const userId = grantResponse?.raw?.user_id;
220
+
221
+ if (!email || !userId) {
222
+ throw new Error('VK authentication requires a completed OAuth session');
223
+ }
224
+
183
225
  const { body } = await bearerGet('https://api.vk.com/method/users.get', accessToken, {
184
- qs: { user_ids: query.raw.user_id, v: '5.122' },
226
+ qs: { user_ids: userId, v: '5.122' },
185
227
  });
228
+
229
+ if (!body.response?.[0]) {
230
+ throw new Error('Invalid VK access token');
231
+ }
232
+
186
233
  return {
187
234
  username: `${body.response[0].last_name} ${body.response[0].first_name}`,
188
- email: query.raw.email,
235
+ email,
189
236
  };
190
237
  },
191
238
  },
@@ -205,9 +252,13 @@ const initProviders = ({ baseURL }) => ({
205
252
  'Client-Id': providers.twitch.key,
206
253
  },
207
254
  });
255
+ const email = body.data?.[0]?.email;
256
+ if (!email) {
257
+ throw new Error('Email not verified by Twitch');
258
+ }
208
259
  return {
209
260
  username: body.data[0].login,
210
- email: body.data[0].email,
261
+ email,
211
262
  };
212
263
  },
213
264
  },
@@ -228,7 +279,11 @@ const initProviders = ({ baseURL }) => ({
228
279
  accessToken
229
280
  );
230
281
 
231
- const email = emailBody.elements[0]['handle~'];
282
+ const email = emailBody.elements[0]?.['handle~'];
283
+
284
+ if (!email?.emailAddress) {
285
+ throw new Error('Email not verified by LinkedIn');
286
+ }
232
287
 
233
288
  return {
234
289
  username: profileBody.localizedFirstName,
@@ -247,10 +302,18 @@ const initProviders = ({ baseURL }) => ({
247
302
  callback: `${baseURL}/cognito/callback`,
248
303
  scope: ['email', 'openid', 'profile'],
249
304
  },
250
- async authCallback({ query, providers }) {
305
+ async authCallback({ providers, grantResponse }) {
251
306
  const jwksUrl = new URL(providers.cognito.jwksurl);
252
- const idToken = query.id_token;
307
+ const idToken = grantResponse?.id_token;
308
+
309
+ if (!idToken) {
310
+ throw new Error('Cognito authentication requires a completed OAuth session');
311
+ }
312
+
253
313
  const tokenPayload = await verifyJwtWithJwks({ idToken, jwksUrl });
314
+ if (tokenPayload.email_verified !== true) {
315
+ throw new Error('Email not verified by Cognito');
316
+ }
254
317
  return {
255
318
  username: tokenPayload['cognito:username'],
256
319
  email: tokenPayload.email,
@@ -293,6 +356,9 @@ const initProviders = ({ baseURL }) => ({
293
356
  `https://${providers.auth0.subdomain}.auth0.com/userinfo`,
294
357
  accessToken
295
358
  );
359
+ if (body.email && body.email_verified !== true) {
360
+ throw new Error('Email not verified by Auth0');
361
+ }
296
362
  const username = body.username || body.nickname || body.name || body.email.split('@')[0];
297
363
  const email = body.email || `${username.replace(/\s+/g, '.')}@strapi.io`;
298
364
 
@@ -324,11 +390,15 @@ const initProviders = ({ baseURL }) => ({
324
390
  const email = body.attributes
325
391
  ? body.attributes.strapiemail || body.attributes.email
326
392
  : body.strapiemail || body.email;
393
+ const emailVerified = body.email_verified ?? body.attributes?.email_verified;
327
394
  if (!username || !email) {
328
395
  strapi.log.warn(
329
396
  `CAS Response Body did not contain required attributes: ${JSON.stringify(body)}`
330
397
  );
331
398
  }
399
+ if (email && emailVerified !== true) {
400
+ throw new Error('Email not verified by CAS');
401
+ }
332
402
  return {
333
403
  username,
334
404
  email,
@@ -347,10 +417,13 @@ const initProviders = ({ baseURL }) => ({
347
417
  },
348
418
  async authCallback({ accessToken }) {
349
419
  const { body } = await bearerGet(
350
- 'https://www.patreon.com/api/oauth2/v2/identity?fields[user]=full_name,email',
420
+ 'https://www.patreon.com/api/oauth2/v2/identity?fields[user]=full_name,email,is_email_verified',
351
421
  accessToken
352
422
  );
353
423
  const patreonData = body.data.attributes;
424
+ if (patreonData.is_email_verified !== true) {
425
+ throw new Error('Email not verified by Patreon');
426
+ }
354
427
  return {
355
428
  username: patreonData.full_name,
356
429
  email: patreonData.email,
@@ -372,6 +445,9 @@ const initProviders = ({ baseURL }) => ({
372
445
  `https://${providers.keycloak.subdomain}/protocol/openid-connect/userinfo`,
373
446
  accessToken
374
447
  );
448
+ if (body.email_verified !== true) {
449
+ throw new Error('Email not verified by Keycloak');
450
+ }
375
451
  return {
376
452
  username: body.preferred_username,
377
453
  email: body.email,
@@ -400,12 +476,12 @@ module.exports = () => {
400
476
  delete authProviders[name];
401
477
  },
402
478
 
403
- async run({ provider, accessToken, query, providers }) {
479
+ async run({ provider, accessToken, query, providers, grantResponse }) {
404
480
  const authProvider = authProviders[provider];
405
481
 
406
482
  assert(authProvider, 'Unknown auth provider');
407
483
 
408
- return authProvider.authCallback({ accessToken, query, providers });
484
+ return authProvider.authCallback({ accessToken, query, providers, grantResponse });
409
485
  },
410
486
  };
411
487
  };
@@ -17,8 +17,8 @@ module.exports = ({ strapi }) => {
17
17
  * @param {String} provider
18
18
  */
19
19
 
20
- const getProfile = async (provider, query) => {
21
- const accessToken = query.access_token || query.code || query.oauth_token;
20
+ const getProfile = async (provider, oauthData, { grantResponse } = {}) => {
21
+ const accessToken = oauthData.access_token || oauthData.code || oauthData.oauth_token;
22
22
 
23
23
  const providers = await strapi
24
24
  .store({ type: 'plugin', name: 'users-permissions', key: 'grant' })
@@ -26,9 +26,10 @@ module.exports = ({ strapi }) => {
26
26
 
27
27
  return getService('providers-registry').run({
28
28
  provider,
29
- query,
29
+ query: oauthData,
30
30
  accessToken,
31
31
  providers,
32
+ grantResponse,
32
33
  });
33
34
  };
34
35
 
@@ -42,15 +43,16 @@ module.exports = ({ strapi }) => {
42
43
  * @return {*}
43
44
  */
44
45
 
45
- const connect = async (provider, query) => {
46
- const accessToken = query.access_token || query.code || query.oauth_token;
46
+ const connect = async (provider, oauthData, { grantResponse } = {}) => {
47
+ const accessToken = oauthData.access_token || oauthData.code || oauthData.oauth_token;
48
+ const idToken = oauthData.id_token;
47
49
 
48
- if (!accessToken) {
50
+ if (!accessToken && !idToken) {
49
51
  throw new Error('No access_token.');
50
52
  }
51
53
 
52
54
  // Get the profile.
53
- const profile = await getProfile(provider, query);
55
+ const profile = await getProfile(provider, oauthData, { grantResponse });
54
56
 
55
57
  const email = _.toLower(profile.email);
56
58
 
@@ -10,12 +10,14 @@ const crypto = require('crypto');
10
10
  const bcrypt = require('bcryptjs');
11
11
  const urlJoin = require('url-join');
12
12
 
13
- const { sanitize } = require('@strapi/utils');
14
- const { toNumber, getOr } = require('lodash/fp');
13
+ const { sanitize, ALLOWED_QUERY_PARAM_KEYS } = require('@strapi/utils');
14
+ const { toNumber, getOr, pick } = require('lodash/fp');
15
15
  const { getService } = require('../utils');
16
16
 
17
17
  const USER_MODEL_UID = 'plugin::users-permissions.user';
18
18
 
19
+ const pickAllowedQueryParams = pick(ALLOWED_QUERY_PARAM_KEYS);
20
+
19
21
  const getSessionManager = () => {
20
22
  const manager = strapi.sessionManager;
21
23
  return manager ?? null;
@@ -29,7 +31,11 @@ module.exports = ({ strapi }) => ({
29
31
  */
30
32
 
31
33
  count(params) {
32
- return strapi.db.query(USER_MODEL_UID).count({ where: params });
34
+ const query = strapi
35
+ .get('query-params')
36
+ .transform(USER_MODEL_UID, pickAllowedQueryParams(params ?? {}));
37
+
38
+ return strapi.db.query(USER_MODEL_UID).count(query);
33
39
  },
34
40
 
35
41
  /**
@@ -101,7 +107,9 @@ module.exports = ({ strapi }) => ({
101
107
  * @return {Promise}
102
108
  */
103
109
  fetch(id, params) {
104
- const query = strapi.get('query-params').transform(USER_MODEL_UID, params ?? {});
110
+ const query = strapi
111
+ .get('query-params')
112
+ .transform(USER_MODEL_UID, pickAllowedQueryParams(params ?? {}));
105
113
 
106
114
  return strapi.db.query(USER_MODEL_UID).findOne({
107
115
  ...query,
@@ -124,7 +132,9 @@ module.exports = ({ strapi }) => ({
124
132
  * @return {Promise}
125
133
  */
126
134
  fetchAll(params) {
127
- const query = strapi.get('query-params').transform(USER_MODEL_UID, params ?? {});
135
+ const query = strapi
136
+ .get('query-params')
137
+ .transform(USER_MODEL_UID, pickAllowedQueryParams(params ?? {}));
128
138
 
129
139
  return strapi.db.query(USER_MODEL_UID).findMany(query);
130
140
  },