@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.
- package/dist/server/controllers/auth.js +8 -2
- package/dist/server/controllers/auth.js.map +1 -1
- package/dist/server/controllers/auth.mjs +8 -2
- package/dist/server/controllers/auth.mjs.map +1 -1
- package/dist/server/graphql/mutations/auth/change-password.js +6 -3
- package/dist/server/graphql/mutations/auth/change-password.js.map +1 -1
- package/dist/server/graphql/mutations/auth/change-password.mjs +6 -3
- package/dist/server/graphql/mutations/auth/change-password.mjs.map +1 -1
- package/dist/server/graphql/mutations/auth/forgot-password.js +6 -3
- package/dist/server/graphql/mutations/auth/forgot-password.js.map +1 -1
- package/dist/server/graphql/mutations/auth/forgot-password.mjs +6 -3
- package/dist/server/graphql/mutations/auth/forgot-password.mjs.map +1 -1
- package/dist/server/graphql/mutations/auth/login.js +9 -6
- package/dist/server/graphql/mutations/auth/login.js.map +1 -1
- package/dist/server/graphql/mutations/auth/login.mjs +9 -6
- package/dist/server/graphql/mutations/auth/login.mjs.map +1 -1
- package/dist/server/graphql/mutations/auth/rate-limit.js +63 -0
- package/dist/server/graphql/mutations/auth/rate-limit.js.map +1 -0
- package/dist/server/graphql/mutations/auth/rate-limit.mjs +61 -0
- package/dist/server/graphql/mutations/auth/rate-limit.mjs.map +1 -0
- package/dist/server/graphql/mutations/auth/register.js +6 -3
- package/dist/server/graphql/mutations/auth/register.js.map +1 -1
- package/dist/server/graphql/mutations/auth/register.mjs +6 -3
- package/dist/server/graphql/mutations/auth/register.mjs.map +1 -1
- package/dist/server/graphql/mutations/auth/reset-password.js +6 -3
- package/dist/server/graphql/mutations/auth/reset-password.js.map +1 -1
- package/dist/server/graphql/mutations/auth/reset-password.mjs +6 -3
- package/dist/server/graphql/mutations/auth/reset-password.mjs.map +1 -1
- package/dist/server/middlewares/rateLimit.js +1 -1
- package/dist/server/middlewares/rateLimit.js.map +1 -1
- package/dist/server/middlewares/rateLimit.mjs +1 -1
- package/dist/server/middlewares/rateLimit.mjs.map +1 -1
- package/dist/server/services/providers-registry.js +80 -19
- package/dist/server/services/providers-registry.js.map +1 -1
- package/dist/server/services/providers-registry.mjs +80 -19
- package/dist/server/services/providers-registry.mjs.map +1 -1
- package/dist/server/services/providers.js +12 -8
- package/dist/server/services/providers.js.map +1 -1
- package/dist/server/services/providers.mjs +12 -8
- package/dist/server/services/providers.mjs.map +1 -1
- package/dist/server/services/user.js +7 -7
- package/dist/server/services/user.js.map +1 -1
- package/dist/server/services/user.mjs +7 -7
- package/dist/server/services/user.mjs.map +1 -1
- package/dist/server/utils/oauth-connect/index.js +11 -6
- package/dist/server/utils/oauth-connect/index.js.map +1 -1
- package/dist/server/utils/oauth-connect/index.mjs +11 -6
- package/dist/server/utils/oauth-connect/index.mjs.map +1 -1
- package/dist/server/utils/trim-grant-session-response.js +67 -0
- package/dist/server/utils/trim-grant-session-response.js.map +1 -0
- package/dist/server/utils/trim-grant-session-response.mjs +65 -0
- package/dist/server/utils/trim-grant-session-response.mjs.map +1 -0
- package/package.json +4 -4
- package/server/src/controllers/auth.js +13 -2
- package/server/src/graphql/mutations/auth/change-password.js +5 -5
- package/server/src/graphql/mutations/auth/forgot-password.js +5 -5
- package/server/src/graphql/mutations/auth/login.js +10 -6
- package/server/src/graphql/mutations/auth/rate-limit.js +62 -0
- package/server/src/graphql/mutations/auth/register.js +5 -5
- package/server/src/graphql/mutations/auth/reset-password.js +5 -5
- package/server/src/middlewares/rateLimit.js +1 -1
- package/server/src/services/providers-registry.js +99 -23
- package/server/src/services/providers.js +9 -7
- package/server/src/services/user.js +15 -5
- package/server/src/utils/oauth-connect/index.js +12 -6
- 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.
|
|
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.
|
|
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.
|
|
76
|
-
"@strapi/strapi": "5.
|
|
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
|
|
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(
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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:
|
|
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,
|
|
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:
|
|
173
|
+
accessCredential: accessSecret,
|
|
143
174
|
consumerKey: providers.twitter.key,
|
|
144
175
|
clientCredential: providers.twitter.secret,
|
|
145
|
-
qs: { include_email: 'true', 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,
|
|
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:
|
|
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
|
|
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
|
|
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({
|
|
305
|
+
async authCallback({ providers, grantResponse }) {
|
|
251
306
|
const jwksUrl = new URL(providers.cognito.jwksurl);
|
|
252
|
-
const idToken =
|
|
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,
|
|
21
|
-
const accessToken =
|
|
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,
|
|
46
|
-
const accessToken =
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
},
|