@strapi/plugin-users-permissions 4.6.0-beta.1 → 4.6.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.
@@ -173,6 +173,21 @@ const forms = {
173
173
  },
174
174
  },
175
175
  ],
176
+ [
177
+ {
178
+ intlLabel: {
179
+ id: getTrad({ id: 'PopUpForm.Providers.jwksurl.label' }),
180
+ defaultMessage: 'JWKS URL',
181
+ },
182
+ name: 'jwksurl',
183
+ type: 'text',
184
+ placeholder: textPlaceholder,
185
+ size: 12,
186
+ validations: {
187
+ required: false,
188
+ },
189
+ },
190
+ ],
176
191
 
177
192
  [
178
193
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/plugin-users-permissions",
3
- "version": "4.6.0-beta.1",
3
+ "version": "4.6.0",
4
4
  "description": "Protect your API with a full-authentication process based on JWT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,11 +27,12 @@
27
27
  "test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
28
28
  },
29
29
  "dependencies": {
30
- "@strapi/helper-plugin": "4.6.0-beta.1",
31
- "@strapi/utils": "4.6.0-beta.1",
30
+ "@strapi/helper-plugin": "4.6.0",
31
+ "@strapi/utils": "4.6.0",
32
32
  "bcryptjs": "2.4.3",
33
33
  "grant-koa": "5.4.8",
34
- "jsonwebtoken": "^8.1.0",
34
+ "jsonwebtoken": "9.0.0",
35
+ "jwk-to-pem": "2.0.5",
35
36
  "koa": "^2.13.4",
36
37
  "koa2-ratelimit": "^1.1.2",
37
38
  "lodash": "4.17.21",
@@ -64,5 +65,5 @@
64
65
  "required": true,
65
66
  "kind": "plugin"
66
67
  },
67
- "gitHead": "2c0bcabdf0bf2a269fed50c6f23ba777845968a0"
68
+ "gitHead": "a9e55435c489f3379d88565bf3f729deb29bfb45"
68
69
  }
@@ -1,8 +1,17 @@
1
1
  'use strict';
2
2
 
3
- const _ = require('lodash');
3
+ const { trim } = require('lodash/fp');
4
+ const {
5
+ template: { createLooseInterpolationRegExp, createStrictInterpolationRegExp },
6
+ } = require('@strapi/utils');
7
+
8
+ const invalidPatternsRegexes = [
9
+ // Ignore "evaluation" patterns: <% ... %>
10
+ /<%[^=]([\s\S]*?)%>/m,
11
+ // Ignore basic string interpolations
12
+ /\${([^{}]*)}/m,
13
+ ];
4
14
 
