Tuesday, May 29, 2012

Liferay and CAS SSO

I am attempting to use CAS SSO as the authentication handler for Liferay. The architecture for it is as following: I want to use the CAS-SSO as the authentication server. And I am trying to set up Liferay as the database of record for the user records, and authenticate all username/passwords against the Liferay database. Out of the box the CAS-SSO is supposed to support authenticating against a database. The mechanism CAS uses is a series of 'AuthenticationHandlers' that are injected when the server is started up, and are defined in the cas-web\WEB-INF\deployerConfigContext.xml file. The only difficult part of the configuration of this for Liferay is the fact that Liferay stores it's password in a salted and encrypted form. To get around this, a 'Password Encoder' class needs to be created that matches the Liferay mechanism. This encoder can then be injected along with the database information in the CAS configuration file as below:
 <bean class="org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler">  
                          <property name="tableUsers">  
                               <value>User_</value>  
                          </property>  
                          <property name="fieldUser">  
                               <value>emailAddress</value>  
                          </property>  
                          <property name="fieldPassword">  
                               <value>password_</value>  
                          </property>  
                          <property name="passwordENcoder">  
                               <bean class="com.sample.libraries.sso.LiferayPasswordEncoder">  
                                    <constructor-arg name="encodingAlgorithm" value="SHA"/>  
                               </bean>  
                          </property>  
                          <property name="dataSource" ref="dataSource" />                      
                     </bean>  
 <!-- Data source .. point this to the Liferay Database -->  
      <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">  
           <property name="driverClassName">  
                <value>com.postgressql.jdbc.Driver</value>  
           </property>  
           <property name="url">  
                <value>jdbc:postgresql://localhost:5432/lportal</value>  
           </property>  
           <property name="username">  
                <value>${user.name}</value>  
           </property>  
           <property name="password">  
                <value>${pass.word}</value>  
           </property>  
      </bean>  
