Friday 15 April 2011

JAAS, EJB Security and Glassfish

With EJBs you can specify security by using annotations from the javax.annotation.security package. The below article describes how to setup security on a bean and access the methods of the bean via an annotation or via JAAS which has been setup on Glassfish.

The below bean class only allows the MANAGER role access to use the services exposed. In this case, findCustomerByAccountNumber. (The AccessRoles.MANAGER resolves to a string so if the string changes the CustomerServiceBean doesn't have to change.)

@RolesAllowed(AccessRoles.MANAGER)
@Stateless(mappedName = JndiResourceName.CUSTOMER_SERVICE)
@Remote(CustomerService.class)
public class CustomerServiceBean implements CustomerService {

    @Override
    public Customer findCustomerByAccountNumber(String accountNumber) {
        Customer customer = null;
        // do stuff to find customer
        return customer;
    } 

}


To call the findCustomerByAccountNumber, the code can use the @RunAs annotation as described below:

    @RunAs(AccessRoles.MANAGER)
    public void verifyCustomer(String accountNumber) {
        // do stuff 
        Customer customer = 
            customerService.findCustomerByAccountNumber(String accountNumber);
        // do more stuff 
    }


But what if the roles calling the verify method can vary ie MANAGERS, OPERATORS, ADMIN. In this scenario, we would want to authenticate the 'caller' before accessing the findCustomerByAccountNumber method. A solution to this would be to use JAAS (Java Authentication and Authorization Service.)

The principal of this is to create realms and have users and groups in the realm. There are a few steps involved which are described as follows:

Firstly, the app server (in this example Glassfish) needs to create a realm and the below describes how to do this using the command line. It assumes connection pools, user and group database tables have been created and populated, and that the flexiblejdbcrealm-0.4.jar is in the Glassfish lib dir:

asadmin --host  delete-auth-realm customer-realm
asadmin --host  create-auth-realm --classname=org.wamblee.glassfish.auth.FlexibleJdbcRealm --property="jaas.context=customerJdbcRealm:datasource.jndi=
jdbc/Customer:sql.password=select password from customeruser where username\=?:sql.groups=select g.groupname from customergroup g inner join user_group ug on g.id\=ug.group_id inner join customeruser u on ug.user_id\=u.id where u.username\=?:password.digest=MD5:password.encoding=BASE64" customer-realm

asadmin --host  --user admin set server-config.security-service.activate-default-principal-to-role-mapping=true
asadmin --host  set-log-level javax.enterprise.system.core.security=INFO
asadmin --host  set-log-level org.wamblee.glassfish.auth=INFO

The login.conf needs to have the below added to it:

customerJdbcRealm {com.mypackage.auth.CustomerLoginModule required;}

The CustomerLoginModule class extends FlexibleJdbcLoginModule and gives us the ability to intercept the login/authentication calls if we so wish. In this case, any login exceptions are being logged:

public class CustomerLoginModule extends FlexibleJdbcLoginModule implements LoginModule {

    private static final Logger SECURITY_LOGGER = Logger.getLogger("com.mypackage");

    @Override
    protected void authenticate() throws javax.security.auth.login.LoginException {

        try {
            super.authenticate();
        } catch(LoginException le){
            SECURITY_LOGGER.error("Authentication failed for " 
                + _username + ". " + le.getMessage());
            throw le;
        }

    }

}


We can now change the verify method to authenticate before calling the 'secure'  findCustomerByAccountNumber method:

    private ProgrammaticLoginInterface programmaticLogin = 
        new ProgrammaticLogin();

    public void verifyCustomer(String accountNumber) {
        Customer customer = null;
        boolean loginSuccessful = programmaticLogin.login("manager", "password", "customer-realm", true);  

        if (loginSuccessful) { 
            customer = customerService.findCustomerByAccountNumber(String accountNumber);
        } else {
            // throw exception 
        }

    }


The call to the ProgrammaticLogin instance attempts to use the supplied name and password directly to login to the current realm. If successful, a security context is created for that user and is used by the EJB when checking what roles are allowed to call it.

For example purposes, the above verifyCustomer method has the name and password hard coded but in reality these values could be obtained from a login web page or other such authentication mechanisms.