Configuring a JDBC Security Realm with BCrypt and Modular Crypt Password Mappers
Posted by aabdelsa in Ashley Abdel-Sayed's Blog on Jun 11, 2019 9:49:36 AMThe Elytron JDBC SecurityRealm allows users to load identities from databases. These identities can be loaded with multiple credentials and attributes. To define the credentials and attributes, users can use one or more principal queries, each of which can be defined from their own datasource. The JDBC SecurityRealm also supports a variety of password mappers to load representations of a password from a database. Many of these mappers load either hashed or digested representations of the password as well as any related salt from the database.
With WildFly 17, there are now a few new options available for JDBC SecurityRealms. While it was previously assumed that encoded fields were always encoded in base64, there is now the option to specify that the passwords and salts are hex encoded. There is also the option to store passwords, salts, and iteration counts (if applicable) as a single string with the modular crypt password mapper. The modular crypt encoded password can be applied to a number of different password types.
In this blog post we will make use of jdbc-security-realm-bcrypt-password project in the elytron-examples repository to demonstrate how to use a JDBC security realm to load a BCrypt password with both hex and base64 encoding, as well as with the modular crypt mapper.
Base64
Generating BCrypt Hashed Password and Salt
Our first step is to generate a BCrypt salted password using the Wildfly Elytron PasswordFactory.
This will require interacting with the org.wildfly.security.password.PasswordFactory API, which obtains access to implementations from java.security.Provider instances
public static void main(String[] args) throws Exception { static final Provider ELYTRON_PROVIDER = new WildFlyElytronProvider(); static final String PASSWORD = "quickstartPwd1!"; PasswordFactory passwordFactory = PasswordFactory.getInstance(BCryptPassword.ALGORITHM_BCRYPT, ELYTRON_PROVIDER); int iterationCount = 10; byte[] salt = new byte[BCryptPassword.BCRYPT_SALT_SIZE]; SecureRandom random = new SecureRandom(); random.nextBytes(salt); IteratedSaltedPasswordAlgorithmSpec iteratedAlgorithmSpec = new IteratedSaltedPasswordAlgorithmSpec(iterationCount, salt); EncryptablePasswordSpec encryptableSpec = new EncryptablePasswordSpec(PASSWORD.toCharArray(), iteratedAlgorithmSpec); BCryptPassword original = (BCryptPassword) passwordFactory.generatePassword(encryptableSpec); byte[] hash = original.getHash(); Base64.Encoder encoder = Base64.getEncoder(); String encodedHash = encoder.encodeToString(hash); String encodedSalt = encoder.encodeToString(salt); System.out.println("Encoded Hash = " + encodedHash); System.out.println("Encoded Salt = " + encodedSalt); }
This will generate a base64 encoded hash of your password and salt which you can store in your database. You should also store your iteration count in the database.
For the jdbc-security-realm-bcrypt-password project, GenerateBCryptPassword is a class provided that generates a BCrypt password. To run this class, you can use the following command from the home directory of the example:
$mvn clean install
$mvn exec:java -Dexec.mainClass="org.wildfly.security.examples.GenerateBCryptPassword" -Dexec.args="false false quickstartPwd1!"
- The first argument indicates if the password and salt should be hex encoded
- The second argument indicates if the password, salt, and iteration count should be modular crypt encoded into a single String
- The last argument is the clear text password you would like to encrypt.(Warning: if this is run on a shared computer other users can see arguments passed to a running process.)
We set hex encoding and modular crypt encoding to false as we would like base64 encoding. We use this command twice to generate hashes for the passwords ‘quickstartPwd1!’ and ‘guestPwd1!’ and load this into our database. The tables used for the example are created in src/main/resources/import.sql. Two parts of this file are worth mentioning:
- Where we create the table of users to hold an ID, username, password, salt, and iteration count
CREATE TABLE USERS (ID INT, USERNAME VARCHAR(20), PASSWORD VARCHAR(60), SALT VARCHAR(60), ITERATION_COUNT INT);
- Where we add two users and their hashed passwords, salts, and iterations counts that we generated using GenerateBCryptPasswords.
--Username: quickstartUser, Password: quickstartPwd1! INSERT INTO USERS (ID, USERNAME, PASSWORD, SALT, ITERATION_COUNT) VALUES (1, 'quickstartUser', 'NxHRwFg/YkkRgGUq/D6ARnD+caxlmIg=', 'DMakCMy9DYKBVW/HNrCrPw==',10); --Username: guest, Password: guestPwd1! INSERT INTO USERS (ID, USERNAME, PASSWORD, SALT, ITERATION_COUNT) VALUES (2, 'guest', 'sspDe1PYqDdAJ5EeHoRJcXZPuM+vOi8=', 'VO0V0x+j/oZ5r7VvxdzDzw==', 10);
Configurations
Once we have generated our hashed passwords and salt, we can load these in using a JDBC security realm and the bcrypt-mapper. Our example provides a configure-server.cli file which has a number of configurations. The section of relevance to us is the following:
/subsystem=elytron/jdbc-realm=servlet-security-jdbc-realm:add( principal-query=[{sql="SELECT PASSWORD, SALT, ITERATION_COUNT FROM USERS WHERE USERNAME = ?", data-source="ServletSecurityDS",bcrypt-mapper={password-index=1, salt-index=2,iteration-count-index=3}}, {sql="SELECT R.NAME, 'Roles' FROM USERS_ROLES UR INNER JOIN ROLES R ON R.ID = UR.ROLE_ID INNER JOIN USERS U ON U.ID =UR.USER_ID WHERE U.USERNAME = ?", data-source="ServletSecurityDS",attribute-mapping=[{index=1, to=roles}]}])
This configuration adds a JDBC security realm and sets the principal query to select the password, the salt, and the iteration count from the database. We define the principal query with the bcrypt-mapper password mapper with the following attributes:
- password-index - the column index of our password in the database table
- salt-index - the column index of our salt in the database table
- iteration-count-index - the column index of our iteration count in the database table
This command also loads the roles from the database which will determine which login access the user has.
Running and Accessing the Application
We’re now ready to run our example and see the results.
Step 1: Run the wildfly server
${path_to_wildfly}/bin/standalone.sh
Step 2: Navigate to the source folder of this example and configure elytron
${path_to_wildfly}/bin/jboss-cli.sh --connect --file=configure-server.cli
This script will:
- Start by adding the JDBC datasource we want to use
- Add the JDBC security realm to the elytron subsystem responsible for verifying credentials
- Add a role decoder for the "roles" attribute mapping
- Adds the servlet-security-quickstart security domain to the elytron subsystem
- Adds the HTTP Authentication Factory to the elytron subsystem
- Configure Undertow's application security domain
Step 3: Build and deploy the artifacts
$mvn clean package install wildfly:deploy
Step 4: Navigate to http://localhost:8080/jdbc-security-realm-bcrypt-password in your browser. You will be prompted to login.
If you login with quickstartUser (password: quickstartPwd1!) you can see that it was successful:
Successfully called Secured Servlet
Principal : quickstartUser
Remote User : quickstartUser
Authentication Type : BASIC
If you log in with guest (password: guestPwd1!) you will receive the following error message:
Forbidden
Step 5: To undeploy, navigate to the home directory of the example and run:
${path_to_wildfly}/bin/jboss-cli.sh --connect --file=restore-configurations.cli
$mvn clean package wildfly:undeploy
Hex
Generating BCrypt Hashed Password and Salt
The PasswordFactory also provides the capability to store passwords with hex encoding. To implement this we make these changes to our code:
public static void main(String[] args) throws Exception { static final Provider ELYTRON_PROVIDER = new WildFlyElytronProvider(); static final String password = "quickstartPwd1!"; PasswordFactory passwordFactory = PasswordFactory.getInstance(BCryptPassword.ALGORITHM_BCRYPT, ELYTRON_PROVIDER); int iterationCount = 10; byte[] salt = new byte[BCryptPassword.BCRYPT_SALT_SIZE]; SecureRandom random = new SecureRandom(); random.nextBytes(salt); IteratedSaltedPasswordAlgorithmSpec iteratedAlgorithmSpec = new IteratedSaltedPasswordAlgorithmSpec(iterationCount, salt); EncryptablePasswordSpec encryptableSpec = new EncryptablePasswordSpec(password.toCharArray(), iteratedAlgorithmSpec); BCryptPassword original = (BCryptPassword) passwordFactory.generatePassword(encryptableSpec); byte[] hash = original.getHash(); encodedHash = ByteIterator.ofBytes(hash).hexEncode().drainToString(); encodedSalt = ByteIterator.ofBytes(salt).hexEncode().drainToString(); System.out.println("Encoded Hash = " + encodedHash); System.out.println("Encoded Salt = " + encodedSalt); }
In the home directory of this example, you can run the following command to use GenerateBCryptPassword to generate a hex encoded password:
$mvn clean install
$mvn exec:java -Dexec.mainClass="org.wildfly.security.examples.GenerateBCryptPassword" -Dexec.args="true false quickstartPwd1!"
Again, we use this command twice to generate hashes for the passwords ‘quickstartPwd1!’ and ‘guestPwd1!’ and insert this into our database.
--Username: quickstartUser, Password: quickstartPwd1! INSERT INTO USERS (ID, USERNAME, PASSWORD, SALT, ITERATION_COUNT) VALUES (1, 'quickstartUser', '1b16d96be4b63b73d6ec7008f656effefa94ddfa5daa14',’d75ef1b0445dde97315936f8cb62d960',10); --Username: guest, Password: guestPwd1 INSERT INTO USERS (ID, USERNAME, PASSWORD, SALT, ITERATION_COUNT) VALUES (2, 'guest', 'c4537f43dd62c6c854a7e39378a972bcca8df71810954d', 'a1f8cff8469a6d8203905c38345fc5a3', 10);
Configurations
The default encoding for bcrypt-mapper is base64. We must now add the attributes password-encoding and salt-encoding to our principal query to indicate hex encoding. All other attributes should remain the same.
/subsystem=elytron/jdbc-realm=servlet-security-jdbc-realm:add( principal-query=[{sql="SELECT PASSWORD, SALT, ITERATION_COUNT FROM USERS WHERE USERNAME = ?", data-source="ServletSecurityDS", bcrypt-mapper={password-index=1, hash-encoding=hex, salt-index=2, salt-encoding=hex, iteration-count-index=3}}, {sql="SELECT R.NAME, 'Roles' FROM USERS_ROLES UR INNER JOIN ROLES R ON R.ID = UR.ROLE_ID INNER JOIN USERS U ON U.ID = UR.USER_ID WHERE U.USERNAME = ?", data-source="ServletSecurityDS", attribute-mapping=[{index=1, to=roles}]}])
Running and Accessing the Application
The results for this will be the same as when using base64 encoding.
Modular Crypt
Generating BCrypt Hashed Password and Salt
The PasswordFactory also provides the capability to encode a number of password types using modular crypt which stores the hash, salt, and iteration count as one String.
public static void main(String[] args) throws Exception { static final Provider ELYTRON_PROVIDER = new WildFlyElytronProvider(); static final String PASSWORD = "quickstartPwd1!"; PasswordFactory passwordFactory = PasswordFactory.getInstance(BCryptPassword.ALGORITHM_BCRYPT, ELYTRON_PROVIDER); int iterationCount = 10; byte[] salt = new byte[BCryptPassword.BCRYPT_SALT_SIZE]; SecureRandom random = new SecureRandom(); random.nextBytes(salt); IteratedSaltedPasswordAlgorithmSpec iteratedAlgorithmSpec = new IteratedSaltedPasswordAlgorithmSpec(iterationCount, salt); EncryptablePasswordSpec encryptableSpec = new EncryptablePasswordSpec(PASSWORD.toCharArray(), iteratedAlgorithmSpec); BCryptPassword original = (BCryptPassword) passwordFactory.generatePassword(encryptableSpec); String modularCryptString = ModularCrypt.encodeAsString(original); System.out.println("Modular Crypt = " + modularCryptString); }
In the example, we can set that we want modular encoding when running GenerateBCryptPassword to generate the password:
$mvn clean install
$mvn exec:java -Dexec.mainClass="org.wildfly.security.examples.GenerateBCryptPassword" -Dexec.args="false true quickstartPwd1!"
Once done, we must change the database to only hold the password:
CREATE TABLE USERS (ID INT, USERNAME VARCHAR(20), PASSWORD VARCHAR(60)); --Username: quickstartUser, Password: quickstartPwd1! INSERT INTO USERS (ID, USERNAME, PASSWORD) VALUES (1, 'quickstartUser', '$2a$10$dDbUML0NLhCvsBMHr9XiDePgbWxHgsQc/1v2UZ3BUWu0PlRGnrddu'); --Username: guest, Password: guestPwd1! INSERT INTO USERS (ID, USERNAME, PASSWORD) VALUES (1, 'quickstartUser', '$2a$10$JqTcM8iL.FVNd8O/i28ed.8uDAYRgXkE9EWM176Tz4V0zQz6/ugYq');
Configurations
We must now change the JDBC security realm configuration to load using the modular-crypt-mapper.
/subsystem=elytron/jdbc-realm=servlet-security-jdbc-realm:add( principal-query=[{sql="SELECT PASSWORD FROM USERS WHERE USERNAME = ?", data-source="ServletSecurityDS", modular-crypt-mapper={password-index=1}}, {sql="SELECT R.NAME, 'Roles' FROM USERS_ROLES UR INNER JOIN ROLES R ON R.ID= UR.ROLE_ID INNER JOIN USERS U ON U.ID = UR.USER_ID WHERE U.USERNAME = ?", data-source="ServletSecurityDS", attribute-mapping=[{index=1, to=roles}]}])
The only attribute we need to set is the password-index to equal the column index of our password in the table we created.
Running and Accessing the Application
The results for this will be the same as when using base64 and hex encoding.
Summary
This blog has shown how to generate a BCrypt hashed password with Base64 encoding, hex encoding, and modular crypt encoding. We have also seen how to use a JDBC security realm to load a BCrypt password using bcrypt-mapper and to load a modular crypt encoded password using modular-crypt-mapper.
For more information on JDBC security realms check out this documentation:
For more information on modular crypt encoding check out this documentation:
For more information on other passwords check out this documentation:
https://github.com/wildfly/wildfly/blob/master/docs/src/main/asciidoc/_elytron/Passwords.adoc
Comments