Writing a Plugin

Install Java 8

If you haven’t already, you need to download the Java 8 JDK. This contains all of the tools you need to compile Java projects and create JAR files. You can download the Java 8 JDK from the Oracle at the following link.

Get the Example Code

To begin, clone the following GitHub repository. This example code will assume you are able to use the Maven build commands.

git clone git@github.com:FusionAuth/fusionauth-example-password-encryptor.git
cd fusionauth-example-password-encryptor
mvn compile package

If the above commands were successful you have now downloaded the example code and performed and initial build of the jar. If last command was not found, you do not yet have Maven build tool installed. You may utilize whatever Java build system you prefer, such as Maven, Ant, Gradle or Savant. This example wil use the Maven build system.

The following is a representation of the plugin project layout.

Plugin Project Layout

fusionauth-example-password-encryptor
  |
  |- src
  |   |- main
  |   |   |- java
  |   |
  |   |- test
  |       |- java
  |
  |- pom.xml

Following the Java convention of using packages for all classes, you will want to create sub-directories under src/main/java and src/test/java that identify the package for your plugin. For example, you might create a directory under each called com/mycompany/fusionauth/plugins that will contain your plugin code.

In the example code you are beginning from, you will find the Plugin MyExamplePasswordEncryptor in the package com/mycompany/fusionauth/plugins.

Your project is now ready for you to start coding.

Edit Your Build File

To modify the name of your plugin, edit your build file. Below is a small portion of the build file, you may change whatever you like, but to start with you will want to change the groupId and the artifactId. These values are used to name the jar that will be built later.

pom.xml

  <groupId>io.fusionauth</groupId>
  <artifactId>fusionauth-example-password-encryptor</artifactId>
  <version>0.1.0</version>

Code the Plugin

This is where you get to code! Ideally you would have one or two known passwords in the data you will be importing to FusionAuth so that you can test your plugin prior to installing it in FusionAuth.

Begin by modifying the MyExamplePasswordEncryptor class found in src/main/java.

You will find a test ready for you to use called MyExamplePasswordEncryptorTest in the package com/mycompany/fusionauth/plugins in the src/test/java directory. Use this test to assert on one or two known plain text passwords to ensure that the hash you calculate is equal to the actual hash that you will be importing from your existing system.

You can run the test like so: mvn test.

You will also find a few other example plugins that are written and tested that you can review to get an idea of how they work. You may delete any of the example code you do not want in your final jar.

Matching Salt and Encryption

The only two required methods for you to implement are defaultFactor(), and encrypt(String password, String salt, int factor). There are two additional methods that are optional: generateSalt() and validateSalt(String salt).

If you do not implement these methods in your plugin, the default implementations below will be used:

PasswordEncryptor interface with default generateSalt and validateSalt methods

/*
 * Copyright (c) 2018-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 io.fusionauth.plugin.spi.security;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;
import java.util.regex.Pattern;

/**
 * Used to hash user passwords.
 *
 * @author Brian Pontarelli
 */
public interface PasswordEncryptor {
  /**
   * This pattern represents a standard MIME compatible Base64 encoding scheme.
   * <p>
   * This pattern can be used to validate most salts. This won't necessarily confirm the correct length of the salt.
   */
  Pattern Base64SaltPattern = Pattern.compile("^[A-Za-z0-9+/]+=*$");

  /**
   * @return The default factor for this PasswordEncryptor.
   */
  int defaultFactor();

  /**
   * Hashes the given password using the given salt.
   *
   * @param password The password to hash.
   * @param salt     The salt that can optionally be used to increase the security of the password hash. This is expected to be a
   *                 Base64 encoded byte array.
   * @param factor   The load or iteration factor for this hashing operation.
   * @return The hashed password in a Base64 encoded string.
   */
  String encrypt(String password, String salt, int factor);

