mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Update accounts-password to use argon2 instead of bcrypt
Users that have a bcrypt password stored get progressively migrated to argon2 when their password is checked by `checkPasswordAsync`
This commit is contained in:
8
packages/accounts-base/accounts-base.d.ts
vendored
8
packages/accounts-base/accounts-base.d.ts
vendored
@@ -81,7 +81,7 @@ export namespace Accounts {
|
||||
passwordEnrollTokenExpiration?: number | undefined;
|
||||
passwordEnrollTokenExpirationInDays?: number | undefined;
|
||||
ambiguousErrorMessages?: boolean | undefined;
|
||||
bcryptRounds?: number | undefined;
|
||||
argon2Iterations?: number | undefined;
|
||||
defaultFieldSelector?: { [key: string]: 0 | 1 } | undefined;
|
||||
collection?: string | undefined;
|
||||
loginTokenExpirationHours?: number | undefined;
|
||||
@@ -353,10 +353,10 @@ export namespace Accounts {
|
||||
|
||||
/**
|
||||
*
|
||||
* Check whether the provided password matches the bcrypt'ed password in
|
||||
* Check whether the provided password matches the argon2'ed password in
|
||||
* the database user record. `password` can be a string (in which case
|
||||
* it will be run through SHA256 before bcrypt) or an object with
|
||||
* properties `digest` and `algorithm` (in which case we bcrypt
|
||||
* it will be run through SHA256 before argon2) or an object with
|
||||
* properties `digest` and `algorithm` (in which case we argon2
|
||||
* `password.digest`).
|
||||
*/
|
||||
function _checkPasswordAsync(
|
||||
|
||||
@@ -13,7 +13,7 @@ const VALID_CONFIG_KEYS = [
|
||||
'passwordEnrollTokenExpirationInDays',
|
||||
'passwordEnrollTokenExpiration',
|
||||
'ambiguousErrorMessages',
|
||||
'bcryptRounds',
|
||||
'argon2Iterations',
|
||||
'defaultFieldSelector',
|
||||
'collection',
|
||||
'loginTokenExpirationHours',
|
||||
@@ -226,8 +226,8 @@ export class AccountsCommon {
|
||||
// - ambiguousErrorMessages {Boolean}
|
||||
// Return ambiguous error messages from login failures to prevent
|
||||
// user enumeration.
|
||||
// - bcryptRounds {Number}
|
||||
// Allows override of number of bcrypt rounds (aka work factor) used
|
||||
// - argon2Iterations {Number}
|
||||
// Allows override of number of argon2 iterations (aka time cost) used
|
||||
// to store passwords.
|
||||
|
||||
/**
|
||||
@@ -245,7 +245,7 @@ export class AccountsCommon {
|
||||
* @param {Number} options.passwordEnrollTokenExpirationInDays The number of days from when a link to set initial password is sent until token expires and user can't set password with the link anymore. Defaults to 30.
|
||||
* @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 `true`.
|
||||
* @param {Number} options.bcryptRounds Allows override of number of bcrypt rounds (aka work factor) used to store passwords. The default is 10.
|
||||
* @param {Number} options.argon2Iterations Allows override of number of argon2 iterations (aka time cost) used to store passwords. The default is 3.
|
||||
* @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 {String|Mongo.Collection} options.collection A collection name or a Mongo.Collection object to hold the users.
|
||||
* @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.
|
||||
|
||||
@@ -10,6 +10,7 @@ Package.describe({
|
||||
|
||||
Npm.depends({
|
||||
bcrypt: "5.0.1",
|
||||
argon2: "0.41.1",
|
||||
});
|
||||
|
||||
Package.onUse((api) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { hash as bcryptHash, compare as bcryptCompare } from 'bcrypt';
|
||||
import argon2 from "argon2";
|
||||
import { compare as bcryptCompare } from "bcrypt";
|
||||
import { Accounts } from "meteor/accounts-base";
|
||||
|
||||
// Utility for grabbing user
|
||||
@@ -6,8 +7,9 @@ const getUserById =
|
||||
async (id, options) =>
|
||||
await Meteor.users.findOneAsync(id, Accounts._addDefaultFieldSelector(options));
|
||||
|
||||
// User records have a 'services.password.bcrypt' field on them to hold
|
||||
// their hashed passwords.
|
||||
// User records have two fields that are used for password-based login:
|
||||
// - 'services.password.bcrypt', which stores the bcrypt password, which is now deprecated
|
||||
// - 'services.password.argon2', which stores the argon2 password
|
||||
//
|
||||
// When the client sends a password to the server, it can either be a
|
||||
// string (the plaintext password) or an object with keys 'digest' and
|
||||
@@ -17,18 +19,25 @@ const getUserById =
|
||||
// strings.
|
||||
//
|
||||
// When the server receives a plaintext password as a string, it always
|
||||
// hashes it with SHA256 before passing it into bcrypt. When the server
|
||||
// hashes it with SHA256 before passing it into argon2. When the server
|
||||
// receives a password as an object, it asserts that the algorithm is
|
||||
// "sha-256" and then passes the digest to bcrypt.
|
||||
// "sha-256" and then passes the digest to argon2.
|
||||
|
||||
Accounts._argon2Iterations = () => Accounts._options.argon2Iterations || 3;
|
||||
|
||||
Accounts._bcryptRounds = () => Accounts._options.bcryptRounds || 10;
|
||||
|
||||
// Given a 'password' from the client, extract the string that we should
|
||||
// bcrypt. 'password' can be one of:
|
||||
// - String (the plaintext password)
|
||||
// - Object with 'digest' and 'algorithm' keys. 'algorithm' must be "sha-256".
|
||||
//
|
||||
/**
|
||||
* Extracts the string to be encrypted using Argon2 from the given `password`.
|
||||
*
|
||||
* @param {string|Object} password - The password provided by the client. It can be:
|
||||
* - A plaintext string password.
|
||||
* - An object with the following properties:
|
||||
* @property {string} digest - The hashed password.
|
||||
* @property {string} algorithm - The hashing algorithm used. Must be "sha-256".
|
||||
*
|
||||
* @returns {string} - The resulting password string to encrypt.
|
||||
*
|
||||
* @throws {Error} - If the `algorithm` in the password object is not "sha-256".
|
||||
*/
|
||||
const getPasswordString = password => {
|
||||
if (typeof password === "string") {
|
||||
password = SHA256(password);
|
||||
@@ -42,65 +51,137 @@ const getPasswordString = password => {
|
||||
return password;
|
||||
};
|
||||
|
||||
// Use bcrypt to hash the password for storage in the database.
|
||||
// Use argon2 to hash the password for storage in the database.
|
||||
// `password` can be a string (in which case it will be run through
|
||||
// SHA256 before bcrypt) or an object with properties `digest` and
|
||||
// `algorithm` (in which case we bcrypt `password.digest`).
|
||||
// SHA256 before argon2) or an object with properties `digest` and
|
||||
// `algorithm` (in which case we argon2 `password.digest`).
|
||||
//
|
||||
const hashPassword = async password => {
|
||||
password = getPasswordString(password);
|
||||
return await bcryptHash(password, Accounts._bcryptRounds());
|
||||
return await argon2.hash(password, {
|
||||
timeCost: Accounts._argon2Iterations(),
|
||||
type: argon2.argon2id
|
||||
});
|
||||
};
|
||||
|
||||
// Extract the number of rounds used in the specified bcrypt hash.
|
||||
const getRoundsFromBcryptHash = hash => {
|
||||
let rounds;
|
||||
if (hash) {
|
||||
const hashSegments = hash.split('$');
|
||||
if (hashSegments.length > 2) {
|
||||
rounds = parseInt(hashSegments[2], 10);
|
||||
/**
|
||||
* Extract the number of iterations used in the specified argon2 hash
|
||||
* @param hash String
|
||||
* @returns {null|number}
|
||||
*/
|
||||
const getIterationsFromArgon2Hash = function(hash) {
|
||||
const parts = hash?.split("$") || [];
|
||||
if (parts.length < 4 || !parts[1].startsWith("argon2")) {
|
||||
throw new Error("Invalid Argon2 hash format");
|
||||
}
|
||||
|
||||
const params = parts[3].split(",");
|
||||
let iterations = null;
|
||||
|
||||
for (const param of params) {
|
||||
if (param.startsWith("t=")) {
|
||||
iterations = parseInt(param.split("=")[1], 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return rounds;
|
||||
|
||||
if (iterations === null) {
|
||||
throw new Error("Iterations parameter not found in the hash");
|
||||
}
|
||||
|
||||
return iterations;
|
||||
};
|
||||
Accounts._getIterationsFromArgon2Hash = getIterationsFromArgon2Hash;
|
||||
|
||||
const getUserPasswordHash = user => {
|
||||
return user.services?.password?.argon2 || user.services?.password?.bcrypt;
|
||||
};
|
||||
|
||||
// Check whether the provided password matches the bcrypt'ed password in
|
||||
// the database user record. `password` can be a string (in which case
|
||||
// it will be run through SHA256 before bcrypt) or an object with
|
||||
// properties `digest` and `algorithm` (in which case we bcrypt
|
||||
// `password.digest`).
|
||||
//
|
||||
// The user parameter needs at least user._id and user.services
|
||||
Accounts._checkPasswordUserFields = {_id: 1, services: 1};
|
||||
//
|
||||
Accounts._checkPasswordUserFields = { _id: 1, services: 1 };
|
||||
|
||||
/**
|
||||
* Checks whether the provided password matches the hashed password stored in the user's database record.
|
||||
*
|
||||
* @param {Object} user - The user object containing at least:
|
||||
* @property {string} _id - The user's unique identifier.
|
||||
* @property {Object} services - The user's services data.
|
||||
* @property {Object} services.password - The user's password object.
|
||||
* @property {string} [services.password.argon2] - The Argon2 hashed password.
|
||||
* @property {string} [services.password.bcrypt] - The bcrypt hashed password, deprecated
|
||||
*
|
||||
* @param {string|Object} password - The password provided by the client. It can be:
|
||||
* - A plaintext string password.
|
||||
* - An object with the following properties:
|
||||
* @property {string} digest - The hashed password.
|
||||
* @property {string} algorithm - The hashing algorithm used. Must be "sha-256".
|
||||
*
|
||||
* @returns {Promise<Object>} - A result object with the following properties:
|
||||
* @property {string} userId - The user's unique identifier.
|
||||
* @property {Object} [error] - An error object if the password does not match or an error occurs.
|
||||
*
|
||||
* @throws {Error} - If an unexpected error occurs during the process.
|
||||
*/
|
||||
const checkPasswordAsync = async (user, password) => {
|
||||
const result = {
|
||||
userId: user._id
|
||||
};
|
||||
|
||||
const formattedPassword = getPasswordString(password);
|
||||
const hash = user.services.password.bcrypt;
|
||||
const hashRounds = getRoundsFromBcryptHash(hash);
|
||||
const hash = getUserPasswordHash(user);
|
||||
|
||||
if (! await bcryptCompare(formattedPassword, hash)) {
|
||||
result.error = Accounts._handleError("Incorrect password", false);
|
||||
} else if (hash && Accounts._bcryptRounds() != hashRounds) {
|
||||
// The password checks out, but the user's bcrypt hash needs to be updated.
|
||||
|
||||
Meteor.defer(async () => {
|
||||
await Meteor.users.updateAsync({ _id: user._id }, {
|
||||
$set: {
|
||||
'services.password.bcrypt':
|
||||
await bcryptHash(formattedPassword, Accounts._bcryptRounds())
|
||||
}
|
||||
// bcrypt hashes start with $2a$ or $2b$
|
||||
// argon2 hashes start with $argon2i$, $argon2d$ or $argon2id$
|
||||
if (hash.startsWith("$2")) {
|
||||
// migration code from bcrypt to argon2
|
||||
const match = await bcryptCompare(formattedPassword, hash);
|
||||
if (!match) {
|
||||
result.error = Accounts._handleError("Incorrect password", false);
|
||||
}
|
||||
else {
|
||||
// The password checks out, but the user's stored password needs to be updated to argon2
|
||||
Meteor.defer(async () => {
|
||||
await Meteor.users.updateAsync(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
"services.password.argon2":
|
||||
await hashPassword(password)
|
||||
},
|
||||
$unset: {
|
||||
"services.password.bcrypt": 1
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// argon2 password
|
||||
const argon2Iterations = getIterationsFromArgon2Hash(hash);
|
||||
|
||||
if (!(await argon2.verify(hash, formattedPassword))) {
|
||||
result.error = Accounts._handleError("Incorrect password", false);
|
||||
}
|
||||
else if (hash && Accounts._argon2Iterations() !== argon2Iterations) {
|
||||
// The password checks out, but the user's argon2 hash needs to be updated with the right number of iterations
|
||||
Meteor.defer(async () => {
|
||||
await Meteor.users.updateAsync(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
"services.password.argon2":
|
||||
await hashPassword(formattedPassword)
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
Accounts._checkPasswordAsync = checkPasswordAsync;
|
||||
Accounts._checkPasswordAsync = checkPasswordAsync;
|
||||
|
||||
///
|
||||
/// LOGIN
|
||||
@@ -185,9 +266,7 @@ Accounts.registerLoginHandler("password", async options => {
|
||||
Accounts._handleError("User not found");
|
||||
}
|
||||
|
||||
|
||||
if (!user.services || !user.services.password ||
|
||||
!user.services.password.bcrypt) {
|
||||
if (!getUserPasswordHash(user)) {
|
||||
Accounts._handleError("User has no password set");
|
||||
}
|
||||
|
||||
@@ -283,7 +362,7 @@ Meteor.methods(
|
||||
Accounts._handleError("User not found");
|
||||
}
|
||||
|
||||
if (!user.services || !user.services.password || !user.services.password.bcrypt) {
|
||||
if (!getUserPasswordHash(user)) {
|
||||
Accounts._handleError("User has no password set");
|
||||
}
|
||||
|
||||
@@ -302,11 +381,14 @@ Meteor.methods(
|
||||
await Meteor.users.updateAsync(
|
||||
{ _id: this.userId },
|
||||
{
|
||||
$set: { 'services.password.bcrypt': hashed },
|
||||
$set: { 'services.password.argon2': hashed },
|
||||
$pull: {
|
||||
'services.resume.loginTokens': { hashedToken: { $ne: currentToken } }
|
||||
},
|
||||
$unset: { 'services.password.reset': 1 }
|
||||
$unset: {
|
||||
'services.password.reset': 1,
|
||||
'services.password.bcrypt': 1
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -320,7 +402,7 @@ Meteor.methods(
|
||||
* @summary Forcibly change the password for a user.
|
||||
* @locus Server
|
||||
* @param {String} userId The id of the user to update.
|
||||
* @param {String} newPassword A new password for the user.
|
||||
* @param {String} newPlaintextPassword A new password for the user.
|
||||
* @param {Object} [options]
|
||||
* @param {Object} options.logout Logout all current connections with this userId (default: true)
|
||||
* @importFromPackage accounts-base
|
||||
@@ -339,9 +421,12 @@ Accounts.setPasswordAsync =
|
||||
|
||||
const update = {
|
||||
$unset: {
|
||||
'services.password.reset': 1
|
||||
'services.password.reset': 1,
|
||||
'services.password.bcrypt': 1
|
||||
},
|
||||
$set: {'services.password.bcrypt': await hashPassword(newPlaintextPassword)}
|
||||
$set: {
|
||||
'services.password.argon2': await hashPassword(newPlaintextPassword)
|
||||
}
|
||||
};
|
||||
|
||||
if (options.logout) {
|
||||
@@ -430,25 +515,32 @@ Accounts.generateResetToken =
|
||||
// if this method is called from the enroll account work-flow then
|
||||
// store the token record in 'services.password.enroll' db field
|
||||
// else store the token record in in 'services.password.reset' db field
|
||||
if(reason === 'enrollAccount') {
|
||||
await Meteor.users.updateAsync({_id: user._id}, {
|
||||
$set : {
|
||||
'services.password.enroll': tokenRecord
|
||||
if (reason === "enrollAccount") {
|
||||
await Meteor.users.updateAsync(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
"services.password.enroll": tokenRecord
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
// before passing to template, update user object with new token
|
||||
Meteor._ensure(user, 'services', 'password').enroll = tokenRecord;
|
||||
} else {
|
||||
await Meteor.users.updateAsync({_id: user._id}, {
|
||||
$set : {
|
||||
'services.password.reset': tokenRecord
|
||||
Meteor._ensure(user, "services", "password").enroll = tokenRecord;
|
||||
}
|
||||
else {
|
||||
await Meteor.users.updateAsync(
|
||||
{ _id: user._id },
|
||||
{
|
||||
$set: {
|
||||
"services.password.reset": tokenRecord
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
// before passing to template, update user object with new token
|
||||
Meteor._ensure(user, 'services', 'password').reset = tokenRecord;
|
||||
Meteor._ensure(user, "services", "password").reset = tokenRecord;
|
||||
}
|
||||
|
||||
return {email, user, token};
|
||||
return { email, user, token };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -669,10 +761,13 @@ Meteor.methods(
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'services.password.bcrypt': hashed,
|
||||
'services.password.argon2': hashed,
|
||||
'emails.$.verified': true
|
||||
},
|
||||
$unset: { 'services.password.enroll': 1 }
|
||||
$unset: {
|
||||
'services.password.enroll': 1,
|
||||
'services.password.bcrypt': 1,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
affectedRecords = await Meteor.users.updateAsync(
|
||||
@@ -683,10 +778,13 @@ Meteor.methods(
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'services.password.bcrypt': hashed,
|
||||
'services.password.argon2': hashed,
|
||||
'emails.$.verified': true
|
||||
},
|
||||
$unset: { 'services.password.reset': 1 }
|
||||
$unset: {
|
||||
'services.password.reset': 1,
|
||||
'services.password.bcrypt': 1,
|
||||
}
|
||||
});
|
||||
}
|
||||
if (affectedRecords !== 1)
|
||||
@@ -990,7 +1088,7 @@ const createUser =
|
||||
const user = { services: {} };
|
||||
if (password) {
|
||||
const hashed = await hashPassword(password);
|
||||
user.services.password = { bcrypt: hashed };
|
||||
user.services.password = { argon2: hashed };
|
||||
}
|
||||
|
||||
return await Accounts._createUserCheckingDuplicates({ user, email, username, options });
|
||||
|
||||
@@ -1171,7 +1171,7 @@ if (Meteor.isServer) (() => {
|
||||
// set a new password.
|
||||
await Accounts.setPasswordAsync(userId, 'new password');
|
||||
user = await Meteor.users.findOneAsync(userId);
|
||||
const oldSaltedHash = user.services.password.bcrypt;
|
||||
const oldSaltedHash = user.services.password.argon2;
|
||||
test.isTrue(oldSaltedHash);
|
||||
// Send a reset password email (setting a reset token) and insert a login
|
||||
// token.
|
||||
@@ -1184,7 +1184,7 @@ if (Meteor.isServer) (() => {
|
||||
// reset with the same password, see we get a different salted hash
|
||||
await Accounts.setPasswordAsync(userId, 'new password', { logout: false });
|
||||
user = await Meteor.users.findOneAsync(userId);
|
||||
const newSaltedHash = user.services.password.bcrypt;
|
||||
const newSaltedHash = user.services.password.argon2;
|
||||
test.isTrue(newSaltedHash);
|
||||
test.notEqual(oldSaltedHash, newSaltedHash);
|
||||
// No more reset token.
|
||||
@@ -1196,7 +1196,7 @@ if (Meteor.isServer) (() => {
|
||||
// reset again, see that the login tokens are gone.
|
||||
await Accounts.setPasswordAsync(userId, 'new password');
|
||||
user = await Meteor.users.findOneAsync(userId);
|
||||
const newerSaltedHash = user.services.password.bcrypt;
|
||||
const newerSaltedHash = user.services.password.argon2;
|
||||
test.isTrue(newerSaltedHash);
|
||||
test.notEqual(oldSaltedHash, newerSaltedHash);
|
||||
test.notEqual(newSaltedHash, newerSaltedHash);
|
||||
@@ -1822,28 +1822,32 @@ if (Meteor.isServer) (() => {
|
||||
]);
|
||||
});
|
||||
|
||||
const getUserHashRounds = user =>
|
||||
Number(user.services.password.bcrypt.substring(4, 6));
|
||||
testAsyncMulti("passwords - allow custom bcrypt rounds",[
|
||||
const getUserHashArgon2Iterations = function (user) {
|
||||
const hash = user?.services?.password?.argon2;
|
||||
return Accounts._getIterationsFromArgon2Hash(hash);
|
||||
}
|
||||
|
||||
testAsyncMulti("passwords - allow custom argon2 iterations",[
|
||||
async function (test) {
|
||||
// Verify that a bcrypt hash generated for a new account uses the
|
||||
// Verify that a argon2 hash generated for a new account uses the
|
||||
// default number of iterations.
|
||||
let username = Random.id();
|
||||
this.password = hashPassword('abc123');
|
||||
this.userId1 = await Accounts.createUser({ username, password: this.password });
|
||||
this.user1 = await Meteor.users.findOneAsync(this.userId1);
|
||||
let rounds = getUserHashRounds(this.user1);
|
||||
test.equal(rounds, Accounts._bcryptRounds());
|
||||
let rounds = getUserHashArgon2Iterations(this.user1);
|
||||
test.equal(rounds, Accounts._argon2Iterations());
|
||||
|
||||
// When a custom number of bcrypt rounds is set via Accounts.config,
|
||||
// and an account was already created using the default number of rounds,
|
||||
// When a custom number of argon2 iterations is set via Accounts.config,
|
||||
// and an account was already created using the default number of iterations,
|
||||
// make sure that a new hash is created (and stored) using the new number
|
||||
// of rounds, the next time the password is checked.
|
||||
this.customRounds = 11;
|
||||
Accounts._options.bcryptRounds = this.customRounds;
|
||||
// of iterations, the next time the password is checked.
|
||||
this.customIterations = 4;
|
||||
Accounts._options.argon2Iterations = this.customIterations;
|
||||
await Accounts._checkPasswordAsync(this.user1, this.password);
|
||||
},
|
||||
async function(test) {
|
||||
const defaultRounds = Accounts._bcryptRounds();
|
||||
const defaultRounds = Accounts._argon2Iterations();
|
||||
let rounds;
|
||||
let username;
|
||||
|
||||
@@ -1852,18 +1856,18 @@ if (Meteor.isServer) (() => {
|
||||
|
||||
Meteor.setTimeout(async () => {
|
||||
this.user1 = await Meteor.users.findOneAsync(this.userId1);
|
||||
rounds = getUserHashRounds(this.user1);
|
||||
test.equal(rounds, this.customRounds);
|
||||
// When a custom number of bcrypt rounds is set, make sure it's
|
||||
// used for new bcrypt password hashes.
|
||||
rounds = getUserHashArgon2Iterations(this.user1);
|
||||
test.equal(rounds, this.customIterations);
|
||||
// When a custom number of argon2 iterations is set, make sure it's
|
||||
// used for new argon2 password hashes.
|
||||
username = Random.id();
|
||||
const userId2 = await Accounts.createUser({ username, password: this.password });
|
||||
const user2 = await Meteor.users.findOneAsync(userId2);
|
||||
rounds = getUserHashRounds(user2);
|
||||
test.equal(rounds, this.customRounds);
|
||||
rounds = getUserHashArgon2Iterations(user2);
|
||||
test.equal(rounds, this.customIterations);
|
||||
|
||||
// Cleanup
|
||||
Accounts._options.bcryptRounds = defaultRounds;
|
||||
Accounts._options.argon2Iterations = defaultRounds;
|
||||
await Meteor.users.removeAsync(this.userId1);
|
||||
await Meteor.users.removeAsync(userId2);
|
||||
resolve();
|
||||
@@ -1871,7 +1875,7 @@ if (Meteor.isServer) (() => {
|
||||
|
||||
return promise;
|
||||
}
|
||||
]); // default number of rounds.
|
||||
]);
|
||||
|
||||
|
||||
Tinytest.addAsync('passwords - extra params in email urls',
|
||||
|
||||
@@ -773,7 +773,7 @@ sign-in process, it also supports email-based sign-in including
|
||||
address verification and password recovery emails.
|
||||
|
||||
The Meteor server stores passwords using the
|
||||
[bcrypt](http://en.wikipedia.org/wiki/Bcrypt) algorithm. This helps
|
||||
[argon2](http://en.wikipedia.org/wiki/Argon2) algorithm. This helps
|
||||
protect against embarrassing password leaks if the server's database is
|
||||
compromised.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user