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

include::https://raw.githubusercontent.com/FusionAuth/fusionauth-example-password-encryptor/master/src/test/java/com/mycompany/fusionauth/plugins/ExamplePBDKF2HMACSHA1PasswordEncryptorTest.java[]

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:

Tenant settings for rehashing passwords on login

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.

Currently rehashing a password when it is changed is not supported. Here’s the tracking issue for this feature.