How to deploy a custom PasswordHashProvider SPI in Keycloak

Guilliano MolaireGuilliano Molaire6 min read

To deploy a custom PasswordHashProvider in Keycloak, implement PasswordHashProviderFactory and PasswordHashProvider, register the factory in META-INF/services, package it as a JAR, copy it to Keycloak’s providers/ directory and run kc.sh build. If new passwords should use it too, set the realm’s Hashing Algorithm password policy to your provider ID.

The example reads bcrypt hashes, so users you move from a bcrypt system keep their passwords. The code and screenshots follow Keycloak 26.4.0.

Prerequisites

  • Keycloak 26.4.0, with access to its installation and admin console.
  • JDK 17 or newer and Maven.
  • Legacy bcrypt hashes to import, and Apache’s htpasswd, whose -B option uses bcrypt hashing, to make a test hash.

Step 1: Create the Maven project

The pom.xml of the bcrypt-hash-provider project, with the Keycloak SPI modules and Bouncy Castle as provided dependencies and the bcprov-jdk18on line marked

The pom declares Keycloak’s SPI modules and Bouncy Castle, which supplies bcrypt, all as provided. If your server has no bcprov JAR, copy one to providers/ with yours, as the provider guide describes for third-party dependencies.

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>bcrypt-hash-provider</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <kc.version>26.4.0</kc.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.keycloak</groupId>
      <artifactId>keycloak-core</artifactId>
      <version>${kc.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.keycloak</groupId>
      <artifactId>keycloak-server-spi</artifactId>
      <version>${kc.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.keycloak</groupId>
      <artifactId>keycloak-server-spi-private</artifactId>
      <version>${kc.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.bouncycastle</groupId>
      <artifactId>bcprov-jdk18on</artifactId>
      <version>1.81</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
</project>

Step 2: Write the PasswordHashProvider

The BcryptPasswordHashProvider class, with the OpenBSDBCrypt.checkPassword line in verify marked

verify checks a typed password against a stored credential, encodedCredential hashes a new one, and policyCheck says whether a stored credential matches the realm’s policy. The provider keeps the whole bcrypt string as the secret’s value.

package com.example.keycloak.hash;

import java.security.SecureRandom;
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
import org.keycloak.credential.hash.PasswordHashProvider;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.credential.PasswordCredentialModel;

public class BcryptPasswordHashProvider implements PasswordHashProvider {

    private static final int DEFAULT_COST = 12;
    private static final SecureRandom RANDOM = new SecureRandom();
    private final String providerId;

    public BcryptPasswordHashProvider(String providerId) {
        this.providerId = providerId;
    }

    @Override
    public boolean verify(String rawPassword, PasswordCredentialModel credential) {
        String hash = credential.getPasswordSecretData().getValue();
        try {
            return OpenBSDBCrypt.checkPassword(hash, rawPassword.toCharArray());
        } catch (RuntimeException e) {
            return false;
        }
    }

    @Override
    public PasswordCredentialModel encodedCredential(String rawPassword, int iterations) {
        int cost = (iterations >= 4 && iterations <= 31) ? iterations : DEFAULT_COST;
        byte[] salt = new byte[16];
        RANDOM.nextBytes(salt);
        String hash = OpenBSDBCrypt.generate("2b", rawPassword.toCharArray(), salt, cost);
        return PasswordCredentialModel.createFromValues(providerId, null, cost, hash);
    }

    @Override
    public boolean policyCheck(PasswordPolicy policy, PasswordCredentialModel credential) {
        return providerId.equals(credential.getPasswordCredentialData().getAlgorithm());
    }

    @Override
    public void close() {
    }
}

Step 3: Write the factory with a unique provider ID

The BcryptPasswordHashProviderFactory class, with the provider ID bcrypt-legacy marked

The factory’s ID is what a credential’s algorithm field and the policy refer to. Keycloak ships argon2, pbkdf2-sha512, pbkdf2-sha256 and pbkdf2, and its developer guide recommends a unique ID per factory, so we use bcrypt-legacy.

package com.example.keycloak.hash;

import org.keycloak.Config;
import org.keycloak.credential.hash.PasswordHashProvider;
import org.keycloak.credential.hash.PasswordHashProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;

public class BcryptPasswordHashProviderFactory implements PasswordHashProviderFactory {

    public static final String ID = "bcrypt-legacy";

    @Override
    public PasswordHashProvider create(KeycloakSession session) {
        return new BcryptPasswordHashProvider(ID);
    }

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public void init(Config.Scope config) {
    }

    @Override
    public void postInit(KeycloakSessionFactory factory) {
    }

    @Override
    public void close() {
    }
}

Step 4: Register the factory in META-INF/services

The META-INF/services file named org.keycloak.credential.hash.PasswordHashProviderFactory, with its single line naming the factory class marked

The developer guide says an SPI implementation also needs a service configuration file. Create src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory holding your factory’s class name:

com.example.keycloak.hash.BcryptPasswordHashProviderFactory

Somewhere to run this that stays patched

Everything above works the same on Skycloak, because it is real upstream Keycloak rather than a fork. What changes is who handles the upgrades, backups and security patches afterwards.

Step 5: Install the JAR and run kc.sh build

The install commands: mvn package, copying the JAR to /opt/keycloak/providers/, and the kc.sh build line marked

Package the project, copy the JAR to providers/ and run the build, which an optimized server needs to register a new provider.

mvn package
cp target/bcrypt-hash-provider-1.0.0.jar /opt/keycloak/providers/
/opt/keycloak/bin/kc.sh build

Step 6: Set the Hashing Algorithm policy

The Password policy form in the Keycloak admin console, with the Hashing Algorithm policy added and bcrypt-legacy typed in its field, marked

In your realm, click Authentication and then the Policies tab, as the admin guide describes; on Password policy, open Add policy, pick Hashing Algorithm, type bcrypt-legacy and save. Do this only when new passwords should be bcrypt too.

To move users to Argon2 instead, leave the policy at argon2: imported hashes still verify through your provider, and stored hashes do not change until the user logs in. Our password policy guide covers the other policies.

Test it

In the master realm, click your user name at the top right, select Realm info and open its Provider info tab, as the admin guide describes, and look for bcrypt-legacy. Then hash a test password:

htpasswd -bnBC 10 legacy.user 'Demo-pass-123' | cut -d: -f2

Paste it into legacy-user.json, with bcrypt-legacy as the algorithm:

{
  "username": "legacy.user",
  "enabled": true,
  "credentials": [{
    "type": "password",
    "secretData": "{\"value\":\"PASTE_THE_BCRYPT_HASH_HERE\"}",
    "credentialData": "{\"hashIterations\":10,\"algorithm\":\"bcrypt-legacy\"}"
  }]
}

Run kcadm.sh create users -r hash-demo -f legacy-user.json, then sign in at /realms/hash-demo/account/ as legacy.user with Demo-pass-123. The sign-in succeeds, and with the policy at argon2, Show data on the user’s Credentials tab now shows argon2.

Troubleshooting

bcrypt-legacy is missing from Provider info. The JAR was not loaded. Check with jar tf that the services file has the exact interface name, then run kc.sh build again.

The provider is missing after a restart. A server started with --optimized needs a build to update its provider registry. Run kc.sh build after every JAR change.

An imported user gets “Invalid username or password”. Often the credential’s algorithm names no loaded provider. Check the spelling, and that no shell expanded the $ signs in the hash.

A built-in hasher answers instead of yours. Your factory reuses a built-in ID, and with several implementations of one ID Keycloak uses only the one with the highest order. Our post on SPI provider ID collisions has more.

NoSuchMethodError at startup after an upgrade. The JAR was compiled against older Keycloak modules. Set kc.version to the new release, rebuild, and test on staging first.

FAQ

Does Keycloak support bcrypt password hashes?

No. Its built-in hashers are argon2 and three PBKDF2 variants, so bcrypt needs a provider like this one.

How do I rehash bcrypt passwords to Argon2 in Keycloak?

Keep the Hashing Algorithm at argon2: the admin guide says stored hashes do not change until the user logs in. Users who never sign in keep bcrypt, so keep the JAR installed.

Can I upload a custom PasswordHashProvider JAR to Skycloak managed Keycloak?

Yes, on Enterprise or on Business with the Bring Your Own Extension add-on. You upload the JAR in the cluster’s Custom Extensions tab, it is scanned with ClamAV, and you install it once it is clean, as our extensions documentation describes.

Should I use a hash provider or user federation for legacy users?

Use a hash provider when you can export the hashes, and a custom user federation provider when they cannot leave the old system. Our post on federating or migrating legacy users helps you choose.

Next steps

Build the sample provider, then see managed Keycloak hosting on Skycloak and ask our support team about running your extension there.

Somewhere to run this that stays patched

Everything above works the same on Skycloak, because it is real upstream Keycloak rather than a fork. What changes is who handles the upgrades, backups and security patches afterwards.

Guilliano Molaire
Written by
Founder, Skycloak

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

© 2026 Skycloak. All Rights Reserved. Design by Yasser SolimanKeycloak™ is a trademark of the Linux Foundation. Skycloak is not affiliated with the Keycloak project.