Sunday, July 13, 2014

Custom database based authentication

Introduction

Off late there are several projects that are being migrated from oracle FORMS to ADF. When it comes to user authentication, general trend in FORMS based application is that it uses user’s own database schema and password to login. Typical migration from FORMS to ADF takes somewhere between few months to couple of years depending upon the size of the application(s). ADF applications are typically designed to use Oracle IAM suite of products for authentication and authorization but to get there it takes a while. Until a final security solution is arrived user profiles needs to be maintained separately according to the underlying LDAP. If organizations want to follow similar FORMS (database) based authentication process until a final security solution is identified they could perhaps look into this approach that is explained in this article.

There is an option in the WebLogic application server to create SQLAuthenticator as the authentication provider in the WebLogic application server but that requires plain text password to be provided which may be against few company policies. How do we solve this problem? Well, you can create a custom authentication provider in java class that will authenticate the user provided credentials against the same database that FORMS uses. I found this as the simplest solution and it works very well. There is absolutely no change in the way user login into ADF application as it uses user credentials to authenticate against the same database that FORMS uses. The java class uses simple JDBC call to connect to the database. For this we need to create an mBean and register it in the WebLogic application server. 

Design & implementation

Below diagram depicts the design for DB based authentication.

I followed the steps given in the URL http://knowaboutadf.blogspot.in/2012/09/custom-authentication-provider-in.html to create a custom database authenticator. I don’t want to repeat the same steps in this article. I have provided additional information below that you may find it useful.

Code that implements LoginModule class has the logic to read and authenticate the end user provided credential is given below.

/**
 * Author: Aravindan Varadan
 * Created on: April 16 2014
 * Updated on: April 21 2014
 * Description: This is a custom authentication login module implementation that will authenticate the users against database schema.  *
 */
package adf.common.security.provider;

import java.io.IOException;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import java.util.HashMap;
import java.util.Map;

import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import weblogic.security.principal.WLSGroupImpl;
import weblogic.security.principal.WLSUserImpl;

public class CustomLoginModuleImpl implements LoginModule {
   
    private Subject subject;
    private CallbackHandler callbackHandler;
    private HashMap userMap;
   
    private boolean loginSucceeded;
    private boolean principalsInSubject;
    private Vector<weblogic.security.principal.WLSAbstractPrincipal> principalsBeforeCommit = new Vector<weblogic.security.principal.WLSAbstractPrincipal>();
    private final static String DEVELOPER_ROLE = "DEVELOPER";
    private final static String DEV_MGR_ROLE = "DEV_MGR";
    private final static String REL_MGR_ROLE = "REL_MGR";
    private final static String TL_ROLE = "TL";
   
    private String username = null;
    private String password = null;
    private String host = null;
    private String port = null;
    private String db = null;
   
    private String tlId = null;
    private String mgrId = null;
    private String deptCd = null;
    
    public MFCustomLoginModuleImpl() {
        super();
    }

    /**
     * This overridden method will be initialized during doLogin() method call from ADF login screen.
     * @param subject
     * @param callbackHandler
     * @param sharedState
     * @param options
     */
    public void initialize(Subject subject, CallbackHandler callbackHandler,
                           Map<String, ?> sharedState,
                           Map<String, ?> options) {
   
        this.subject = subject;
        this.callbackHandler = callbackHandler;
       
        userMap = (HashMap)options.get("usermap");
       
        System.out.println("MULTIFONDS custom DB based login module initialized");
       
        if(subject.getPrincipals() != null){
            System.out.println("MF Custom Authentication provider subject is not null");
        }
      
    }
   
    /**
     * This is the method that will be called during doLogin() action from ADF login screen.
     * User entered credentials are captured and authenticated against database.
     * @return
     * @throws LoginException
     */
    public boolean login() throws LoginException {
       
        Callback[] callbacks;
        callbacks = new Callback[2];
        callbacks[0] = new NameCallback ("username:");
        callbacks[1] = new PasswordCallback("password:",false);
       
        try{
            System.out.println("-------Invoking CustomLoginModuleImpl login() method----");
            callbackHandler.handle(callbacks);
           
        }catch(IOException io){
            throw new LoginException(io.toString());
        }catch(UnsupportedCallbackException uce){
            throw new LoginException(uce.toString());                                         
        }
       
        String username = ((NameCallback)callbacks[0]).getName();
       
        System.out.println("MFCustomAuthenticator: DB param provided by the user to authenticate -->"+username);      
       
        char[] pw = ((PasswordCallback)callbacks[1]).getPassword();
        password = new String(pw);
       
        try{
            loginSucceeded = dbAuthenticate();   
        }catch(SQLException sqle){
            loginSucceeded = false;
            throw new FailedLoginException(sqle.getMessage());
        }
   
        principalsBeforeCommit.add(new WLSUserImpl(username));
   
        return loginSucceeded;
       
    }
   
