Handle encrypted PGTs (#260)

* Handle encrypted PGTs

* add tests

* stick to Java 6 and use commons-codec for Base64

* Remove the encrypted PGT after a PGT has been retrieved

* use Bouncycastle to load PEM files

* update to latest BC dependency
This commit is contained in:
LELEU Jérôme 2019-04-03 13:53:09 +02:00 committed by GitHub
parent 96f51465a8
commit abacb75df2
8 changed files with 252 additions and 41 deletions

View File

@ -371,6 +371,8 @@ Validates the tickets using the CAS 2.0 protocol. If you provide either the `acc
| `millisBetweenCleanUps` | Startup delay for the cleanup task to remove expired tickets from the storage. Defaults to `60000 msec` | No
| `ticketValidatorClass` | Ticket validator class to use/create | No
| `hostnameVerifier` | Hostname verifier class name, used when making back-channel calls | No
| `privateKeyPath` | The path to a private key to decrypt PGTs directly sent encrypted as an attribute | No
| `privateKeyAlgorithm` | The algorithm of the private key. Defaults to `RSA` | No
#### org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter
Validates the tickets using the CAS 3.0 protocol. If you provide either the `acceptAnyProxy` or the `allowedProxyChains` parameters,

View File

@ -55,6 +55,8 @@ public interface ConfigurationKeys {
ConfigurationKey<String> CAS_SERVER_URL_PREFIX = new ConfigurationKey<String>("casServerUrlPrefix", null);
ConfigurationKey<String> ENCODING = new ConfigurationKey<String>("encoding", null);
ConfigurationKey<Long> TOLERANCE = new ConfigurationKey<Long>("tolerance", 1000L);
ConfigurationKey<String> PRIVATE_KEY_PATH = new ConfigurationKey<String>("privateKeyPath", null);
ConfigurationKey<String> PRIVATE_KEY_ALGORITHM = new ConfigurationKey<String>("privateKeyAlgorithm", "RSA");
/**
* @deprecated As of 3.4. This constant is not used by the client and will

View File

@ -0,0 +1,90 @@
package org.jasig.cas.client.util;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
/**
* Utility class to parse private keys.
*
* @author Jerome LELEU
* @since 3.6.0
*/
public class PrivateKeyUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(PrivateKeyUtils.class);
static {
Security.addProvider(new BouncyCastleProvider());
}
public static PrivateKey createKey(final String path, final String algorithm) {
PrivateKey key = readPemPrivateKey(path);
if (key == null) {
return readDERPrivateKey(path, algorithm);
} else {
return key;
}
}
private static PrivateKey readPemPrivateKey(final String path) {
LOGGER.debug("Attempting to read as PEM [{}]", path);
final File file = new File(path);
InputStreamReader isr = null;
BufferedReader br = null;
try {
isr = new FileReader(file);
br = new BufferedReader(isr);
final PEMParser pp = new PEMParser(br);
final PEMKeyPair pemKeyPair = (PEMKeyPair) pp.readObject();
final KeyPair kp = new JcaPEMKeyConverter().getKeyPair(pemKeyPair);
return kp.getPrivate();
} catch (final Exception e) {
LOGGER.error("Unable to read key", e);
return null;
} finally {
try {
if (br != null) {
br.close();
}
if (isr != null) {
isr.close();
}
} catch (final IOException e) {}
}
}
private static PrivateKey readDERPrivateKey(final String path, final String algorithm) {
LOGGER.debug("Attempting to read key as DER [{}]", path);
final File file = new File(path);
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
long byteLength = file.length();
byte[] bytes = new byte[(int) byteLength];
fis.read(bytes, 0, (int) byteLength);
final PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(bytes);
final KeyFactory factory = KeyFactory.getInstance(algorithm);
return factory.generatePrivate(privSpec);
} catch (final Exception e) {
LOGGER.error("Unable to read key", e);
return null;
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (final IOException e) {}
}
}
}

View File

@ -19,6 +19,7 @@
package org.jasig.cas.client.validation;
import java.io.IOException;
import java.security.PrivateKey;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
@ -30,6 +31,7 @@ import org.jasig.cas.client.proxy.*;
import org.jasig.cas.client.ssl.HttpURLConnectionFactory;
import org.jasig.cas.client.ssl.HttpsURLConnectionFactory;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.PrivateKeyUtils;
import org.jasig.cas.client.util.ReflectUtils;
import static org.jasig.cas.client.configuration.ConfigurationKeys.*;
@ -54,7 +56,7 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal
TOLERANCE.getName(), IGNORE_PATTERN.getName(), IGNORE_URL_PATTERN_TYPE.getName(), HOSTNAME_VERIFIER.getName(), HOSTNAME_VERIFIER_CONFIG.getName(),
EXCEPTION_ON_VALIDATION_FAILURE.getName(), REDIRECT_AFTER_VALIDATION.getName(), USE_SESSION.getName(), SECRET_KEY.getName(), CIPHER_ALGORITHM.getName(), PROXY_RECEPTOR_URL.getName(),
PROXY_GRANTING_TICKET_STORAGE_CLASS.getName(), MILLIS_BETWEEN_CLEAN_UPS.getName(), ACCEPT_ANY_PROXY.getName(), ALLOWED_PROXY_CHAINS.getName(), TICKET_VALIDATOR_CLASS.getName(),
PROXY_CALLBACK_URL.getName(), RELAY_STATE_PARAMETER_NAME.getName()
PROXY_CALLBACK_URL.getName(), RELAY_STATE_PARAMETER_NAME.getName(), METHOD.getName(), PRIVATE_KEY_PATH.getName(), PRIVATE_KEY_ALGORITHM.getName()
};
/**
@ -72,6 +74,8 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal
protected Class<? extends Cas20ProxyTicketValidator> defaultProxyTicketValidatorClass;
private PrivateKey privateKey;
/**
* Storage location of ProxyGrantingTickets and Proxy Ticket IOUs.
*/
@ -113,6 +117,8 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal
}
this.millisBetweenCleanUps = getInt(ConfigurationKeys.MILLIS_BETWEEN_CLEAN_UPS);
this.privateKey = buildPrivateKey(getString(PRIVATE_KEY_PATH), getString(PRIVATE_KEY_ALGORITHM));
super.initInternal(filterConfig);
}
@ -139,6 +145,13 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal
return (T) ReflectUtils.newInstance(ticketValidatorClass, casServerUrlPrefix);
}
public static PrivateKey buildPrivateKey(final String keyPath, final String keyAlgorithm) {
if (keyPath != null) {
return PrivateKeyUtils.createKey(keyPath, keyAlgorithm);
}
return null;
}
/**
* Constructs a Cas20ServiceTicketValidator or a Cas20ProxyTicketValidator based on supplied parameters.
*
@ -184,6 +197,8 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal
}
}
validator.setPrivateKey(this.privateKey);
validator.setCustomParameters(additionalParameters);
return validator;
}

View File

@ -19,9 +19,13 @@
package org.jasig.cas.client.validation;
import java.io.StringReader;
import java.security.PrivateKey;
import java.util.*;
import javax.crypto.Cipher;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.commons.codec.binary.Base64;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.authentication.AttributePrincipalImpl;
import org.jasig.cas.client.proxy.Cas20ProxyRetriever;
@ -43,6 +47,9 @@ import org.xml.sax.helpers.DefaultHandler;
*/
public class Cas20ServiceTicketValidator extends AbstractCasProtocolUrlBasedTicketValidator {
public static final String PGT_ATTRIBUTE = "proxyGrantingTicket";
private static final String PGTIOU_PREFIX = "PGTIOU-";
/** The CAS 2.0 protocol proxy callback url. */
private String proxyCallbackUrl;
@ -52,12 +59,14 @@ public class Cas20ServiceTicketValidator extends AbstractCasProtocolUrlBasedTick
/** Implementation of the proxy retriever. */
private ProxyRetriever proxyRetriever;
/** Private key for decryption */
private PrivateKey privateKey;
/**
* Constructs an instance of the CAS 2.0 Service Ticket Validator with the supplied
* CAS server url prefix.
*
* @param casServerUrlPrefix the CAS Server URL prefix.
* @param urlFactory URL connection factory to use when communicating with the server
*/
public Cas20ServiceTicketValidator(final String casServerUrlPrefix) {
super(casServerUrlPrefix);
@ -85,14 +94,7 @@ public class Cas20ServiceTicketValidator extends AbstractCasProtocolUrlBasedTick
}
final String principal = parsePrincipalFromResponse(response);
final String proxyGrantingTicketIou = parseProxyGrantingTicketFromResponse(response);
final String proxyGrantingTicket;
if (CommonUtils.isBlank(proxyGrantingTicketIou) || this.proxyGrantingTicketStorage == null) {
proxyGrantingTicket = null;
} else {
proxyGrantingTicket = this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou);
}
final String proxyGrantingTicket = retrieveProxyGrantingTicket(response);
if (CommonUtils.isEmpty(principal)) {
throw new TicketValidationException("No principal was found in the response from the CAS server.");
@ -101,6 +103,7 @@ public class Cas20ServiceTicketValidator extends AbstractCasProtocolUrlBasedTick
final Assertion assertion;
final Map<String, Object> attributes = extractCustomAttributes(response);
if (CommonUtils.isNotBlank(proxyGrantingTicket)) {
attributes.remove(PGT_ATTRIBUTE);
final AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes,
proxyGrantingTicket, this.proxyRetriever);
assertion = new AssertionImpl(attributePrincipal);
@ -113,8 +116,42 @@ public class Cas20ServiceTicketValidator extends AbstractCasProtocolUrlBasedTick
return assertion;
}
protected String parseProxyGrantingTicketFromResponse(final String response) {
return XmlUtils.getTextForElement(response, "proxyGrantingTicket");
protected String retrieveProxyGrantingTicket(final String response) {
final List<String> values = XmlUtils.getTextForElements(response, PGT_ATTRIBUTE);
for (final String value : values) {
if (value != null) {
if (value.startsWith(PGTIOU_PREFIX)) {
return retrieveProxyGrantingTicketFromStorage(value);
} else {
return retrieveProxyGrantingTicketViaEncryption(value);
}
}
}
return null;
}
protected String retrieveProxyGrantingTicketFromStorage(final String pgtIou) {
if (this.proxyGrantingTicketStorage != null) {
return this.proxyGrantingTicketStorage.retrieve(pgtIou);
}
return null;
}
protected String retrieveProxyGrantingTicketViaEncryption(final String encryptedPgt) {
if (this.privateKey != null) {
try {
final Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
final byte[] cred64 = new Base64().decode(encryptedPgt);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
final byte[] cipherData = cipher.doFinal(cred64);
final String pgt = new String(cipherData);
logger.debug("Decrypted PGT: {}", pgt);
return pgt;
} catch (final Exception e) {
logger.error("Unable to decrypt PGT", e);
}
}
return null;
}
protected String parsePrincipalFromResponse(final String response) {
@ -258,4 +295,12 @@ public class Cas20ServiceTicketValidator extends AbstractCasProtocolUrlBasedTick
return this.attributes;
}
}
public PrivateKey getPrivateKey() {
return privateKey;
}
public void setPrivateKey(final PrivateKey privateKey) {
this.privateKey = privateKey;
}
}

View File

@ -20,8 +20,10 @@ package org.jasig.cas.client.validation;
import static org.junit.Assert.*;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.util.List;
import org.jasig.cas.client.PublicTestHttpServer;
import org.jasig.cas.client.authentication.AttributePrincipalImpl;
import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage;
import org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl;
import org.jasig.cas.client.proxy.ProxyRetriever;
@ -37,32 +39,33 @@ import org.junit.Test;
public final class Cas20ServiceTicketValidatorTests extends AbstractTicketValidatorTests {
private static final PublicTestHttpServer server = PublicTestHttpServer.instance(8088);
private static final String USERNAME = "username";
private static final String PGTIOU = "PGTIOU-1-test";
private static final String PGT = "PGT-1-ixcY6jtRXZ4OrJ39SadtLEcTLsGNhE8-NYtvDTK3kk5iAEdatRcnGrGjLckOwK8xU6ocastest";
private static final String ENCRYPTED_PGT = "H3wqFQLBlvhbrPVo4yrwIF9p8yJhCfzHnLHgTWTYVw42sLDJj7c3PBFHKgZfaY9l57qDbKA0fZY979GGFFgnSz1VOOlTgVRi/nmbpwlScRLHP8qUf2JGUyhu0+nTRp6TcQiEqpf5iquXNyQ9UXPyWPdTM/YtgtYtcIOzMovjN5c=";
private Cas20ServiceTicketValidator ticketValidator;
private ProxyGrantingTicketStorage proxyGrantingTicketStorage;
private Field proxyGrantingTicketField;
public Cas20ServiceTicketValidatorTests() {
super();
}
/*@AfterClass
public static void classCleanUp() {
server.shutdown();
} */
@Before
public void setUp() throws Exception {
this.proxyGrantingTicketStorage = getProxyGrantingTicketStorage();
this.proxyGrantingTicketStorage = new ProxyGrantingTicketStorageImpl();
this.proxyGrantingTicketStorage.save(PGTIOU, PGT);
this.ticketValidator = new Cas20ServiceTicketValidator(CONST_CAS_SERVER_URL_PREFIX + "8088");
this.ticketValidator.setProxyCallbackUrl("test");
this.ticketValidator.setProxyGrantingTicketStorage(getProxyGrantingTicketStorage());
this.ticketValidator.setProxyGrantingTicketStorage(this.proxyGrantingTicketStorage);
this.ticketValidator.setProxyRetriever(getProxyRetriever());
this.ticketValidator.setPrivateKey(Cas20ProxyReceivingTicketValidationFilter.buildPrivateKey("src/test/resources/private.pem", "RSA"));
this.ticketValidator.setRenew(true);
}
private ProxyGrantingTicketStorage getProxyGrantingTicketStorage() {
return new ProxyGrantingTicketStorageImpl();
proxyGrantingTicketField = AttributePrincipalImpl.class.getDeclaredField("proxyGrantingTicket");
proxyGrantingTicketField.setAccessible(true);
}
private ProxyRetriever getProxyRetriever() {
@ -90,8 +93,7 @@ public final class Cas20ServiceTicketValidatorTests extends AbstractTicketValida
}
@Test
public void testYesResponseButNoPgt() throws TicketValidationException, UnsupportedEncodingException {
final String USERNAME = "username";
public void testYesResponseButNoPgtiou() throws TicketValidationException, UnsupportedEncodingException {
final String RESPONSE = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationSuccess><cas:user>"
+ USERNAME + "</cas:user></cas:authenticationSuccess></cas:serviceResponse>";
server.content = RESPONSE.getBytes(server.encoding);
@ -102,10 +104,7 @@ public final class Cas20ServiceTicketValidatorTests extends AbstractTicketValida
}
@Test
public void testYesResponseWithPgt() throws TicketValidationException, UnsupportedEncodingException {
final String USERNAME = "username";
final String PGTIOU = "testPgtIou";
final String PGT = "test";
public void testYesResponseWithPgtiou() throws TicketValidationException, UnsupportedEncodingException, IllegalAccessException {
final String RESPONSE = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationSuccess><cas:user>"
+ USERNAME
+ "</cas:user><cas:proxyGrantingTicket>"
@ -113,17 +112,15 @@ public final class Cas20ServiceTicketValidatorTests extends AbstractTicketValida
+ "</cas:proxyGrantingTicket></cas:authenticationSuccess></cas:serviceResponse>";
server.content = RESPONSE.getBytes(server.encoding);
this.proxyGrantingTicketStorage.save(PGTIOU, PGT);
final Assertion assertion = this.ticketValidator.validate("test", "test");
assertEquals(USERNAME, assertion.getPrincipal().getName());
// assertEquals(PGT, assertion.getProxyGrantingTicketId());
final AttributePrincipalImpl principal = (AttributePrincipalImpl) assertion.getPrincipal();
assertEquals(USERNAME, principal.getName());
assertEquals(PGT, proxyGrantingTicketField.get(principal));
}
@Test
public void testGetAttributes() throws TicketValidationException, UnsupportedEncodingException {
final String USERNAME = "username";
final String PGTIOU = "testPgtIou";
public void testGetAttributes() throws TicketValidationException, UnsupportedEncodingException, IllegalAccessException {
final String RESPONSE = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationSuccess><cas:user>"
+ USERNAME
+ "</cas:user><cas:proxyGrantingTicket>"
@ -132,20 +129,53 @@ public final class Cas20ServiceTicketValidatorTests extends AbstractTicketValida
server.content = RESPONSE.getBytes(server.encoding);
final Assertion assertion = this.ticketValidator.validate("test", "test");
assertEquals(USERNAME, assertion.getPrincipal().getName());
assertEquals("test", assertion.getPrincipal().getAttributes().get("password"));
assertEquals("id", assertion.getPrincipal().getAttributes().get("eduPersonId"));
assertEquals("test1\n\ntest", assertion.getPrincipal().getAttributes().get("longAttribute"));
final AttributePrincipalImpl principal = (AttributePrincipalImpl) assertion.getPrincipal();
assertEquals(USERNAME, principal.getName());
assertEquals("test", principal.getAttributes().get("password"));
assertEquals("id", principal.getAttributes().get("eduPersonId"));
assertEquals("test1\n\ntest", principal.getAttributes().get("longAttribute"));
try {
List<?> multivalued = (List<?>) assertion.getPrincipal().getAttributes().get("multivaluedAttribute");
List<?> multivalued = (List<?>) principal.getAttributes().get("multivaluedAttribute");
assertArrayEquals(new String[] { "value1", "value2" }, multivalued.toArray());
} catch (Exception e) {
fail("'multivaluedAttribute' attribute expected as List<Object> object.");
}
//assertEquals(PGT, assertion.getProxyGrantingTicketId());
assertEquals(PGT, proxyGrantingTicketField.get(principal));
}
@Test
public void testYesResponseWithEncryptedPgt() throws TicketValidationException, UnsupportedEncodingException, IllegalAccessException {
final String RESPONSE = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationSuccess><cas:user>"
+ USERNAME
+ "</cas:user><cas:attributes><cas:proxyGrantingTicket>"
+ ENCRYPTED_PGT
+ "</cas:proxyGrantingTicket></cas:attributes></cas:authenticationSuccess></cas:serviceResponse>";
server.content = RESPONSE.getBytes(server.encoding);
final Assertion assertion = this.ticketValidator.validate("test", "test");
final AttributePrincipalImpl principal = (AttributePrincipalImpl) assertion.getPrincipal();
assertEquals(USERNAME, principal.getName());
assertEquals(PGT, proxyGrantingTicketField.get(principal));
}
@Test
public void testYesResponseWithPgtiouAndEncryptedPgt() throws TicketValidationException, UnsupportedEncodingException, IllegalAccessException {
final String RESPONSE = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationSuccess><cas:user>"
+ USERNAME
+ "</cas:user><cas:proxyGrantingTicket>"
+ PGTIOU
+ "</cas:proxyGrantingTicket><cas:attributes><cas:proxyGrantingTicket>"
+ ENCRYPTED_PGT
+ "</cas:proxyGrantingTicket></cas:attributes></cas:authenticationSuccess></cas:serviceResponse>";
server.content = RESPONSE.getBytes(server.encoding);
final Assertion assertion = this.ticketValidator.validate("test", "test");
final AttributePrincipalImpl principal = (AttributePrincipalImpl) assertion.getPrincipal();
assertEquals(USERNAME, principal.getName());
assertEquals(PGT, proxyGrantingTicketField.get(principal));
}
@Test
public void testInvalidResponse() throws Exception {

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDcup6Yn4+K33A7jydo5YWuDYboBla65o4bikkSMxCvXWAvDwL/
Qy69i4q7z/nZo/O2XgVQlfKFciXSSl4qfWj0R5XILCz/g8bjwG3r9tMJUFGhGlsm
SCTbZ4+GcjnxREE8tu3x7LBhmIHZ9/FuStwnuGIBXj4iNs9K/mmYPPNAfwIDAQAB
AoGBALs2xgGphDRDo4vAtapw0lt4Oa5egf1wQ6P0PFnlWgeDaWtAjkg3kVNPIdJ+
aepA9xr80AEzzUmGMbIVRZ1AVV0YB2MBNl1FmFwjJLsZ7E9JbD84QQWUAZfmdp4h
desvzPJA+FrVNuYxVJ8sO8pmOv2toueqeJYMOlSjFsQfR80RAkEA+bWxETU8RR2y
bBW4sIJjRXb/SrztpRqjSCnqcFYa38QraYcWfk/TGU/u8U5j39xI2UM8pNSr8j+l
i5ZC+SYzkwJBAOJKCgEuGInS8B5DVhuDswf0aRsn7P3mi68kAZqUaI/7Z55aopek
usg02qK9Wn0aESRdjbNu0gOta9rpetYLKuUCQQDIE/O3RP9wpbXTcqgUDbU68Hjn
Sm/jjW9tH+Cvd956krTyDgJQ3ObY7joW8OeHc/qO0pfhvmGzbZnYOWKaPSivAkA+
sX6WFxxLSvqll8hKdTFrucZI9MXPDkmS62naVtWlVmS91aSIWOY6w5HzVny0fj1T
kuvIU6KxzCE+lEMo/A0VAkEAtonSABWxd4CA/QOWbi9ZKpPUM1T5o/yATheS8DTk
ZKFA2XDXB5Aj1lCjY/5QaOhfyE2AnvlukCRPzO5FWHabKQ==
-----END RSA PRIVATE KEY-----

12
pom.xml
View File

@ -238,6 +238,18 @@
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.61</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>