5
- const invalidPatternsRegexes = [/<%[^=]([^<>%]*)%>/m, /\${([^{}]*)}/m];
6
15
  const authorizedKeys = [
7
16
  'URL',
8
17
  'ADMIN_URL',
@@ -19,27 +28,42 @@ const matchAll = (pattern, src) => {
19
28
  let match;
20
29
 
21
30
  const regexPatternWithGlobal = RegExp(pattern, 'g');
31
+
22
32
  // eslint-disable-next-line no-cond-assign
23
33
  while ((match = regexPatternWithGlobal.exec(src))) {
24
34
  const [, group] = match;
25
35
 
26
- matches.push(_.trim(group));
36
+ matches.push(trim(group));
27
37
  }
38
+
28
39
  return matches;
29
40
  };
30
41
 
31
42
  const isValidEmailTemplate = (template) => {
43
+ // Check for known invalid patterns
32
44
  for (const reg of invalidPatternsRegexes) {
33
45
  if (reg.test(template)) {
34
46
  return false;
35
47
  }
36
48
  }
37
49
 
38
- const matches = matchAll(/<%=([^<>%=]*)%>/, template);
39
- for (const match of matches) {
40
- if (!authorizedKeys.includes(match)) {
41
- return false;
42
- }
50
+ const interpolation = {
51
+ // Strict interpolation pattern to match only valid groups
52
+ strict: createStrictInterpolationRegExp(authorizedKeys),
53
+ // Weak interpolation pattern to match as many group as possible.
54
+ loose: createLooseInterpolationRegExp(),
55
+ };
56
+
57
+ // Compute both strict & loose matches
58
+ const strictMatches = matchAll(interpolation.strict, template);
59
+ const looseMatches = matchAll(interpolation.loose, template);
60
+
61
+ // If we have more matches with the loose RegExp than with the strict one,
62
+ // then it means that at least one of the interpolation group is invalid
63
+ // Note: In the future, if we wanted to give more details for error formatting
64
+ // purposes, we could return the difference between the two arrays
65
+ if (looseMatches.length > strictMatches.length) {
66
+ return false;
43
67
  }
44
68
 
45
69
  return true;
@@ -2,6 +2,48 @@
2
2
 
3
3
  const { strict: assert } = require('assert');
4
4
  const jwt = require('jsonwebtoken');
5
+ const jwkToPem = require('jwk-to-pem');
6
+
7
+ const getCognitoPayload = async ({ idToken, jwksUrl, purest }) => {
8
+ const {
9
+ header: { kid },
10
+ payload,
11
+ } = jwt.decode(idToken, { complete: true });
12
+
13
+ if (!payload || !kid) {
14
+ throw new Error('The provided token is not valid');
15
+ }
16
+
17
+ const config = {
18
+ cognito: {
19
+ discovery: {
20
+ origin: jwksUrl.origin,
21
+ path: jwksUrl.pathname,
22
+ },
23
+ },
24
+ };
25
+ try {
26
+ const cognito = purest({ provider: 'cognito', config });
27
+ // get the JSON Web Key (JWK) for the user pool
28
+ const { body: jwk } = await cognito('discovery').request();
29
+ // Get the key with the same Key ID as the provided token
30
+ const key = jwk.keys.find(({ kid: jwkKid }) => jwkKid === kid);
31
+ const pem = jwkToPem(key);
32
+
33
+ // https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
34
+ const decodedToken = await new Promise((resolve, reject) => {
35
+ jwt.verify(idToken, pem, { algorithms: ['RS256'] }, (err, decodedToken) => {
36
+ if (err) {
37
+ reject();
38
+ }
39
+ resolve(decodedToken);
40
+ });
41
+ });
42
+ return decodedToken;
43
+ } catch (err) {
44
+ throw new Error('There was an error verifying the token');
45
+ }
46
+ };
5
47
 
6
48
  const getInitialProviders = ({ purest }) => ({
7
49
  async discord({ accessToken }) {
@@ -19,19 +61,14 @@ const getInitialProviders = ({ purest }) => ({
19
61
  };
20
62
  });
21
63
  },
22
- async cognito({ query }) {
23
- // get the id_token
64
+ async cognito({ query, providers }) {
65
+ const jwksUrl = new URL(providers.cognito.jwksurl);
24
66
  const idToken = query.id_token;
25
- // decode the jwt token
26
- const tokenPayload = jwt.decode(idToken);
27
- if (!tokenPayload) {
28
- throw new Error('unable to decode jwt token');
29
- } else {
30
- return {
31
- username: tokenPayload['cognito:username'],
32
- email: tokenPayload.email,
33
- };
34
- }
67
+ const tokenPayload = await getCognitoPayload({ idToken, jwksUrl, purest });
68
+ return {
69
+ username: tokenPayload['cognito:username'],
70
+ email: tokenPayload.email,
71
+ };
35
72
  },
36
73
  async facebook({ accessToken }) {
37
74
  const facebook = purest({ provider: 'facebook' });
@@ -109,17 +109,25 @@ module.exports = ({ strapi }) => ({
109
109
  await this.edit(user.id, { confirmationToken });
110
110
 
111
111
  const apiPrefix = strapi.config.get('api.rest.prefix');
112
- settings.message = await userPermissionService.template(settings.message, {
113
- URL: urlJoin(getAbsoluteServerUrl(strapi.config), apiPrefix, '/auth/email-confirmation'),
114
- SERVER_URL: getAbsoluteServerUrl(strapi.config),
115
- ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
116
- USER: sanitizedUserInfo,
117
- CODE: confirmationToken,
118
- });
119
112
 
120
- settings.object = await userPermissionService.template(settings.object, {
121
- USER: sanitizedUserInfo,
122
- });
113
+ try {
114
+ settings.message = await userPermissionService.template(settings.message, {
115
+ URL: urlJoin(getAbsoluteServerUrl(strapi.config), apiPrefix, '/auth/email-confirmation'),
116
+ SERVER_URL: getAbsoluteServerUrl(strapi.config),
117
+ ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
118
+ USER: sanitizedUserInfo,
119
+ CODE: confirmationToken,
120
+ });
121
+
122
+ settings.object = await userPermissionService.template(settings.object, {
123
+ USER: sanitizedUserInfo,
124
+ });
125
+ } catch {
126
+ strapi.log.error(
127
+ '[plugin::users-permissions.sendConfirmationEmail]: Failed to generate a template for "user confirmation email". Please make sure your email template is valid and does not contain invalid characters or patterns'
128
+ );
129
+ return;
130
+ }
123
131
 
124
132
  // Send an email to the user.
125
133
  await strapi
@@ -3,6 +3,11 @@
3
3
  const _ = require('lodash');
4
4
  const { filter, map, pipe, prop } = require('lodash/fp');
5
5
  const urlJoin = require('url-join');
6
+ const {
7
+ template: { createStrictInterpolationRegExp },
8
+ errors,
9
+ keysDeep,
10
+ } = require('@strapi/utils');
6
11
 
7
12
  const { getService } = require('../utils');
8
13
 
@@ -230,7 +235,15 @@ module.exports = ({ strapi }) => ({
230
235
  },
231
236
 
232
237
  template(layout, data) {
233
- const compiledObject = _.template(layout);
234
- return compiledObject(data);
238
+ const allowedTemplateVariables = keysDeep(data);
239
+
240
+ // Create a strict interpolation RegExp based on possible variable names
241
+ const interpolate = createStrictInterpolationRegExp(allowedTemplateVariables, 'g');
242
+
243
+ try {
244
+ return _.template(layout, { interpolate, evaluate: false, escape: false })(data);
245
+ } catch (e) {
246
+ throw new errors.ApplicationError('Invalid email template');
247
+ }
235
248
  },
236
249
  });