    /**
     * User belonged security groups will be added by this method.
     * Assumptions:
     * If a user record has TL ID not empty and dept_cd is not REL then it is assumed that that users' role will be DEVELOPER
     * If a user record has MGR ID not empty and dept_cd is not REL then it is assumed that that users' role will be TL
     * If a user record has both TL ID and MGR ID and dept_cd is not REL then it is assumed that that users' role will be DEVELOPER
     * If a user record has MGR ID column empty and TL ID column empty and dept_cd is not REL then it is assumed that that users' role will be DEV_MGR
     * If a user record has dept_cd as REL then it is assumed that that users' role will be REL_MGR
     *
     */
    private void addGroupsForSubject(){
       
        String[] userGroups = new String[1];
       
        if(deptCd != null && deptCd.equalsIgnoreCase("REL")){
            userGroups[0] = REL_MGR_ROLE;
        }else{
           
            if((tlId != null) && (mgrId != null))
                userGroups[0] = DEVELOPER_ROLE;
           
            if(tlId != null)
                userGroups[0] = DEVELOPER_ROLE;
           
            if((tlId == null) && (mgrId != null))
                userGroups[0] = TL_ROLE;
           
            if((tlId == null) && (mgrId == null))
                userGroups[0] = DEV_MGR_ROLE;
        }
       
            System.out.println("\tgroupName\t= " + userGroups[0]);
            principalsBeforeCommit.add(new WLSGroupImpl(userGroups[0]));
    }

    /**
     * If authentication is successful then this method returns true
     * @return
     */
    public boolean commit() {
        boolean flag = false;
       
         if (loginSucceeded) {            
            subject.getPrincipals().addAll(principalsBeforeCommit);            
            System.out.println("CustomAuthenticator: Security groups --> " +subject);
            principalsInSubject = true;
            flag = true;
         }
         return flag;
    }

    /**
     * This method will be called when authentication fails
     * @return
     */
    public boolean abort() {
        if (principalsInSubject) {
            subject.getPrincipals().removeAll(principalsBeforeCommit);
            principalsInSubject = false;
        }
            System.out.println("CustomAuthenticator: Returning true in abort");
       
        return true;
    }

    /**
     * Not doing anything in this method as of now.
     * @return
     */
    public boolean logout() {
        return false;
    }
   
    /**
     * @param dbparam
     * @param password
     * @return
     * @throws SQLException
     */
    private boolean dbAuthenticate() throws SQLException{
        Connection conn = null;
        boolean flag = false;
        Properties connProperties = new Properties();
       
       
       
        connProperties.put("user", username);
        connProperties.put("password", password);
       
        PreparedStatement usergroupsPrepStmt = null;
        ResultSet rs = null;
        String userGrpSql = "SELECT TL_ID, MANAGER_ID,DEPT_CD FROM EMPLOYEE WHERE SHORT_CD=UPPER(?)";
       
        try{
            conn = DriverManager.getConnection("jdbc:oracle:thin:@"+host+":"+port +":"+db, connProperties);
            System.out.println("MFCustomAuthenticator: Successfully connected to DB. Now attempting to fetch user groups from the database...");
                       
            usergroupsPrepStmt = conn.prepareStatement(userGrpSql);
            usergroupsPrepStmt.setString(1, username);
            rs = usergroupsPrepStmt.executeQuery();
           
           
            while(rs.next()){
                tlId = rs.getString("TL_ID");
                mgrId = rs.getString("MANAGER_ID");
                deptCd = rs.getString("DEPT_CD");
            }
           
            System.out.println("MFCustomAuthenticator: TL_ID returned by SQL "+tlId);
            System.out.println("MFCustomAuthenticator: MANAGER_ID returned by SQL "+mgrId);
            System.out.println("MFCustomAuthenticator: DEPT_CD returned by SQL "+deptCd);
           
            addGroupsForSubject(); //Exception is not caught for this method.
                                   //Reason is, even if this fails authentication will still go through but usergroup will be null.
                                   //That has to be handled in the application code.
           
            flag = true;
           
        }catch(SQLException sqle){
            System.out.println("MFCustomAuthenticator: SQL error code returned "+sqle.getErrorCode());
            System.out.println("MFCustomAuthenticator: SQL error status returned is "+sqle.getSQLState());
            System.out.println("MFCustomAuthenticator: Actual error --> "+sqle);
           
            flag = false;
           
            if(sqle.getErrorCode() == 1017){
                throw new SQLException("MFCustomAuthenticator: invalid username/password; logon denied",sqle.getSQLState());
            }else{
                throw new SQLException("MFCustomAuthenticator:Authentication failed");
            }
           
        }finally{
            try{
              
                if(usergroupsPrepStmt != null){                   
                    usergroupsPrepStmt.close();
                    System.out.println("MFCustomAuthenticator: Successfully closed Prepared statement");
                }
              
                if(conn != null){
                    System.out.println("MFCustomAuthenticator: Closing the connection...");
                    conn.close();
                    flag = true;
                    System.out.println("MFCustomAuthenticator: Successfully closed the connection, setting the return flag to true");               
                }else{
                    System.out.println("MFCustomAuthenticator: No connection object was obtained");
                }
                   
                return flag;
               
            }catch(Exception e){
                System.out.println("MFCustomAuthenticator: Error in closing connection "+e);
            }
            return flag;
        }
                  
    }
}

