Custom Password Hashing
Overview
There are times when you have a custom password hash that you want to import into FusionAuth. FusionAuth supports a number of password hashing schemes but you can write a custom plugin if you have hashed your passwords using a different scheme.
You can use your custom password hashing scheme going forward, or you can rehash your passwords. You’d use the former strategy if you wanted to use a strong, unsupported password hashing scheme such as Argon2. You’d use the latter strategy if you are migrating from a system with a weaker hashing algorithm.
This code uses the words ‘encryption’ and ‘encryptor’ for backwards compatibility, but what it is really doing is hashing the password. The functionality used to be referred to as Password Encryptors
.
Write the Password Encryptor Class
The main plugin interface in FusionAuth is the Password Encryptors interface. This allows you to write a custom password hashing scheme. A custom password hashing scheme is useful when you import users from an existing database into FusionAuth so that the users don’t need to reset their passwords to login into your applications.
To write a Password Encryptor, you must first implement the io.fusionauth.plugin.spi.security.PasswordEncryptor
interface. Here’s an example Password Encryptor.
Password Encryptor
/*
* Copyright (c) 2019, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package com.mycompany.fusionauth.plugins;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import com.mycompany.fusionauth.util.HexTools;
import io.fusionauth.plugin.spi.security.PasswordEncryptor;
/**
* This is an example of a PBKDF2 HMAC SHA1 Salted hashing algorithm.
*
* <p>
* This code is provided to assist in your deployment and management of FusionAuth. Use of this
* software is not covered under the FusionAuth license agreement and is provided "as is" without
* warranty. https://fusionauth.io/license
* </p>
*
* @author Daniel DeGroff
*/
public class ExamplePBDKF2HMACSHA1PasswordEncryptor implements PasswordEncryptor {
private final int keyLength;
public ExamplePBDKF2HMACSHA1PasswordEncryptor() {
// Default key length is 512 bits
this.keyLength = 64;
}
@Override
public int defaultFactor() {
return 10_000;
}
@Override
public String encrypt(String password, String salt, int factor) {
if (factor <= 0) {
throw new IllegalArgumentException("Invalid factor value [" + factor + "]");
}
SecretKeyFactory keyFactory;
try {
keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No such algorithm [PBKDF2WithHmacSHA1]");
}
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), factor, keyLength * 8);
SecretKey secret;
try {
secret = keyFactory.generateSecret(keySpec);
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException("Could not generate secret key for algorithm [PBKDF2WithHmacSHA1]");
}
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getEncoded(), "HmacSHA1");
Mac mac;
try {
mac = Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No such algorithm [HmacSHA1]");
}
try {
mac.init(secretKeySpec);
byte[] hashedPassword = mac.doFinal(password.getBytes(StandardCharsets.UTF_8));
return HexTools.encode(hashedPassword);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("Invalid key used to initialize HmacSHA1");
}
}
}
Adding the Guice Bindings
To complete the main plugin code (before we write a unit test), you need to add Guice binding for your new Password Encryptor. Password Encryptors use Guice Multibindings via Map. Here is an example of binding our new Password Encryptor so that FusionAuth can use it for users.
Guice Module
/*
* Copyright (c) 2020-2022, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package com.mycompany.fusionauth.plugins.guice;
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.MapBinder;
import com.mycompany.fusionauth.plugins.ExamplePBDKF2HMACSHA1PasswordEncryptor;
import io.fusionauth.plugin.spi.PluginModule;
import io.fusionauth.plugin.spi.security.PasswordEncryptor;
/**
* @author Daniel DeGroff
*/
@PluginModule
public class MyExampleFusionAuthPluginModule extends AbstractModule {
@Override
protected void configure() {
MapBinder<String, PasswordEncryptor> passwordEncryptorMapBinder = MapBinder.newMapBinder(binder(), String.class, PasswordEncryptor.class);
// TODO :
// 1. Add one or more bindings here
// 2. Name your binding. This will be the value you set in the 'encryptionScheme' on the user to utilize this encryptor.
// 3. Delete any example code you don't use and do not want in your plugin. In addition to the bindings, you should delete any corresponding classes and tests you do not use in your plugin.
// Example PBKDF2 with a SHA-1
passwordEncryptorMapBinder.addBinding("example-salted-pbkdf2-hmac-sha1-10000").to(ExamplePBDKF2HMACSHA1PasswordEncryptor.class);
}
}
You can see that we have bound the Password Encryptor under the name example-salted-pbkdf2-hmac-sha1-10000
. This is the same name that you will use when creating users via the User API.
Writing a Unit Test
You’ll probably want to write some tests to ensure that your new Password Encryptor is working properly. Our example uses TestNG, but you can use JUnit or another framework if you prefer. Here’s a simple unit test for our Password Encryptor:
Unit Test
package com.mycompany.fusionauth.plugins;
import io.fusionauth.plugin.spi.security.PasswordEncryptor;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
/**
* @author Daniel DeGroff
*/
public class ExamplePBDKF2HMACSHA1PasswordEncryptorTest {
@Test(dataProvider = "hashes")
public void encrypt(String password, String salt, String hash) {
PasswordEncryptor encryptor = new ExamplePBDKF2HMACSHA1PasswordEncryptor();
assertEquals(encryptor.encrypt(password, salt, 10_000), hash);
}
@DataProvider(name = "hashes")
public Object[][] hashes() {
return new Object[][]{
{"password123", "1484161696d0ca62390273b98846f49671cecd78", "4761D3392092F9CA6036B53DC92C6D7F3D597576"},
{"password123", "ea95629c7954d73ea670f07a798e9fd4ab907593", "9480AD9A59CB5053B832BA5E731AFCD1F78068EC"},
};
}
}
To run the tests using the Java Maven build tool, run the following command.
mvn test
Integration Test
After you have completed your plugin, the unit test and installed the plugin into a running FusionAuth installation, you can test it by hitting the User API and creating a test user. Here’s an example JSON request that uses the new Password Encryptor:
{
"user": {
"id": "00000000-0000-0000-0000-000000000001",
"active": true,
"email": "test0@fusionauth.io",
"encryptionScheme": "example-salted-pbkdf2-hmac-sha1-10000",
"password": "password",
"username": "username0",
"timezone": "Denver",
"data": {
"attr1": "value1",
"attr2": ["value2", "value3"]
},
"preferredLanguages": ["en", "fr"],
"registrations": [
{
"applicationId": "00000000-0000-0000-0000-000000000042",
"data": {
"attr3": "value3",
"attr4": ["value4", "value5"]
},
"id": "00000000-0000-0000-0000-000000000003",
"preferredLanguages": ["de"],
"roles": ["role 1"],
"username": "username0"
}
]
}
}
Notice that we’ve passed in the encryptionScheme
property with a value of example-salted-pbkdf2-hmac-sha1-10000
. This will instruct FusionAuth to use your newly written Password Encryptor.
Sample Code
A sample plugin project is available. If you are looking to write your own custom password hashing algorithm, this project is a good starting point.
There is also a selection of contributed plugins, provided by the community and made available without warranty. That may also be useful to you, as someone may already have written the hasher you need.
Rehashing User Passwords
The purpose of writing a custom password hasher is to import users into FusionAuth using an existing hashing scheme. This allows you to seamlessly import your users without requiring them to change their password. The downside of this approach is that you now have preserved a hash which may be weak. FusionAuth will continue to use that hash unless you rehash users’ passwords.
To remedy this common situation, FusionAuth has the ability to rehash passwords on user login. Once enabled, during the next login event for a given user, FusionAuth will transparently rehash that user’s password. The stronger, more secure hash will be used in the future for that user.
To import users and transparently rehash their passwords, do the following:
- Write a custom password hasher.
- Import user passwords, setting the scheme for each user to the custom password hasher.
- Decide on the new hashing scheme you want to use.
- In the administrative user interface, navigate to Tenants -> Your Tenant -> Password and then to the Cryptographic hash settings section. Here you will configure both the new scheme and the rehash on login behavior.
- Configure the tenant to use the new hashing scheme by selecting it. You may use one of the standard hashing schemes or a different custom scheme. This will be used for all new users in this tenant as well.
- Configure the tenant to rehash on login by checking the Re-hash on login checkbox.
- Save the tenant configuration.
After you have enabled this, when a user logs in, the password they provide will be transparently rehashed and they will use the stronger scheme in the future.
Beginning in version 1.42.0
, when this configuration is enabled, in addition to re-hashing on login, the password will be re-hashed on password change.
Deleting Plugins
You should never delete an installed password hashing plugin unless you are certain that no users are using the hash provided by that plugin. If any users still have that encryptionScheme
associated with their account, they will see undefined behavior when they try to log in or change their password. This may include runtime exceptions.
The only way to completely and safely delete a custom password hashing plugin is to ensure all user accounts have migrated off of the algorithm defined by the plugin.
However, the hashing scheme is associated with each account and cannot be queried by the User Search API. You can view it for an individual user by navigating to Users -> Your User -> Manage and choosing the Source tab, but you can’t query this value for all users.
If you have a plan which includes support and need to know which users are using a particular password hashing scheme, open a support ticket so that the team can assist.
The FusionAuth recommendation is to leave all password hashing plugins in place once installed.
If you are sure you need to remove a password hashing plugin, take the following steps:
- Ensure that no users are using the
encryptionScheme
associated with this plugin assisted by the FusionAuth support team as mentioned above. - Ensure that no tenants are using the
encryptionScheme
associated with this plugin. This can be verified by querying all the tenants and looking at thetenant.passwordEncryptionConfiguration.encryptionScheme
value. - Delete the plugin from the filesystem.
- Restart the FusionAuth application on all nodes.