  /**
   * Generates a random salt that is compatible with the PasswordEncryptor. The default implementation uses two UUIDs
   * and Base 64 encodes them.
   *
   * @return The salt.
   */
  default String generateSalt() {
    ByteBuffer buf = ByteBuffer.allocate(32);
    UUID first = UUID.randomUUID();
    buf.putLong(first.getLeastSignificantBits());
    buf.putLong(first.getMostSignificantBits());
    UUID second = UUID.randomUUID();
    buf.putLong(second.getLeastSignificantBits());
    buf.putLong(second.getMostSignificantBits());
    return Base64.getEncoder().encodeToString(buf.array());
  }

  /**
   * Optionally return a human-readable name that will be used to select this plugin in the UI.
   *
   * @return a display name, or null
   */
  default String pluginDisplayName() {
    return null;
  }

  /**
   * Validates the salt for this PasswordEncryptor. In most cases this is not necessary to implement this method.
   * <p>
   * Most of the password hashes will use a Base64 encoded salt.
   * <p>
   * If you are using a Bcrypt or Bcrypt like algorithm which uses a non-MIME compatible Base64 encoding you will want to
   * implement this to ensure the salt is corrected validated.
   *
   * @param salt the salt!
   * @return true if the salt is valid for this hashing implementation, false if invalid.
   */
  default boolean validateSalt(String salt) {
    // Note, the more common Base64 character set is defined at the top of this file in a regular expression.
    // You can use that as well, but it doesn't validate that the string is a properly encoded value. It only
    // ensures the string does not contain any characters what would otherwise not be in the base64 character set.
    try {
      Base64.getDecoder().decode(salt.getBytes(StandardCharsets.UTF_8));
      return true;
    } catch (Exception e) {
      return false;
    }
  }
}

If your plugin requires a specific salt length differing from the default implementation, implement these methods in your plugin to generate a salt that meets your requirements.

In other words, the salt generated by your plugin should be consumable by the custom encrypt method.

Using Dependencies

Oftentimes you’ll have dependencies for your plugin code. This is especially true if you are leveraging other cryptographic libraries to ensure password hashes are created correctly.

If you are using FusionAuth 1.36.0 or later and have dependencies, you can place dependencies along with your password plugin into a single directory and that will be loaded in a separate class loader. Please note, that while your plugin will be loaded into a new class loader, it will still share the classpath with FusionAuth. For this reason, you should still be cautious when including dependencies. If at all possible reduce dependencies to a minimum.

If you are using FusionAuth version 1.35.0 or before, and have dependencies, make sure you use the maven-shade-plugin plugin to shade your dependencies, or a similar plugin to combine the jar files. Using this strategy, your plugin must be deployed as an uberjar.

Create the Guice binding

FusionAuth uses Guice for dependency injection and also to setup plugins. No matter what type of plugin you are writing, you need to add a single Guice module to your project.

In order for FusionAuth to locate your plugin, the package you put your plugin module into must include a parent package named either plugin or plugins. For example, a plugin class cannot be named com.mycompany.fusionauth.MyExampleFusionAuthPluginModule. Instead, it must be named com.mycompany.fusionauth.plugins.MyExampleFusionAuthPluginModule.

Edit the example Guice module in the src/main/java directory: com/mycompany/fusionauth/plugins/guice/MyExampleFusionAuthPluginModule.java.

Here is an example of what you will find in the example Guice module referenced above.

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);
  }
}

Notice that this plugin is annotated with the class io.fusionauth.plugin.spi.PluginModule. This is how FusionAuth locates the Guice module and installs your plugin.

Building

This will assume you using the Maven build tool. You are welcome to utilize any Java build tool that you wish.

