瀏覽代碼

Support for new style of CAS clearPass with encrypted passwords

idwright 9 年之前
父節點
當前提交
dfd14b4ad3

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

@@ -1062,7 +1062,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),

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

@@ -55,6 +55,7 @@ import password.pwm.ldap.auth.SessionAuthenticator;
 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;
 
@@ -291,10 +292,9 @@ 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()) {
-                authenticationMethods.remove(AuthenticationMethod.CAS);
-            }
+        	if (!CASFilterAuthenticationProvider.isFilterEnabled(pwmRequest)) {
+        		authenticationMethods.remove(AuthenticationMethod.CAS);
+        	}
         }
         for (final AuthenticationMethod authenticationMethod : authenticationMethods) {
             if (!pwmRequest.isAuthenticated()) {

+ 159 - 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;
@@ -40,13 +65,27 @@ import password.pwm.ldap.auth.PwmAuthenticationSource;
 import password.pwm.ldap.auth.SessionAuthenticator;
 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
@@ -54,10 +93,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");
                 }
             }
@@ -76,9 +115,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();
@@ -97,24 +134,47 @@ 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;
+	    AttributePrincipal attributePrincipal = assertion.getPrincipal();
+		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);
 
-        final String clearPassRequestUrl = clearPassUrl + "?" + "ticket="
-                + proxyTicket + "&" + "service="
-                + StringUtil.urlEncode(clearPassUrl);
+			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 response = CommonUtils.getResponseFromServer(
-                clearPassRequestUrl, "UTF-8");
+			final String clearPassRequestUrl = clearPassUrl + "?" + "ticket="
+					+ proxyTicket + "&" + "service="
+					+ StringUtil.urlEncode(clearPassUrl);
 
-        final String username = assertion.getPrincipal().getName();
-        final PasswordData password = new PasswordData(XmlUtils.getTextForElement(response, "credentials"));
-
-        if (password == null) {
+			String response;
+			try {
+				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);
@@ -127,4 +187,78 @@ public class CASFilterAuthenticationProvider implements PwmHttpFilterAuthenticat
         sessionAuthenticator.searchAndAuthenticateUser(username, password, null, null);
         return true;
     }
+
+	private static PasswordData decryptPassword(final String alg,
+			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);
+			KeyFactory kf;
+			try {
+				kf = KeyFactory.getInstance(alg);
+			} catch (NoSuchAlgorithmException e1) {
+				LOGGER.error("Decryption failed", e1);
+				return password;
+			}
+			PrivateKey privateKey;
+			try {
+				privateKey = kf.generatePrivate(spec);
+			} catch (InvalidKeySpecException e1) {
+				LOGGER.error("Decryption failed", e1);
+				return password;			}
+			Cipher cipher;
+			try {
+				cipher = Cipher.getInstance(privateKey.getAlgorithm());
+			} catch (NoSuchAlgorithmException | NoSuchPaddingException e1) {
+				LOGGER.error("Decryption failed", e1);
+				return password;			}
+			byte[] cred64;
+			try {
+				cred64 = StringUtil.base64Decode(encodedPsw);
+			} catch (IOException e1) {
+				LOGGER.error("Decryption failed", e1);
+				return password;			}
+			try {
+				cipher.init(Cipher.DECRYPT_MODE, privateKey);
+			} catch (InvalidKeyException e1) {
+				LOGGER.error("Decryption failed", e1);
+				return password;
+			}
+			byte[] cipherData = null;
+			try {
+
+				cipherData = cipher.doFinal(cred64);
+			} catch (IllegalBlockSizeException e) {
+				LOGGER.error("Decryption failed", e);
+				return password;
+			} catch (BadPaddingException e) {
+				LOGGER.error("Decryption failed", e);
+				return password;
+			}
+			if (cipherData != null) {
+				try {
+					password = new PasswordData(new String(cipherData));
+				} catch (PwmUnrecoverableException e) {
+					LOGGER.error("Decryption failed", e);
+					return password;
+				}
+			}
+		}
+		return password;
+	}
 }

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

@@ -3408,6 +3408,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 />

+ 4 - 0
src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -233,6 +233,8 @@ Setting_Description_captcha.recaptcha.publicKey=Add a public reCAPTCHA key.  If
 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, 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=Control if each response is required to be unique
 Setting_Description_challenge.allowSetup.queryMatch=Permission used to determine if a user is a permitted to configure challenges.  The user must be returned during this ldap query or else the user will not be permitted to configure challenges.
 Setting_Description_challenge.allowUnlock=Allow unlock during forgotten password.  If true, and if the user's account is locked due to too many invalid login attempts, and the user's password is not expired, then the user will be given a chance to unlock their account instead of resetting their password.
@@ -704,6 +706,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