This post is likely gonna get less relevant when Magento 2 comes out, but I decided to put it out there any way. In my limited time working with Magento I’ve come across a couple scenario’s I wanted to touch on. Hope it’s helpful for anyone.
Let say a merchant wants a new Magento online store. Now, there are a couple scenario’s that are likely or less likely:
- The previous store was built with Magento. Customers can easily be imported using the various tools available (Magmi, import/export, dataflow, etc.)
- The previous store was built on a platform that stores passwords in plaintext - fortunately this is not very likely ;)
- The previous store was built on a different platform, using different hashing algorithms.
- The previous store was built on a different platform which happens to use the same hashing algorithm as Magento,
- A combination of #3 and #4 (yes - I’ve seen it).
Lets discuss #2 to #5.
Importing from a platform that stores passwords in plaintext
This one is easy. You can create a new customer object, set the password for it and it will automatically convert it into a hash when you save it. Check out the beforeSave
method in Mage_Customer_Model_Customer_Attribute_Backend_Password
.
A simple use case:
$customer = Mage::getModel(‘customer/customer’)
->setFirstname($firstname)
->setLastname($lastname)
->setEmail($email)
->setWebsite($website)
->setPassword($password)
->setImportMode(true)
->save();
I like to create my own advanced dataflow adapters. Their logic is simple, it doesn’t time out since it uses multiple (AJAX) calls to import a batch, and it uses the main customer model to access the persistent storage, so I know all required business logic will be executed.
It’s also easy to map the CSV the client gave you - which always has a different format ;) - to what Magento expects.
Creating an adapter that processes a CSV export from the Zencarts customer table can look like this:
class EI_ZencartCustomerImport_Model_Convert_Adapter_Customer
extends Mage_Customer_Model_Convert_Adapter_Customer
{
public function saveRow($data)
{
// I like working with objects better than arrays
$zenCustomer = new Varien_Object($data);
$customerData = array(
'website' => 'main',
'email' => trim($zenCustomer->getCustomersEmailAddress()),
'group' => 'General',
'firstname' => trim($zenCustomer->getCustomersFirstname()),
'lastname' => trim($zenCustomer->getCustomersLastname()),
'password_hash' => $zenCustomer->getCustomersPassword()
);
// Let's temporarily disable all email communication for this request
// just to be sure it won't send out any emails
Mage::app()->getStore()->setConfig('system/smtp/disable', 1);
parent::saveRow($customerData);
return $this;
}
}
One thing you want to take note of is passwords with less than 6 characters. You can prompt customers for their password to be reset because of security concerns.
Basically, it involves creating an attribute - essentially a flag - called “Password needs reset” for the customer. You turn on the flag when you're importing a customer whose password is less than 6 characters. When the customer tries to login and the flag is on, you redirect them to the forgot your password page (rename it to be a better fit, “reset password”) telling them their password is insecure and they need to update it before continuing.
Be aware though that you need to remove the 6 character password-validation for the login form to be submitted in the first place.
Importing from a platform that uses an incompatible password hash
If we follow customer authentication method stack down to where the hashes come into play, we would find the following:
Mage_Customer_Model_Customer::authenticate
Mage_Customer_Model_Customer::validatePassword
Mage_Core_Helper_Data::validateHash
Mage_Core_Helper_Data::getEncryptor
Mage_Core_Model_Encryption::validateHash
Mage_Core_Model_Encryption::hash
As you can see, somewhere in that trace Mage_Core_Helper_Data::getEncryptor
gets called. If we look at the method, we find out that we can inject a different encryptor by changing the following config node: global/helpers/core/encryption_model
.
If you’re using multiple stores or websites and some of them have a different algorithm, it gets a bit more complicated. You could do a check in your encryptor to check which website we're in right now and you would also need to make sure it works when you create a customer in the backend (possibly using Mage_Core_Model_App_Emulation
).
However, a better solution is to create a fallback in your encrpytor.
For example:
class EI_ZencartCustomerImport_Model_Encryption_Zen
extends Mage_Core_Model_Encryption
{
public function validateHash($password, $hash)
{
$match = parent::validateHash($password, $hash);
if ($match) {
return true;
}
// your own logic ...
}
}
Coincidentally, this is also how you’d do a combination of #3 and #4 - by creating a "fallback" in your encryption model.
I had seen the combination in Zencart, where depending on whether the customer used the create account option, or checkout using PayPal, the hash would either be sha256 or md5 + salt respectively.
Conclusion
Trying to mess around with informing customers of their new passwords is pretty painful - you wouldn't want to send out an email to every customer informing they need to reset their password. It can come across as scammy, phishing mail or simply annoying. There also really isn't a need for it - we've just seen a couple ways we can keep their old passwords. If any, you can force people to reset their password on login attempts.
Closing thoughts
Check out this module that changes Magento's default encryption to a much harder to crack variant: https://github.com/ikonoshirt/pbkdf2
I would like to see an implementation that uses PHP 5.5’s password hash function. It uses bcrypt hashing and takes a parameter “cost” which can be incremented as hardware becomes faster and hashes become easier to brute-force.
Not 100% sure on this, but it seems Magento 2 uses sha265 for hashing customer passwords: Encryptor class in Github.
I also love using dataflow and creating my own adapters for anything that needs to be imported, from attribute option values, to complex products to custom data. It might not be as fast as different methods, but for one-time-imports this is totally fine.