Code of CustomAuthenticationProvider that implements weblogic AuthenticationProvider interface is provided below.

/**
 * Author: Aravindan Varadan
 * Created on: April 16 2014
 * Updated on: April 21 2014
 * Description: This is a custom authentication provider that will authenticate the users aganist database schema
 */

package adf.common.security.provider;

import java.util.HashMap;

import javax.security.auth.login.AppConfigurationEntry;

import weblogic.management.security.ProviderMBean;

import weblogic.security.spi.IdentityAsserter;
import weblogic.security.spi.PrincipalValidator;
import weblogic.security.spi.SecurityServices;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;

public final class CustomAuthenticationProvider implements weblogic.security.spi.AuthenticationProvider {
   
    private LoginModuleControlFlag controlFlag;
    private String description;
    private HashMap userGroupMapping = null;
   
    public CustomAuthenticationProvider() {
        super();
    }


    public AppConfigurationEntry getLoginModuleConfiguration() {
       
        HashMap<String, Object> options = new HashMap<String, Object>() ;
        options.put("usermap",userGroupMapping);
       
        return new AppConfigurationEntry("adf.common.security.provider.CustomLoginModuleImpl",controlFlag, options);
       
    }

    public AppConfigurationEntry getAssertionModuleConfiguration() {
        return null;
    }

    public PrincipalValidator getPrincipalValidator() {
        return null;
    }

    public IdentityAsserter getIdentityAsserter() {
        return null;
    }

    public void initialize(ProviderMBean providerMBean,
                           SecurityServices securityServices) {
       
        CustomAuthenticationProviderMBean mBean = (CustomAuthenticationProviderMBean)providerMBean;
        description = mBean.getDescription();
       
        System.out.println("Detected custom DB based authentication provider in WebLogic Server");
      
        String flag = mBean.getControlFlag();
        if(flag.equalsIgnoreCase("REQUIRED"))
            controlFlag = LoginModuleControlFlag.REQUIRED;
        else if(flag.equalsIgnoreCase("OPTIONAL"))
            controlFlag = LoginModuleControlFlag.OPTIONAL;
        else if(flag.equalsIgnoreCase("SUFFICIENT"))
            controlFlag = LoginModuleControlFlag.SUFFICIENT;
        else if(flag.equalsIgnoreCase("REQUISITE"))
            controlFlag = LoginModuleControlFlag.REQUISITE;
        else
            throw new IllegalArgumentException("INVALID CONTROL FLAG "+flag); 
        
    }

    public String getDescription() {
        return null;
    }

    public void shutdown() {
    }
   
}
 
The XML file that I used to generate mBean is given below.

<MBeanType Name="MFCustomAuthenticationProvider" DisplayName="MF custom DB based authentication provider" Package="multifonds.adf.common.security.provider"
Extends="weblogic.management.security.authentication.Authenticator" PersistPolicy="OnUpdate">

<MBeanAttribute Name="ProviderClassName" Type="java.lang.String" Writeable="false"Default=""multifonds.adf.common.security.provider.MFCustomAuthenticationProvider""/>

<MBeanAttribute Name="Description" Type="java.lang.String" Writeable="false" Default=""MF custom authentication provider""/>

<MBeanAttribute Name="Version" Type="java.lang.String" Writeable="false" Default=""1.0"" Description="The version of the weblogic authorization provider."/>

</MBeanType>

After following all the steps given in the URL if you restart the server you will see the log message “Detected custom DB based authentication provider in WebLogic Server”. This means that the new mBean is configured correctly. 

Post ADF login you should be taken to the home page of the application ("/adfAuthentication?success_url=/faces/home.jspx";).

Conclusion

This could be considered as a temporary but simple and efficient solution to leverage the same database that FORMS also uses to authenticate the users. As you can see in the code above we can even pull the user roles from the database that FORMS usually maintains in the database tables and set it in the subject. This can eventually be read through SecurityContext object in ADF. Hope few of you find this article useful!
Do let me know if you have any comments or better suggestions.