Merge branch 'devel' into remove-useraccounts-guide

This commit is contained in:
Jan Dvorak
2022-08-03 08:55:18 +02:00
committed by GitHub
12 changed files with 582 additions and 1749 deletions

View File

@@ -44,22 +44,34 @@ Reviwers are members of the community that help with Pull Requests reviews.
Current Reviewers:
- [meteor](https://github.com/meteor/meteor)
- [@denihs](https://github.com/denihs)
- [@filipenevola](https://github.com/filipenevola)
- [@henriquealbert](https://github.com/henriquealbert)
- [@fredmaiaarantes](https://github.com/fredmaiaarantes)
- [@StorytellerCZ](https://github.com/StorytellerCZ)
- [@zodern](https://github.com/zodern)
- [@henriquealbert](https://github.com/henriquealbert)
- [@aquinoit](https://github.com/aquinoit)
- [@Grubba27](https://github.com/Grubba27)
- [@filipenevola](https://github.com/filipenevola)
- [@StorytellerCZ](https://github.com/StorytellerCZ)
- [@zodern](https://github.com/zodern)
- [CaptainN](https://github.com/orgs/meteor/people/CaptainN)
- [radekmie](https://github.com/orgs/meteor/people/radekmie)
#### Core Committer
The contributors with commit access to meteor/meteor are employees of Meteor Software Ltd or community members who have distinguished themselves in other contribution areas. If you want to become a core committer please start writing PRs.
The contributors with commit access to meteor/meteor are employees of Meteor Software LP or community members who have distinguished themselves in other contribution areas or members of partner companies. If you want to become a core committer please start writing PRs.
Current Core Committers:
- [@denihs](https://github.com/denihs)
- [@zodern](https://github.com/zodern)
- [@filipenevola](https://github.com/filipenevola)
- [@henriquealbert](https://github.com/henriquealbert)
- [@fredmaiaarantes](https://github.com/fredmaiaarantes)
- [@henriquealbert](https://github.com/henriquealbert)
- [@Grubba27](https://github.com/Grubba27)
- [StorytellerCZ](https://github.com/orgs/meteor/people/StorytellerCZ)
- [CaptainN](https://github.com/orgs/meteor/people/CaptainN)
- [radekmie](https://github.com/orgs/meteor/people/radekmie)
- [piotrpospiech](https://github.com/orgs/meteor/people/piotrpospiech)
- [edimarlnx](https://github.com/orgs/meteor/people/edimarlnx)
- [matheusccastroo](https://github.com/orgs/meteor/people/matheusccastroo)
- [eduwr](https://github.com/orgs/meteor/people/eduwr)
### Tracking project work

View File

@@ -16,6 +16,14 @@ The second step in the passwordless flow. Like all the other `loginWith` functio
{% apibox "Accounts.sendLoginTokenEmail" "module":"accounts-base" %}
Use this function if you want to manually send the email to users to login with token from the server. Do note that you will need to create the token/sequence and save it in the DB yourself. This is good if you want to change how the tokens look or are generated, but unless you are sure of what you are doing we don't recommend it.
<h3 id="config-options">Settings Options</h3>
You can use the function `Accounts.config` in the server to change some settings on this package:
- **tokenSequenceLength**: use `Accounts.config({tokenSequenceLength: _Number_})` to the size of the token sequence generated. The default is 6.
- **loginTokenExpirationHours**: use `Accounts.config({loginTokenExpirationHours: _Number_})` to set the amount of time a token sent is valid. As it's just a number, you can use, for example, 0.5 to make the token valid for just half hour. The default is 1 hour.
<h3 id="passwordless-email-templates">E-mail templates</h3>
`accounts-passwordless` brings new templates that you can edit to change the look of emails which send code to users. The email template is named `sendLoginToken` and beside `user` and `url`, the templates also receive a data object with `sequence` which is the user's code.

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,10 @@
"name": "meteor-node-stubs",
"author": "Ben Newman <ben@meteor.com>",
"description": "Stub implementations of Node built-in modules, a la Browserify",
"version": "1.2.3",
"version": "1.2.5",
"main": "index.js",
"license": "MIT",
"homepage": "https://github.com/meteor/meteor/blob/devel/npm-packages/meteor-node-stubs/README.md",
"scripts": {
"prepare": "node scripts/build-deps.js"
},
@@ -15,7 +16,7 @@
"console-browserify": "^1.2.0",
"constants-browserify": "^1.0.0",
"crypto-browserify": "^3.12.0",
"domain-browser": "^4.19.0",
"domain-browser": "^4.22.0",
"elliptic": "^6.5.4",
"events": "^3.3.0",
"https-browserify": "^1.0.0",
@@ -64,7 +65,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/meteor/node-stubs.git"
"url": "https://github.com/meteor/meteor/tree/devel/npm-packages/meteor-node-stubs"
},
"keywords": [
"stubs",
@@ -79,6 +80,5 @@
],
"bugs": {
"url": "https://github.com/meteor/node-stubs/issues"
},
"homepage": "https://github.com/meteor/node-stubs#readme"
}
}

View File

@@ -14,6 +14,8 @@ const VALID_CONFIG_KEYS = [
'ambiguousErrorMessages',
'bcryptRounds',
'defaultFieldSelector',
'loginTokenExpirationHours',
'tokenSequenceLength',
];
/**
@@ -218,6 +220,8 @@ export class AccountsCommon {
* @param {Number} options.passwordEnrollTokenExpiration The number of milliseconds from when a link to set initial password is sent until token expires and user can't set password with the link anymore. If `passwordEnrollTokenExpirationInDays` is set, it takes precedent.
* @param {Boolean} options.ambiguousErrorMessages Return ambiguous error messages from login failures to prevent user enumeration. Defaults to false.
* @param {MongoFieldSpecifier} options.defaultFieldSelector To exclude by default large custom fields from `Meteor.user()` and `Meteor.findUserBy...()` functions when called without a field selector, and all `onLogin`, `onLoginFailure` and `onLogout` callbacks. Example: `Accounts.config({ defaultFieldSelector: { myBigArray: 0 }})`. Beware when using this. If, for instance, you do not include `email` when excluding the fields, you can have problems with functions like `forgotPassword` that will break because they won't have the required data available. It's recommend that you always keep the fields `_id`, `username`, and `email`.
* @param {Number} options.loginTokenExpirationHours When using the package `accounts-2fa`, use this to set the amount of time a token sent is valid. As it's just a number, you can use, for example, 0.5 to make the token valid for just half hour. The default is 1 hour.
* @param {Number} options.tokenSequenceLength When using the package `accounts-2fa`, use this to the size of the token sequence generated. The default is 6.
*/
config(options) {
// We don't want users to accidentally only call Accounts.config on the

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: 'A user account system',
version: '2.2.3',
version: '2.2.4',
});
Package.onUse(api => {

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: 'No-password login/sign-up support for accounts',
version: '2.1.2',
version: '2.1.3',
});
Package.onUse(api => {
@@ -20,3 +20,10 @@ Package.onUse(api => {
api.addFiles('passwordless_client.js', 'client');
api.addFiles('server_utils.js', 'server');
});
Package.onTest(function(api) {
api.use(['accounts-base', 'ecmascript', 'tinytest', 'sha']);
api.addFiles('server_utils.js', 'server');
api.mainModule('server_tests.js', 'server');
});

View File

@@ -1,47 +1,13 @@
import { Accounts } from 'meteor/accounts-base';
import {getUserById, NonEmptyString, tokenValidator} from './server_utils';
import {
DEFAULT_TOKEN_SEQUENCE_LENGTH,
getUserById,
NonEmptyString,
tokenValidator,
checkToken,
} from './server_utils';
import { Random } from 'meteor/random';
const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
const checkToken = ({ user, sequence, selector }) => {
const result = {
userId: user._id,
};
const { createdAt, token: userToken } = user.services.passwordless;
if (
new Date(
createdAt.getTime() +
Accounts._options.loginTokenExpirationHours * ONE_HOUR_IN_MILLISECONDS
) >= new Date()
) {
result.error = Accounts._handleError('Expired token', false);
}
if (selector.email) {
const foundTokenEmail = user.services.passwordless.tokens.find(
({ email: tokenEmail, token }) =>
SHA256(selector.email + sequence) === token &&
selector.email === tokenEmail
);
if (foundTokenEmail) {
return { ...result, verifiedEmail: foundTokenEmail.email };
}
result.error = Accounts._handleError('Email or token mismatch', false);
return result;
}
if (sequence && SHA256(user._id + sequence) === userToken) {
return result;
}
result.error = Accounts._handleError('Token mismatch', false);
return result;
};
const findUserWithOptions = ({ selector }) => {
if (!selector) {
Accounts._handleError('A selector is necessary');
@@ -139,7 +105,7 @@ const createUser = userData => {
function generateSequence() {
return Random.hexString(
Accounts._options.tokenSequenceLength || 6
Accounts._options.tokenSequenceLength || DEFAULT_TOKEN_SEQUENCE_LENGTH
).toUpperCase();
}
@@ -149,7 +115,11 @@ Meteor.methods({
fields: { emails: 1 },
});
if (!user && (options.userCreationDisabled || Accounts._options.forbidClientAccountCreation)) {
if (
!user &&
(options.userCreationDisabled ||
Accounts._options.forbidClientAccountCreation)
) {
Accounts._handleError('User not found');
}

View File

@@ -0,0 +1,122 @@
import { Random } from 'meteor/random';
import { checkToken } from './server_utils';
import { SHA256 } from 'meteor/sha';
const USER_TOKEN = '123ABC';
const getData = ({ createdAt }) => {
const userId = Random.id();
const email = `${userId}@meteorapp.com`;
const idToken = SHA256(userId + USER_TOKEN);
const emailToken = SHA256(email + USER_TOKEN);
const user = {
_id: userId,
email,
services: {
passwordless: {
createdAt,
tokens: [{ email, token: emailToken }],
token: idToken,
},
},
};
return {
user,
};
};
Tinytest.add('passwordless - time expired', test => {
const createdAt = new Date('July 17, 2022 13:00:00');
const currentDate = new Date('July 17, 2022 14:01:00');
const { user } = getData({ createdAt });
const result = checkToken({
user,
sequence: USER_TOKEN,
selector: { email: user.email },
currentDate,
});
test.isTrue(!!result.error);
test.equal(result.error.reason, 'Expired token');
});
Tinytest.add('passwordless - Email and token mismatch', test => {
const createdAt = new Date('July 17, 2022 13:00:00');
const currentDate = new Date('July 17, 2022 13:05:00');
const { user } = getData({ createdAt });
// Email mismatch
const resultEmail = checkToken({
user,
sequence: USER_TOKEN,
selector: { email: 'invalid@email.com' },
currentDate,
});
test.isTrue(!!resultEmail.error);
test.equal(resultEmail.error.reason, 'Email or token mismatch');
// Token mismatch
const resultToken = checkToken({
user,
sequence: 'ABC321',
selector: { email: user.email },
currentDate,
});
test.isTrue(!!resultToken.error);
test.equal(resultToken.error.reason, 'Email or token mismatch');
});
Tinytest.add('passwordless - Token mismatch', test => {
const createdAt = new Date('July 17, 2022 13:00:00');
const currentDate = new Date('July 17, 2022 13:05:00');
const { user } = getData({ createdAt });
const result = checkToken({
user,
sequence: 'AAA111',
selector: {},
currentDate,
});
test.isTrue(!!result.error);
test.equal(result.error.reason, 'Token mismatch');
});
Tinytest.add('passwordless - Valid token with email', test => {
const createdAt = new Date('July 17, 2022 13:00:00');
const currentDate = new Date('July 17, 2022 13:05:00');
const { user } = getData({ createdAt });
const result = checkToken({
user,
sequence: USER_TOKEN,
selector: { email: user.email },
currentDate,
});
test.isFalse(!!result.error);
});
Tinytest.add('passwordless - Valid token without email', test => {
const createdAt = new Date('July 17, 2022 13:00:00');
const currentDate = new Date('July 17, 2022 13:05:00');
const { user } = getData({ createdAt });
const result = checkToken({
user,
sequence: USER_TOKEN,
selector: {},
currentDate,
});
test.isFalse(!!result.error);
});

View File

@@ -1,10 +1,16 @@
import { Accounts } from 'meteor/accounts-base';
import { check, Match } from 'meteor/check';
import { SHA256 } from 'meteor/sha';
const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
export const DEFAULT_TOKEN_SEQUENCE_LENGTH = 6;
export const getUserById = (id, options) =>
Meteor.users.findOne(id, Accounts._addDefaultFieldSelector(options));
export const tokenValidator = () => {
const tokenLength = Accounts._options.tokenLength || 6;
const tokenLength =
Accounts._options.tokenSequenceLength || DEFAULT_TOKEN_SEQUENCE_LENGTH;
return Match.Where(
str => Match.test(str, String) && str.length <= tokenLength
);
@@ -14,3 +20,48 @@ export const NonEmptyString = Match.Where(x => {
check(x, String);
return x.length > 0;
});
export const checkToken = ({
user,
sequence,
selector,
currentDate = new Date(),
}) => {
const result = {
userId: user._id,
};
const { createdAt, token: userToken } = user.services.passwordless;
const { loginTokenExpirationHours = 1 } = Accounts._options || {};
const expirationDate = new Date(
createdAt.getTime() + loginTokenExpirationHours * ONE_HOUR_IN_MILLISECONDS
);
if (expirationDate <= currentDate) {
result.error = Accounts._handleError('Expired token', false);
}
if (selector.email) {
const foundTokenEmail = user.services.passwordless.tokens.find(
({ email: tokenEmail, token }) =>
SHA256(selector.email + sequence) === token &&
selector.email === tokenEmail
);
if (foundTokenEmail) {
return { ...result, verifiedEmail: foundTokenEmail.email };
}
result.error = Accounts._handleError('Email or token mismatch', false);
return result;
}
if (sequence && SHA256(user._id + sequence) === userToken) {
return result;
}
result.error = Accounts._handleError('Token mismatch', false);
return result;
};

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,12 @@
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
},
"meteor-promise": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/meteor-promise/-/meteor-promise-0.9.0.tgz",
"integrity": "sha512-O1Fj1Oa5FfyIkAkDtZVnoYYEIC3miy7lvEeIQZVYunGSbOuivSbfAiPPsD+P45WNlcBALhUo94UzlHeIKBYNuQ=="
"version": "1.0.0-alpha.0",
"resolved": "https://registry.npmjs.org/meteor-promise/-/meteor-promise-1.0.0-alpha.0.tgz",
"integrity": "sha512-f0WbzHSkAqzaQW+LSVhj/XES9dnxNqiKj/qd18Dj0Mt6znt0+f+PYFEsO9PkLdHnIJzvX1iHDjfHvLzpTNPymw=="
},
"promise": {
"version": "8.1.0",