July 20, 2015

Multitenancy Part 1 - Custom Login Module & Bcrypt Password Hashing on WildFly 9

Quick note on multi-tenancy

This example is for a shared schema approach, which is a common requirement in many SaaS applications. Hibernate currently supports separate database and separate schema, but not shared schema. There are plans for shared schema in Hibernate 5.  My next post will show you how to use this custom login module/principal to support shared schema multi-tenancy in your Java EE 7 application.

For an excellent overview on multitenacy, check this article.

Quick note on password hashing

The default options in WildFly for storing passwords (MD5, SHA-512 etc) are great for demo's or prototypes, but are not suitable for a live environment. For a good introduction to password hashing see here.

What is covered in this post

How to create a custom principal
How to create custom login module
How to apply non-default hashing/password algorithm (BCrypt in this example)
How to modify your custom principal for a multi-tenancy application (shared schema approach)

Works with WildFly 9 (picketbox 4.9.2.Final), minor modification is required for WildFly 8 (pickbox <= 4.0.21.Final)

Why a custom principal?

In addition to the username, I need to store a reference to the userId and the tenantId. I want to have the option of looking up a JPA User by their userId, not their username. Also, because this is for a multi-tenant application, I want to be able to quickly access the tenantId (for example when applying a filter to queries).

Why a custom login module?

Two reasons. First I need to set the tenantId and userId on the identity object (principal).
Second, I want to apply a strong hashing algorithm and a per-user salt. In this example I use the BCrypt hashing function.

Now to the code...

First add the maven dependencies:
<!-- provides security implementation for WildFly -->
<dependency>
    <groupId>org.picketbox</groupId>
    <artifactId>picketbox</artifactId>
    <version>4.9.2.Final</version>
    <scope>provided</scope>
</dependency>
<!-- Java BCrypt library -->
<dependency>
    <groupId>de.svenkubiak</groupId>
    <artifactId>jBCrypt</artifactId>
    <version>0.4</version>
</dependency>
<dependency>
    <groupId>javax.transaction</groupId>
    <artifactId>jta</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.jboss.logging</groupId>
    <artifactId>jboss-logging</artifactId>
    <version>3.3.0.Final</version>
    <scope>provided</scope>
</dependency>

Create the custom principal

In this example we will be extending org.jboss.security.SimplePrincipal with our own implementation, which will store a reference to the tenantId and userId in addition to the username.
public class DatabasePrincipal extends SimplePrincipal {

    private Long userId;
    private Integer tenantId;
    
    //required when creating principal See AbstractServerLoginModule.createIdentity();
    public DatabasePrincipal(String userName) {
        super(userName);
    }

    public Long getUserId() {
        return userId;
    }

    public Integer getTenantId() {
        return tenantId;
    }

    void setUserId(long userId) {
        this.userId = userId;
    }

    void setTenantId(int tenantId) {
        this.tenantId = tenantId;
    }
}

Create the custom database login module

We will extending DatabaseServerLoginModule and overriding validatePassword() so that we can use jBCrypt to check the raw password against the hashed password. To learn how to generate the hash in the first place you can check the jBCrypt docs. We also need to override the getUsersPassword() method as we need to get the tenantId and userId from PreparedStatement and then set them on our DatabasePrincipal.
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.security.auth.login.LoginException;
import javax.sql.DataSource;
import javax.transaction.SystemException;
import javax.transaction.Transaction;

import org.jboss.logging.Logger;
import org.jboss.security.PicketBoxMessages;
import org.jboss.security.auth.spi.DatabaseServerLoginModule;

public class MultitenantDatabaseServerLoginModule extends DatabaseServerLoginModule {

    private Logger log = Logger.getLogger(getClass());
 
    @Override
    protected boolean validatePassword(String enteredPassword, String encrypted) {
        
        if (enteredPassword == null || encrypted == null) {
            return false;
        }

        return BCrypt.checkpw(enteredPassword, encrypted)) {
    }

