Forráskód Böngészése

Merge pull request #193 from idwright/cas-config

Cas configuration update
Jason 8 éve
szülő
commit
30eb81d9b6

+ 5 - 0
pom.xml

@@ -31,6 +31,11 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <build.number>0</build.number>  <!-- default in case not set on command line -->
         <build.revision>0</build.revision>  <!-- default in case not set on command line -->
+		<!-- Properties used for CAS configuration -->
+		<cas.server>https://cas.localdomain.local:8443/cas/</cas.server>
+        <pwm.server>https://pwm.localdomain.local:8443</pwm.server>
+        <java.cas.client.config.strategy>WEB_XML</java.cas.client.config.strategy>
+        <java.cas.client.config.location>/etc/java-cas-client.properties</java.cas.client.config.location>
     </properties>
 
     <profiles>

+ 4 - 1
src/main/java/password/pwm/config/PwmSetting.java

@@ -1075,7 +1075,10 @@ public enum PwmSetting {
     // CAS SSO
     CAS_CLEAR_PASS_URL(
             "cas.clearPassUrl", PwmSettingSyntax.STRING, PwmSettingCategory.CAS_SSO),
-
+    CAS_CLEARPASS_KEY(
+            "cas.clearPass.key", PwmSettingSyntax.FILE, PwmSettingCategory.CAS_SSO),
+    CAS_CLEARPASS_ALGORITHM(
+            "cas.clearPass.alg", PwmSettingSyntax.STRING, PwmSettingCategory.CAS_SSO),
     // http sso
     SSO_AUTH_HEADER_NAME(
             "security.sso.authHeaderName", PwmSettingSyntax.STRING, PwmSettingCategory.HTTP_SSO),

+ 2 - 2
src/main/java/password/pwm/http/filter/AuthenticationFilter.java

@@ -57,6 +57,7 @@ import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.BasicAuthInfo;
+import password.pwm.util.CASFilterAuthenticationProvider;
 import password.pwm.util.LocaleHelper;
 import password.pwm.util.logging.PwmLogger;
 
@@ -294,8 +295,7 @@ public class AuthenticationFilter extends AbstractPwmFilter {
     public static ProcessStatus attemptAuthenticationMethods(final PwmRequest pwmRequest) throws IOException, ServletException {
         final Set<AuthenticationMethod> authenticationMethods = new HashSet<>(Arrays.asList(AuthenticationMethod.values()));
         {
-            final String casUrl = pwmRequest.getConfig().readSettingAsString(PwmSetting.CAS_CLEAR_PASS_URL);
-            if (casUrl == null || casUrl.trim().isEmpty()) {
+            if (!CASFilterAuthenticationProvider.isFilterEnabled(pwmRequest)) {
                 authenticationMethods.remove(AuthenticationMethod.CAS);
             }
         }

+ 145 - 25
src/main/java/password/pwm/util/CASFilterAuthenticationProvider.java

@@ -22,14 +22,39 @@
 
 package password.pwm.util;
 
-import com.novell.ldapchai.exception.ChaiUnavailableException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Map;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.servlet.http.HttpSession;
+
+import org.jasig.cas.client.authentication.AttributePrincipal;
+import org.jasig.cas.client.ssl.HttpsURLConnectionFactory;
 import org.jasig.cas.client.util.AbstractCasFilter;
 import org.jasig.cas.client.util.CommonUtils;
 import org.jasig.cas.client.util.XmlUtils;
 import org.jasig.cas.client.validation.Assertion;
+
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+
 import password.pwm.PwmApplication;
 import password.pwm.PwmHttpFilterAuthenticationProvider;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.value.FileValue;
+import password.pwm.config.value.FileValue.FileContent;
+import password.pwm.config.value.FileValue.FileInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
@@ -41,13 +66,27 @@ import password.pwm.ldap.auth.SessionAuthenticator;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
-import javax.servlet.http.HttpSession;
-import java.io.UnsupportedEncodingException;
-
 public class CASFilterAuthenticationProvider implements PwmHttpFilterAuthenticationProvider {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(CASFilterAuthenticationProvider.class);
 
+    public static boolean isFilterEnabled(final PwmRequest pwmRequest) {
+        final String clearPassUrl = pwmRequest.getConfig().readSettingAsString(PwmSetting.CAS_CLEAR_PASS_URL);
+        
+        if (!(clearPassUrl == null || clearPassUrl.trim().isEmpty())) {
+            return true;
+        }
+        
+        final String alg = pwmRequest.getConfig().readSettingAsString(PwmSetting.CAS_CLEARPASS_ALGORITHM);
+        final Map<FileInformation, FileContent> privatekey = pwmRequest.getConfig().readSettingAsFile(PwmSetting.CAS_CLEARPASS_KEY);
+        
+        if (!privatekey.isEmpty() && (!(alg == null || alg.trim().isEmpty()))) {
+            return true;
+        }
+    
+        return false;
+    }
+    
     @Override
     public void attemptAuthentication(
             final PwmRequest pwmRequest
@@ -55,10 +94,10 @@ public class CASFilterAuthenticationProvider implements PwmHttpFilterAuthenticat
             throws PwmUnrecoverableException
     {
         try {
-            final String clearPassUrl = pwmRequest.getConfig().readSettingAsString(PwmSetting.CAS_CLEAR_PASS_URL);
-            if (clearPassUrl != null && clearPassUrl.length() > 0) {
+             
+            if (CASFilterAuthenticationProvider.isFilterEnabled(pwmRequest)) {
                 LOGGER.trace(pwmRequest, "checking for authentication via CAS");
-                if (authUserUsingCASClearPass(pwmRequest, clearPassUrl)) {
+                if (authUserUsingCASClearPass(pwmRequest)) {
                     LOGGER.debug(pwmRequest, "login via CAS successful");
                 }
             }
@@ -77,9 +116,7 @@ public class CASFilterAuthenticationProvider implements PwmHttpFilterAuthenticat
     }
 
     private static boolean authUserUsingCASClearPass(
-            final PwmRequest pwmRequest,
-            final String clearPassUrl
-    )
+            final PwmRequest pwmRequest)
             throws UnsupportedEncodingException, PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
@@ -98,24 +135,46 @@ public class CASFilterAuthenticationProvider implements PwmHttpFilterAuthenticat
             return false;
         }
 
-        // read cas proxy ticket
-        final String proxyTicket = assertion.getPrincipal().getProxyTicketFor(clearPassUrl);
-        if (proxyTicket == null) {
-            LOGGER.trace(pwmSession,"no CAS proxy ticket available, skipping CAS authentication attempt");
-            return false;
+        final String username = assertion.getPrincipal().getName();
+        PasswordData password = null;
+        final AttributePrincipal attributePrincipal = assertion.getPrincipal();
+        final Map<String, Object> casAttributes = attributePrincipal.getAttributes();
+        
+        final String encodedPsw = (String) casAttributes.get("credential");
+        if (encodedPsw == null) {
+            LOGGER.trace("No credential");
+        } else {
+            final Map<FileInformation, FileContent> privatekey = pwmRequest.getConfig().readSettingAsFile(PwmSetting.CAS_CLEARPASS_KEY);
+            final String alg = pwmRequest.getConfig().readSettingAsString(PwmSetting.CAS_CLEARPASS_ALGORITHM);
+
+            password = decryptPassword(alg, privatekey, encodedPsw);
         }
+        
+        // If using the old method
+        final String clearPassUrl = pwmRequest.getConfig().readSettingAsString(PwmSetting.CAS_CLEAR_PASS_URL);
+        if ((clearPassUrl != null && clearPassUrl.length() > 0) && (password == null || password.getStringValue().length() < 1)) {
+            LOGGER.trace(pwmSession, "Using CAS clearpass via proxy");
+            // read cas proxy ticket
+            final String proxyTicket = assertion.getPrincipal().getProxyTicketFor(clearPassUrl);
+            if (proxyTicket == null) {
+                LOGGER.trace(pwmSession,"no CAS proxy ticket available, skipping CAS authentication attempt");
+                return false;
+            }
 
-        final String clearPassRequestUrl = clearPassUrl + "?" + "ticket="
-                + proxyTicket + "&" + "service="
-                + StringUtil.urlEncode(clearPassUrl);
-
-        final String response = CommonUtils.getResponseFromServer(
-                clearPassRequestUrl, "UTF-8");
-
-        final String username = assertion.getPrincipal().getName();
-        final PasswordData password = new PasswordData(XmlUtils.getTextForElement(response, "credentials"));
+            final String clearPassRequestUrl = clearPassUrl + "?" + "ticket="
+                    + proxyTicket + "&" + "service="
+                    + StringUtil.urlEncode(clearPassUrl);
 
-        if (password == null) {
+            try {
+                final String response = CommonUtils.getResponseFromServer(
+                        new URL(clearPassRequestUrl), new HttpsURLConnectionFactory(), "UTF-8");
+                password = new PasswordData(XmlUtils.getTextForElement(response, "credentials"));
+            } catch (MalformedURLException e) {
+                LOGGER.error(pwmSession, "Invalid CAS clearPassUrl");
+            }
+            
+        }
+        if (password == null || password.getStringValue().length() < 1) {
             final String errorMsg = "CAS server did not return credentials for user '" + username + "'";
             LOGGER.trace(pwmSession, errorMsg);
             final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_WRONGPASSWORD,errorMsg);
@@ -128,4 +187,65 @@ public class CASFilterAuthenticationProvider implements PwmHttpFilterAuthenticat
         sessionAuthenticator.searchAndAuthenticateUser(username, password, null, null);
         return true;
     }
+
+    private static PasswordData decryptPassword(final String alg,
+            final Map<FileInformation, FileContent> privatekey, final String encodedPsw)
+            {
+        PasswordData password = null;
+        
+        if (alg == null || alg.trim().isEmpty()) {
+            return password;
+        }
+        
+        final byte[] privateKeyBytes;
+        if (privatekey != null && !privatekey.isEmpty()) {
+            final FileValue.FileInformation fileInformation1 = privatekey.keySet().iterator().next();
+            final FileValue.FileContent fileContent = privatekey.get(fileInformation1);
+            privateKeyBytes = fileContent.getContents();
+        } else {
+            privateKeyBytes = null;
+        }
+        
+        if (privateKeyBytes != null) {
+            final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes);
+            try {
+                final KeyFactory kf = KeyFactory.getInstance(alg);
+                final PrivateKey privateKey = kf.generatePrivate(spec);
+                final Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
+                final byte[] cred64 = StringUtil.base64Decode(encodedPsw);
+                cipher.init(Cipher.DECRYPT_MODE, privateKey);
+                final byte[] cipherData = cipher.doFinal(cred64);
+                if (cipherData != null) {
+                    try {
+                        password = new PasswordData(new String(cipherData));
+                    } catch (PwmUnrecoverableException e) {
+                        LOGGER.error("Decryption failed", e);
+                        return password;
+                    }
+                }
+            } catch (NoSuchAlgorithmException e1) {
+                LOGGER.error("Decryption failed", e1);
+                return password;
+            } catch (InvalidKeySpecException e1) {
+                LOGGER.error("Decryption failed", e1);
+                return password;
+            } catch (NoSuchPaddingException e1) {
+                LOGGER.error("Decryption failed", e1);
+                return password;
+            } catch (IOException e1) {
+                LOGGER.error("Decryption failed", e1);
+                return password;
+            } catch (InvalidKeyException e1) {
+                LOGGER.error("Decryption failed", e1);
+                return password;
+            } catch (IllegalBlockSizeException e) {
+                LOGGER.error("Decryption failed", e);
+                return password;
+            } catch (BadPaddingException e) {
+                LOGGER.error("Decryption failed", e);
+                return password;
+            }
+        }
+        return password;
+    }
 }

+ 10 - 0
src/main/resources/password/pwm/config/PwmSetting.xml

@@ -3591,6 +3591,16 @@
             <value />
         </default>
     </setting>
+    <setting hidden="false" key="cas.clearPass.key" level="2">
+        <default>
+            <value />
+        </default>
+    </setting>
+    <setting hidden="false" key="cas.clearPass.alg" level="2">
+        <default>
+            <value />
+        </default>
+    </setting>
     <setting hidden="false" key="urlshortener.classname" level="2">
         <default>
             <value />

+ 5 - 1
src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -230,7 +230,9 @@ Setting_Description_captcha.recaptcha.privateKey=Add a private reCAPTCHA key.  I
 Setting_Description_captcha.recaptcha.publicKey=Add a public reCAPTCHA key.  If blank, @PwmAppName@ does not perform the CAPTCHA verification.
 Setting_Description_captcha.skip.cookie=Specify a known browser cookie value in a cookie named 'captcha-key'.  This allows @PwmAppName@ to skip the CAPTCHA request if the value of the browser cookie is correct.  @PwmAppName@ stores the cookie value in the browser after a successful CAPTCHA check.<br/><br/>If blank, then @PwmAppName@ does not store nor read the browser cookie.  If set to 'INSTANCEID', then @PwmAppName@ uses the instanceID.  If set to any other value, then @PwmAppName@ uses the literal value.
 Setting_Description_captcha.skip.param=Specify a parameter with a key of "skipCaptcha" to allow @PwmAppName@ to skip the CAPTCHA request. This is useful for "internal" clients or links where the CAPTCHA is unneccessary.<br/><br/>For example, if the value is 'okay', a request to\:<br/><br/><i>/public/forgottenpassword?skipCaptcha\=okay</i><br/><br/> causes @PwmAppName@ to bypass the CAPTCHA.
-Setting_Description_cas.clearPassUrl=For <a href\="http\://www.jasig.org/cas">CAS</a> authentication integration, specify the ClearPass URL.  If blank, @PwmAppName@ disables the CAS authentication integration.<br/><br/>You must also edit the <i>WEB-INF/web.xml</i> file to enable CAS integration.  Uncomment the section for the CAS servlet filters, and modify the CAS servlet parameters as appropriate for your configuration.
+Setting_Description_cas.clearPassUrl=For <a href\="http\://www.jasig.org/cas">CAS</a> authentication integration, enter the ClearPass url here.  If blank, CAS authentication integration will be disabled.<br/><br/>You will also need to edit the <i>WEB-INF/web.xml</i> file to enable CAS integration.  Uncomment the section for the CAS servlet filters, and modify the CAS servlet parameters as appropriate for your configuration.
+Setting_Description_cas.clearPass.key=<a href\="https://apereo.github.io/cas/4.2.x/integration/ClearPass.html">ClearPass</a> encryption key
+Setting_Description_cas.clearPass.alg=The algorithm used by the encryption key
 Setting_Description_challenge.allowDuplicateResponses=Enable this option if you require each response to be unique.
 Setting_Description_challenge.allowSetup.queryMatch=Specify the permissions used to determine if you permits the users to configure challenges.  This LDAP query must return the user or else @PwmAppName@ does not permit the user to configure challenges.
 Setting_Description_challenge.allowUnlock=Enable this option if @PwmAppName@ allows user accounts to be unlocked during forgotten password.  If true, and if the users' accounts are locked due to too many invalid login attempts, and the users' passwords are not expired, then @PwmAppName@ gives the users a chance to unlock their accounts instead of resetting their passwords.
@@ -703,6 +705,8 @@ Setting_Label_captcha.recaptcha.publicKey=reCAPTCHA Public Key
 Setting_Label_captcha.skip.cookie=Captcha Skip Cookie
 Setting_Label_captcha.skip.param=Captcha Skip Parameter Value
 Setting_Label_cas.clearPassUrl=CAS ClearPass URL
+Setting_Label_cas.clearPass.key=CAS ClearPass Encryption Key
+Setting_Label_cas.clearPass.alg=CAS ClearPass Algorithm
 Setting_Label_challenge.allowDuplicateResponses=Allow Duplicate Responses
 Setting_Label_challenge.allowSetup.queryMatch=Save Challenge Permission
 Setting_Label_challenge.allowUnlock=Allow Unlock

+ 26 - 6
src/main/webapp/WEB-INF/web.xml

@@ -35,6 +35,18 @@
         <param-name>applicationPath</param-name>
         <param-value>unspecified</param-value>
     </context-param>
+    <!-- uncomment and set parameters for CAS integration
+    <context-param>
+        <param-name>configurationStrategy</param-name>
+        <param-value>${java.cas.client.config.strategy}</param-value>
+    </context-param>
+
+    <context-param>
+        <param-name>configFileLocation</param-name>
+        <param-value>${java.cas.client.config.location}</param-value>
+    </context-param>
+    End CAS Config -->
+    
     <welcome-file-list>
         <welcome-file>index.jsp</welcome-file>
     </welcome-file-list>
@@ -59,42 +71,50 @@
     <filter>
         <filter-name>CAS Validation Filter</filter-name>
         <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
+        <!- cas-config-web.xml start ->
         <init-param>
             <param-name>casServerUrlPrefix</param-name>
-            <param-value>https://cas.localdomain.local:8443/cas/</param-value>
+            <param-value>${cas.server}</param-value>
         </init-param>
         <init-param>
             <param-name>serverName</param-name>
-            <param-value>https://pwm.localdomain.local:8443</param-value>
+            <param-value>${pwm.server}</param-value>
         </init-param>
         <init-param>
             <param-name>proxyCallbackUrl</param-name>
-            <param-value>https://pwm.localdomain.local:8443/pwm/proxyCallback</param-value>
+            <param-value>${pwm.server}pwm/proxyCallback</param-value>
         </init-param>
         <init-param>
             <param-name>proxyReceptorUrl</param-name>
             <param-value>/proxyCallback</param-value>
         </init-param>
+        <!- cas-config-web.xml end ->
     </filter>
     <filter>
         <filter-name>CAS Authentication Filter</filter-name>
         <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
+        <!- cas-config-web.xml start ->
         <init-param>
             <param-name>casServerLoginUrl</param-name>
-            <param-value>https://cas.localdomain.local:8443/cas/login</param-value>
+            <param-value>${cas.server}login</param-value>
         </init-param>
         <init-param>
             <param-name>serverName</param-name>
-            <param-value>https://pwm.localdomain.local:8443</param-value>
+            <param-value>${pwm.server}</param-value>
         </init-param>
         <init-param>
             <param-name>gateway</param-name>
             <param-value>false</param-value>
         </init-param>
+        <!- cas-config-web.xml end ->
     </filter>
     <filter>
         <filter-name>CAS Single Sign Out Filter</filter-name>
         <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
+        <init-param>
+			<param-name>casServerUrlPrefix</param-name>
+			<param-value>${cas.server}</param-value>
+		</init-param>
     </filter>
     <filter-mapping>
         <filter-name>CAS Single Sign Out Filter</filter-name>
@@ -115,7 +135,7 @@
         <filter-name>CAS Validation Filter</filter-name>
         <url-pattern>/proxyCallback</url-pattern>
     </filter-mapping>
-    -->
+    End CAS Config -->
     <filter>
         <filter-name>GZIPFilter</filter-name>
         <filter-class>password.pwm.http.filter.GZIPFilter</filter-class>