Sample password encoder:
 package com.sample.libraries.sso;  
 import java.io.UnsupportedEncodingException;  
 import java.security.MessageDigest;  
 import java.security.NoSuchAlgorithmException;  
 import java.security.SecureRandom;  
 import java.util.Random;  
 import javax.validation.constraints.NotNull;  
 import org.jasig.cas.authentication.handler.PasswordEncoder;  
 import org.vps.crypt.Crypt;  
 import com.liferay.portal.kernel.util.Base64;  
 import com.liferay.portal.kernel.util.Digester;  
 import com.liferay.portal.kernel.util.Validator;  
 public class LiferayPasswordEncoder implements org.jasig.cas.authentication.handler.PasswordEncoder {  
       public static final String UTF8 = "UTF-8";  
        public static final String TYPE_CRYPT = "CRYPT";  
        public static final String TYPE_MD2 = "MD2";  
        public static final String TYPE_MD5 = "MD5";  
        public static final String TYPE_NONE = "NONE";  
        public static final String TYPE_SHA = "SHA";  
        public static final String TYPE_SHA_256 = "SHA-256";  
        public static final String TYPE_SHA_384 = "SHA-384";  
        public static final String TYPE_SSHA = "SSHA";  
        public static final DigesterImpl digesterImpl = new DigesterImpl();  
        @NotNull  
        private static String PASSWORDS_ENCRYPTION_ALGORITHM = TYPE_SHA;  
        public LiferayPasswordEncoder() {  
        }  
        public LiferayPasswordEncoder(final String encodingAlgorithm) {  
          PASSWORDS_ENCRYPTION_ALGORITHM = encodingAlgorithm;  
        }  
        public static final char[] saltChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"  
            .toCharArray();  
        public static String encrypt(String clearTextPassword) {  
          return encrypt(PASSWORDS_ENCRYPTION_ALGORITHM, clearTextPassword, null);  
        }  
        public static String encrypt(String clearTextPassword,  
            String currentEncryptedPassword) {  
          return encrypt(PASSWORDS_ENCRYPTION_ALGORITHM, clearTextPassword,  
              currentEncryptedPassword);  
        }  
        public static String encrypt(String algorithm, String clearTextPassword,  
            String currentEncryptedPassword) {  
          if (algorithm.equals(TYPE_CRYPT)) {  
            byte[] saltBytes = _getSaltFromCrypt(currentEncryptedPassword);  
            return encodePassword(algorithm, clearTextPassword, saltBytes);  
          } else if (algorithm.equals(TYPE_NONE)) {  
            return clearTextPassword;  
          } else if (algorithm.equals(TYPE_SSHA)) {  
            byte[] saltBytes = _getSaltFromSSHA(currentEncryptedPassword);  
            return encodePassword(algorithm, clearTextPassword, saltBytes);  
          } else {  
            return encodePassword(algorithm, clearTextPassword, null);  
          }  
        }  
        protected static String encodePassword(String algorithm,  
            String clearTextPassword, byte[] saltBytes) {  
          try {  
            if (algorithm.equals(TYPE_CRYPT)) {  
              return Crypt.crypt(saltBytes, clearTextPassword.getBytes(UTF8));  
            } else if (algorithm.equals(TYPE_SSHA)) {  
              byte[] clearTextPasswordBytes = clearTextPassword  
                  .getBytes(UTF8);  
              // Create a byte array of salt bytes appeneded to password bytes  
              byte[] pwdPlusSalt = new byte[clearTextPasswordBytes.length  
                  + saltBytes.length];  
              System.arraycopy(clearTextPasswordBytes, 0, pwdPlusSalt, 0,  
                  clearTextPasswordBytes.length);  
              System.arraycopy(saltBytes, 0, pwdPlusSalt,  
                  clearTextPasswordBytes.length, saltBytes.length);  
              // Digest byte array  
              MessageDigest sha1Digest = MessageDigest.getInstance("SHA-1");  
              byte[] pwdPlusSaltHash = sha1Digest.digest(pwdPlusSalt);  
              // Appends salt bytes to the SHA-1 digest.  
              byte[] digestPlusSalt = new byte[pwdPlusSaltHash.length  
                  + saltBytes.length];  
              System.arraycopy(pwdPlusSaltHash, 0, digestPlusSalt, 0,  
                  pwdPlusSaltHash.length);  
              System.arraycopy(saltBytes, 0, digestPlusSalt,  
                  pwdPlusSaltHash.length, saltBytes.length);  
              // Base64 encode and format string  
              return Base64.encode(digestPlusSalt);  
            } else {  
              return digesterImpl.digest(algorithm, clearTextPassword);  
            }  
          } catch (NoSuchAlgorithmException nsae) {  
            throw new SecurityException("LiferayPasswordEncryption error:"  
                + nsae.getMessage(), nsae);  
          } catch (UnsupportedEncodingException uee) {  
            throw new SecurityException("LiferayPasswordEncryption error:"  
                + uee.getMessage(), uee);  
          }  
        }  
        private static byte[] _getSaltFromCrypt(String cryptString) {  
          byte[] saltBytes = null;  
          try {  
            if (Validator.isNull(cryptString)) {  
              // Generate random salt  
              Random random = new Random();  
              int numSaltChars = saltChars.length;  
              StringBuilder sb = new StringBuilder();  
              int x = random.nextInt(Integer.MAX_VALUE) % numSaltChars;  
              int y = random.nextInt(Integer.MAX_VALUE) % numSaltChars;  
              sb.append(saltChars[x]);  
              sb.append(saltChars[y]);  
              String salt = sb.toString();  
              saltBytes = salt.getBytes(Digester.ENCODING);  
            } else {  
              // Extract salt from encrypted password  
              String salt = cryptString.substring(0, 2);  
              saltBytes = salt.getBytes(Digester.ENCODING);  
            }  
          } catch (UnsupportedEncodingException uee) {  
            throw new SecurityException(  
                "Unable to extract salt from encrypted password: "  
                    + uee.getMessage(), uee);  
          }  
          return saltBytes;  
        }  
        private static byte[] _getSaltFromSSHA(String sshaString) {  
          byte[] saltBytes = new byte[8];  
          if (Validator.isNull(sshaString)) {  
            // Generate random salt  
            Random random = new SecureRandom();  
            random.nextBytes(saltBytes);  
          } else {  
            // Extract salt from encrypted password  
            try {  
              byte[] digestPlusSalt = Base64.decode(sshaString);  
              byte[] digestBytes = new byte[digestPlusSalt.length - 8];  
              System.arraycopy(digestPlusSalt, 0, digestBytes, 0,  
                  digestBytes.length);  
              System.arraycopy(digestPlusSalt, digestBytes.length, saltBytes,  
                  0, saltBytes.length);  
            } catch (Exception e) {  
              throw new SecurityException(  
                  "Unable to extract salt from encrypted password: "  
                      + e.getMessage(), e);  
            }  
          }  
          return saltBytes;  
        }  
        public String encode(String pwd) {  
          return encrypt(pwd);  
        }  
 }