    @Override
    protected String getUsersPassword() throws LoginException {
        
        boolean trace = log.isTraceEnabled();
        String username = getUsername();
        String password = null;
        ResultSet rs = null;

        Transaction tx = null;
        if (suspendResume) {
            // tx = TransactionDemarcationSupport.suspendAnyTransaction();
            try {
                if (tm == null)
                    throw PicketBoxMessages.MESSAGES.invalidNullTransactionManager();
                tx = tm.suspend();
            } catch (SystemException e) {
                throw new RuntimeException(e);
            }
        }

        try {

            InitialContext ctx = new InitialContext();
            DataSource dataSource = (DataSource) ctx.lookup(dsJndiName);

            // Get the password
            if (trace) {
                log.trace("Excuting query: " + principalsQuery + ", with username: " + username);
            }
            try (Connection connection = dataSource.getConnection();
                    PreparedStatement statement = connection.prepareStatement(principalsQuery)) {

                statement.setString(1, username);
                rs = statement.executeQuery();

                if (rs.next() == false) {
                    if (trace) {
                        log.trace("Query returned no matches from db");
                    }
                    throw PicketBoxMessages.MESSAGES.noMatchingUsernameFoundInPrincipals();
                }

                Integer userId = rs.getInt(1);
                Integer tenantId = rs.getInt(2);
                password = rs.getString(3);

                //Set the tenant and user properties on the custom principal
                //there maybe a better place to do this, but this is the only place I can see it working without having to hit the database with another query. 
                DatabasePrincipal principle = (DatabasePrincipal) getIdentity();
                principle.setTenantId(tenantId);
                principle.setUserId(userId);

                if (trace) {
                    log.trace("Obtained user password");
                }

            } catch (SQLException ex) {
             LoginException le = new LoginException(PicketBoxMessages.MESSAGES.failedToProcessQueryMessage());
                le.initCause(ex.getCause());
                log.error(ex);
                throw le;
            } finally {
                if (rs != null) {
                    try {
                        rs.close();
                    } catch (SQLException e) {
                    }
                }
            }

        } catch (NamingException ex) {
            LoginException le = new LoginException(PicketBoxMessages.MESSAGES.failedToLookupDataSourceMessage(dsJndiName));
            le.initCause(ex);
            log.error(ex);
            throw le;
        } finally {
            if (suspendResume) {
                // TransactionDemarcationSupport.resumeAnyTransaction(tx);
                try {
                    tm.resume(tx);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                if (log.isTraceEnabled())
                    log.trace("resumeAnyTransaction");
            }
        }
        return password;
    }
}

Configure the application

We now need to tell our application to use the new security domain. For Servlets (and FORM based authentication) you will need to add a security domain to the jboss-web.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
    <security-domain>multitenantdomain</security-domain>
</jboss-web>

To use declarative security in your EJB's you will need to add the security domain to your jboss-ejb3.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<jboss:jboss
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:s="urn:security:1.1"
        version="3.1" impl-version="2.0">
 
    <assembly-descriptor>
        <s:security>
            <ejb-name>*</ejb-name>
            <s:security-domain>multitenantdomain</s:security-domain>
        </s:security>
    </assembly-descriptor>
</jboss:jboss>

And to use the security domain in your EJB, use the org.jboss.ejb3.annotation.SecurityDomain annotation:
@Stateless
@SecurityDomain("other")
public class MyEJB {

}

Configure WildFly

Lastly we need to tell WildFly which login module to use, and which principal implementation we want it to instantiate. We also need to specify our query (returning user_id and tenant_id).
<security-domain name="multitenantdomain" cache-type="default">
    <authentication>
        <login-module code="com.ritchie.security.MultitenantDatabaseServerLoginModule" flag="required">
            <module-option name="dsJndiName" value="java:jboss/datasources/MultitenantDS"/>
            <module-option name="principalsQuery" value="select user_id, tenant_id, password from User where username=?"/>
            <module-option name="rolesQuery" value="select Role, 'Roles' from User where Username=?"/>
            <module-option name="password-stacking" value="useFirstPass"/>
            <module-option name="principalClass" value="com.ritchie.security.DatabasePrincipal"/>
        </login-module>
    </authentication>
</security-domain>

And... for the sake of completeness

Here is a simple login page and web.xml configuration
web.xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
    <login-config>
        <auth-method>FORM</auth-method>
        <form-login-config>
            <form-login-page>/login.html</form-login-page>
            <form-error-page>/access-denied.html</form-error-page>
        </form-login-config>
    </login-config>
    <security-constraint>
        <display-name>Open Content</display-name>
        <web-resource-collection>
            <web-resource-name>Unrestricted Content</web-resource-name>
            <url-pattern>/login.xhtml</url-pattern>
        </web-resource-collection>
    </security-constraint>
    <security-constraint>
        <display-name>Access Constraint</display-name>
 <web-resource-collection>
            <web-resource-name>Site Access Constraint</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>*</role-name>
        </auth-constraint>
    </security-constraint>
    <security-role>
        <role-name>administrator</role-name>
    </security-role>
</web-app>
index.html
<!DOCTYPE html>
<html>
   <head>
      <meta charset="UTF-8">
      <title>Login Page</title>
   </head>
   <body>
      <form method="post" action="j_security_check">
         <label for="username">Username:</label>
         <input id="username" required="required" type="text" name="j_username"/>
         <label for="password">Password:</label>
         <input id="password" required="required" type="password" name="j_password"/>
         <button type="submit">Login</button>
      </form>
   </body>
</html>

In the next post we will build on this example and create a CrudService that will apply a filter to queries to ensure data isolation between tenants.


Ref:
https://msdn.microsoft.com/en-us/library/aa479086.aspx
https://docs.jboss.org/author/display/WFLY8/Authentication+Modules
https://docs.jboss.org/author/display/WFLY8/Securing+EJBs
https://dzone.com/articles/creating-custom-login-modules
http://throwingfire.com/storing-passwords-securely/#notpasswordhashes
http://www.mindrot.org/projects/jBCrypt/