mvn clean compile package
ls -lah ./target/*.jar
-rw-r--r--  1 robotdan  staff   4.5K Apr 24 08:06 ./target/fusionauth-example-password-encryptor-0.1.0.jar

The above command will compile and build a jar artifact that we will install onto FusionAuth. The jar found in the target directory is your plugin.

Install the Plugin

After you have completed your plugin code and all of your unit tests pass, you are ready to install the plugin into FusionAuth. You will utilize the jar build output file from the previous step.

Next, you need to create the plugin directory in your FusionAuth installation. Depending on where you installed FusionAuth, you will create the plugin directory in the FUSIONAUTH_HOME directory. This directory is the directory right above the FUSIONAUTH_HOME directory. Here are some examples for the plugin directory:

Linux and macOS

/usr/local/fusionauth/plugins

Windows

\fusionauth\plugins

The location of this directory might be different if you install using the ZIP bundles and placed FusionAuth somewhere else.

Next, you copy this JAR file from your plugin project into the plugin directory like this:

Linux/Mac/Unix

cp target/fusionauth-example-password-encryptor-0.1.0.jar /usr/local/fusionauth/plugins

Windows

cp target\fusionauth-example-password-encryptor-0.1.0.jar \fusionauth\plugins

Now you can restart FusionAuth to load your plugin.

If you plugin is found and loaded successfully, you should see a message like this in the logs:

INFO  io.fusionauth.api.plugin.guice.PluginModule - Installing plugin [com.mycompany.fusionauth.plugins.guice.MyExampleFusionAuthPluginModule]
INFO  io.fusionauth.api.plugin.guice.PluginModule - Plugin successfully installed

At this point, you should be able to log in to the administrative user interface, navigate to Tenants -> Your Tenant -> Password and scroll to the Cryptographic hash settings section. Your new hashing scheme will appear in the Scheme field. This is another way to verify the plugin is installed in FusionAuth.

Tenant settings with a new plugin installed.

Do not set the Scheme field to that new plugin value unless you want all users in that tenant to use that hash.

Installing On FusionAuth Cloud

The above instructions document how to install a plugin on a self-hosted FusionAuth system, where you have access to the file system and/or container image where FusionAuth is running.

FusionAuth Cloud does not allow such access, among other limits.

If you have a FusionAuth Cloud deployment and want to install a custom plugin, please open a support ticket. Make sure you include the jar file as an attachment.

The FusionAuth support team will then coordinate with you to install the plugin and restart FusionAuth.

Multiple Plugins

You are allowed to have as many plugins as you want. This may occur if you are consolidating multiple systems into FusionAuth, each with their own password hashing algorithm.

When you do so, ensure you have unique values for the classname, the test name and the binding name. They may remain in the same package and maven artifact or jar file.

If you do not ensure that each hashing class has a unique name and binding value, system behavior is undetermined.

For example, to have two plugins based on the above example plugin project, copy the following files:

  • MyExamplePasswordEncryptor.java to MyOtherExamplePasswordEncryptor.java
  • MyExamplePasswordEncryptorTest.java to MyOtherExamplePasswordEncryptorTest.java

Then modify the MyOtherExamplePasswordEncryptor files to implement and test the new hash.

Finally, add the following to the MyExampleFusionAuthPluginModule.java file:

passwordEncryptorMapBinder.addBinding("custom-other-hash").to(MyOtherExamplePasswordEncryptor.class);

Please note, when selecting the string value that will be used as your encryptionScheme, please do prefix it with custom- or your company such as acme- to avoid the possibility of a name collision if FusionAuth were to add additional hashing schemes in the base product.

You can then build and install the plugin in the same fashion as you would with one plugin.

Multiple Plugin Projects

You may also have multiple plugin projects. This may make sense if you want more logical separation between the plugin code.

In that case, ensure that the guice module class, the package name and the artifactId values are distinct.

If you have two projects, you’ll have to build and deploy each of the artifacts to the correct location in FusionAuth.

Troubleshooting

If your plugin is not installed, ensure:

  • You added the binding with a unique name to a Guice module.
  • The jar file is valid and contains both the Guice module and the implementation of the PasswordEncryptor class.
  • The plugin jar file is in the top level plugins directory (the peer to the bin and config directories). This directory may need to be created.
  • The jar file and the plugins directory are accessible to the FusionAuth instance.

If you have verified the plugin is installed via the administrative user interface or log messages, but a user with a known password cannot login successfully:

  • Ensure that users are imported using the correct name in the encryptionScheme field. Review other settings such as the factor as well.
  • Add tests to double check that you are hashing the passwords in the way you expect.