Merge pull request #9 from battags/CASC-184

CASC-184 - upgrade to OpenSAML2, update XML parsing to work with old and new server code.
This commit is contained in:
Scott 2012-07-24 19:15:34 -07:00
commit f40d38c1a3
3 changed files with 135 additions and 78 deletions

View File

@ -24,10 +24,9 @@
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml</artifactId>
<version>1.1</version>
<version>${opensaml.version}</version>
<type>jar</type>
<scope>provided</scope>
<optional>true</optional>
<scope>compile</scope>
</dependency>
<dependency>
@ -90,5 +89,6 @@
<properties>
<spring.version>2.5.6.SEC01</spring.version>
<opensaml.version>2.5.1-1</opensaml.version>
</properties>
</project>

View File

@ -22,7 +22,22 @@ package org.jasig.cas.client.validation;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.authentication.AttributePrincipalImpl;
import org.jasig.cas.client.util.CommonUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import org.opensaml.*;
import org.opensaml.common.IdentifierGenerator;
import org.opensaml.common.impl.SecureRandomIdentifierGenerator;
import org.opensaml.saml1.core.*;
import org.opensaml.ws.soap.soap11.Envelope;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.io.Unmarshaller;
import org.opensaml.xml.io.UnmarshallerFactory;
import org.opensaml.xml.io.UnmarshallingException;
import org.opensaml.xml.parse.BasicParserPool;
import org.opensaml.xml.parse.XMLParserException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.*;
import java.net.HttpURLConnection;
@ -41,11 +56,33 @@ import javax.net.ssl.HttpsURLConnection;
*/
public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator {
static {
try {
// we really only need to do this once, so this is why its here.
DefaultBootstrap.bootstrap();
} catch (final ConfigurationException e) {
throw new RuntimeException(e);
}
}
/** Time tolerance to allow for time drifting. */
private long tolerance = 1000L;
private final BasicParserPool basicParserPool;
private final IdentifierGenerator identifierGenerator;
public Saml11TicketValidator(final String casServerUrlPrefix) {
super(casServerUrlPrefix);
this.basicParserPool = new BasicParserPool();
this.basicParserPool.setNamespaceAware(true);
try {
this.identifierGenerator = new SecureRandomIdentifierGenerator();
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
protected String getUrlSuffix() {
@ -62,9 +99,7 @@ public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator
@Override
protected void setDisableXmlSchemaValidation(final boolean disabled) {
if (disabled) {
// according to our reading of the SAML 1.1 code, this should disable the schema checking. However, there may be a couple
// of error messages that slip through on start up!
XML.parserPool.setDefaultSchemas(null, null);
this.basicParserPool.setSchema(null);
}
}
@ -78,133 +113,120 @@ public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator
protected Assertion parseResponseFromServer(final String response) throws TicketValidationException {
try {
final String removeStartOfSoapBody = response.substring(response.indexOf("<SOAP-ENV:Body>") + 15);
final String removeEndOfSoapBody = removeStartOfSoapBody.substring(0, removeStartOfSoapBody.indexOf("</SOAP-ENV:Body>"));
final SAMLResponse samlResponse = new SAMLResponse(new ByteArrayInputStream(getBytes(removeEndOfSoapBody)));
if (!samlResponse.getAssertions().hasNext()) {
final Document responseDocument = this.basicParserPool.parse(new ByteArrayInputStream(getBytes(response)));
final Element responseRoot = responseDocument.getDocumentElement();
final UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
final Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(responseRoot);
final Envelope envelope = (Envelope) unmarshaller.unmarshall(responseRoot);
final Response samlResponse = (Response) envelope.getBody().getOrderedChildren().get(0);
final List<org.opensaml.saml1.core.Assertion> assertions = samlResponse.getAssertions();
if (assertions.isEmpty()) {
throw new TicketValidationException("No assertions found.");
}
for (final Iterator<?> iter = samlResponse.getAssertions(); iter.hasNext();) {
final SAMLAssertion assertion = (SAMLAssertion) iter.next();
for (final org.opensaml.saml1.core.Assertion assertion : assertions) {
if (!isValidAssertion(assertion)) {
continue;
}
final SAMLAuthenticationStatement authenticationStatement = getSAMLAuthenticationStatement(assertion);
final AuthenticationStatement authenticationStatement = getSAMLAuthenticationStatement(assertion);
if (authenticationStatement == null) {
throw new TicketValidationException("No AuthentiationStatement found in SAML Assertion.");
}
final SAMLSubject subject = authenticationStatement.getSubject();
final Subject subject = authenticationStatement.getSubject();
if (subject == null) {
throw new TicketValidationException("No Subject found in SAML Assertion.");
}
final SAMLAttribute[] attributes = getAttributesFor(assertion, subject);
final List<Attribute> attributes = getAttributesFor(assertion, subject);
final Map<String,Object> personAttributes = new HashMap<String,Object>();
for (final SAMLAttribute samlAttribute : attributes) {
for (final Attribute samlAttribute : attributes) {
final List<?> values = getValuesFrom(samlAttribute);
personAttributes.put(samlAttribute.getName(), values.size() == 1 ? values.get(0) : values);
personAttributes.put(samlAttribute.getAttributeName(), values.size() == 1 ? values.get(0) : values);
}
final AttributePrincipal principal = new AttributePrincipalImpl(subject.getNameIdentifier().getName(), personAttributes);
final AttributePrincipal principal = new AttributePrincipalImpl(subject.getNameIdentifier().getNameIdentifier(), personAttributes);
final Map<String,Object> authenticationAttributes = new HashMap<String,Object>();
authenticationAttributes.put("samlAuthenticationStatement::authMethod", authenticationStatement.getAuthMethod());
authenticationAttributes.put("samlAuthenticationStatement::authMethod", authenticationStatement.getAuthenticationMethod());
return new AssertionImpl(principal, authenticationAttributes);
}
} catch (final SAMLException e) {
} catch (final UnmarshallingException e) {
throw new TicketValidationException(e);
} catch (final XMLParserException e) {
throw new TicketValidationException(e);
}
throw new TicketValidationException("No Assertion found within valid time range. Either there's a replay of the ticket or there's clock drift. Check tolerance range, or server/client synchronization.");
}
private boolean isValidAssertion(final SAMLAssertion assertion) {
final Date notBefore = assertion.getNotBefore();
final Date notOnOrAfter = assertion.getNotOnOrAfter();
private boolean isValidAssertion(final org.opensaml.saml1.core.Assertion assertion) {
final DateTime notBefore = assertion.getConditions().getNotBefore();
final DateTime notOnOrAfter = assertion.getConditions().getNotOnOrAfter();
if (assertion.getNotBefore() == null || assertion.getNotOnOrAfter() == null) {
if (notBefore == null || notOnOrAfter == null) {
log.debug("Assertion has no bounding dates. Will not process.");
return false;
}
final long currentTime = getCurrentTimeInUtc().getTime();
final DateTime currentTime = new DateTime(DateTimeZone.UTC);
final Interval validityRange = new Interval(notBefore.minus(this.tolerance), notOnOrAfter.plus(this.tolerance));
if (currentTime + tolerance < notBefore.getTime()) {
if (validityRange.contains(currentTime)) {
log.debug("Current time is within the interval validity.");
return true;
}
if (currentTime.isBefore(validityRange.getStart())) {
log.debug("skipping assertion that's not yet valid...");
return false;
}
if (notOnOrAfter.getTime() <= currentTime - tolerance) {
log.debug("skipping expired assertion...");
return false;
}
return true;
log.debug("skipping expired assertion...");
return false;
}
private SAMLAuthenticationStatement getSAMLAuthenticationStatement(final SAMLAssertion assertion) {
for (final Iterator<?> iter = assertion.getStatements(); iter.hasNext();) {
final SAMLStatement statement = (SAMLStatement) iter.next();
private AuthenticationStatement getSAMLAuthenticationStatement(final org.opensaml.saml1.core.Assertion assertion) {
final List<AuthenticationStatement> statements = assertion.getAuthenticationStatements();
if (statement instanceof SAMLAuthenticationStatement) {
return (SAMLAuthenticationStatement) statement;
if (statements.isEmpty()) {
return null;
}
return statements.get(0);
}
private List<Attribute> getAttributesFor(final org.opensaml.saml1.core.Assertion assertion, final Subject subject) {
final List<Attribute> attributes = new ArrayList<Attribute>();
for (final AttributeStatement attribute : assertion.getAttributeStatements()) {
if (subject.getNameIdentifier().getNameIdentifier().equals(attribute.getSubject().getNameIdentifier().getNameIdentifier())) {
attributes.addAll(attribute.getAttributes());
}
}
return null;
return attributes;
}
private SAMLAttribute[] getAttributesFor(final SAMLAssertion assertion, final SAMLSubject subject) {
final List<SAMLAttribute> attributes = new ArrayList<SAMLAttribute>();
for (final Iterator<?> iter = assertion.getStatements(); iter.hasNext();) {
final SAMLStatement statement = (SAMLStatement) iter.next();
if (statement instanceof SAMLAttributeStatement) {
final SAMLAttributeStatement attributeStatement = (SAMLAttributeStatement) statement;
// used because SAMLSubject does not implement equals
if (subject.getNameIdentifier().getName().equals(attributeStatement.getSubject().getNameIdentifier().getName())) {
for (final Iterator<?> iter2 = attributeStatement.getAttributes(); iter2.hasNext();)
attributes.add((SAMLAttribute) iter2.next());
}
}
}
return attributes.toArray(new SAMLAttribute[attributes.size()]);
}
private List<?> getValuesFrom(final SAMLAttribute attribute) {
private List<?> getValuesFrom(final Attribute attribute) {
final List<Object> list = new ArrayList<Object>();
for (final Iterator<?> iter = attribute.getValues(); iter.hasNext();) {
list.add(iter.next());
for (final Object o : attribute.getAttributeValues()) {
list.add(o.toString());
}
return list;
}
private Date getCurrentTimeInUtc() {
final Calendar c = Calendar.getInstance();
c.setTimeZone(TimeZone.getTimeZone("UTC"));
return c.getTime();
}
protected String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
String MESSAGE_TO_SEND;
try {
MESSAGE_TO_SEND = "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Header/><SOAP-ENV:Body><samlp:Request xmlns:samlp=\"urn:oasis:names:tc:SAML:1.0:protocol\" MajorVersion=\"1\" MinorVersion=\"1\" RequestID=\"" + SAMLIdentifierFactory.getInstance().getIdentifier() + "\" IssueInstant=\"" + CommonUtils.formatForUtcTime(new Date()) + "\">"
final String MESSAGE_TO_SEND = "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Header/><SOAP-ENV:Body><samlp:Request xmlns:samlp=\"urn:oasis:names:tc:SAML:1.0:protocol\" MajorVersion=\"1\" MinorVersion=\"1\" RequestID=\"" + this.identifierGenerator.generateIdentifier() + "\" IssueInstant=\"" + CommonUtils.formatForUtcTime(new Date()) + "\">"
+ "<samlp:AssertionArtifact>" + ticket
+ "</samlp:AssertionArtifact></samlp:Request></SOAP-ENV:Body></SOAP-ENV:Envelope>";
} catch (final SAMLException e) {
throw new RuntimeException(e);
}
HttpURLConnection conn = null;

View File

@ -21,6 +21,9 @@ package org.jasig.cas.client.validation;
import org.jasig.cas.client.PublicTestHttpServer;
import org.jasig.cas.client.util.CommonUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import org.junit.*;
import java.io.UnsupportedEncodingException;
@ -52,7 +55,7 @@ public final class Saml11TicketValidatorTests extends AbstractTicketValidatorTes
}*/
@Test
public void testValidationFailedResponse() throws UnsupportedEncodingException {
public void testCompatibilityValidationFailedResponse() throws UnsupportedEncodingException {
final String RESPONSE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope\n" +
" xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Header/><SOAP-ENV:Body><Response\n" +
" xmlns=\"urn:oasis:names:tc:SAML:1.0:protocol\"\n" +
@ -75,11 +78,10 @@ public final class Saml11TicketValidatorTests extends AbstractTicketValidatorTes
}
@Test
public void testValidationSuccessWithNoAttributes() throws UnsupportedEncodingException {
public void testCompatibilityValidationSuccessWithNoAttributes() throws UnsupportedEncodingException {
final Interval range = currentTimeRangeInterval();
final Date now = new Date();
final Date before = new Date(now.getTime() - 5000);
final Date after = new Date(now.getTime() + 200000000);
final String RESPONSE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Header/><SOAP-ENV:Body><Response xmlns=\"urn:oasis:names:tc:SAML:1.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:samlp=\"urn:oasis:names:tc:SAML:1.0:protocol\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" IssueInstant=\"" + CommonUtils.formatForUtcTime(now) + "\" MajorVersion=\"1\" MinorVersion=\"1\" Recipient=\"test\" ResponseID=\"_e1e2124c08ab456eab0bbab3e1c0c433\"><Status><StatusCode Value=\"samlp:Success\"></StatusCode></Status><Assertion xmlns=\"urn:oasis:names:tc:SAML:1.0:assertion\" AssertionID=\"_d2fd0d6e4da6a6d7d2ba5274ab570d5c\" IssueInstant=\"" + CommonUtils.formatForUtcTime(now) + "\" Issuer=\"testIssuer\" MajorVersion=\"1\" MinorVersion=\"1\"><Conditions NotBefore=\"" + CommonUtils.formatForUtcTime(before) + "\" NotOnOrAfter=\"" + CommonUtils.formatForUtcTime(after) + "\"><AudienceRestrictionCondition><Audience>test</Audience></AudienceRestrictionCondition></Conditions><AuthenticationStatement AuthenticationInstant=\"2008-06-19T14:34:44.426Z\" AuthenticationMethod=\"urn:ietf:rfc:2246\"><Subject><NameIdentifier>testPrincipal</NameIdentifier><SubjectConfirmation><ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:artifact</ConfirmationMethod></SubjectConfirmation></Subject></AuthenticationStatement></Assertion></Response></SOAP-ENV:Body></SOAP-ENV:Envelope>";
final String RESPONSE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Header/><SOAP-ENV:Body><Response xmlns=\"urn:oasis:names:tc:SAML:1.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:1.0:assertion\" xmlns:samlp=\"urn:oasis:names:tc:SAML:1.0:protocol\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" IssueInstant=\"" + CommonUtils.formatForUtcTime(now) + "\" MajorVersion=\"1\" MinorVersion=\"1\" Recipient=\"test\" ResponseID=\"_e1e2124c08ab456eab0bbab3e1c0c433\"><Status><StatusCode Value=\"samlp:Success\"></StatusCode></Status><Assertion xmlns=\"urn:oasis:names:tc:SAML:1.0:assertion\" AssertionID=\"_d2fd0d6e4da6a6d7d2ba5274ab570d5c\" IssueInstant=\"" + CommonUtils.formatForUtcTime(now) + "\" Issuer=\"testIssuer\" MajorVersion=\"1\" MinorVersion=\"1\"><Conditions NotBefore=\"" + CommonUtils.formatForUtcTime(range.getStart().toDate()) + "\" NotOnOrAfter=\"" + CommonUtils.formatForUtcTime(range.getEnd().toDate()) + "\"><AudienceRestrictionCondition><Audience>test</Audience></AudienceRestrictionCondition></Conditions><AuthenticationStatement AuthenticationInstant=\"2008-06-19T14:34:44.426Z\" AuthenticationMethod=\"urn:ietf:rfc:2246\"><Subject><NameIdentifier>testPrincipal</NameIdentifier><SubjectConfirmation><ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:artifact</ConfirmationMethod></SubjectConfirmation></Subject></AuthenticationStatement></Assertion></Response></SOAP-ENV:Body></SOAP-ENV:Envelope>";
server.content = RESPONSE.getBytes(server.encoding);
try {
final Assertion a = this.validator.validate("test", "test");
@ -88,4 +90,37 @@ public final class Saml11TicketValidatorTests extends AbstractTicketValidatorTes
fail(e.toString());
}
}
@Test
public void openSaml2GeneratedResponse() throws UnsupportedEncodingException {
final Interval range = currentTimeRangeInterval();
final Date now = new Date();
final String response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><soap11:Envelope xmlns:soap11=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap11:Body>"
+ "<saml1p:Response xmlns:saml1p=\"urn:oasis:names:tc:SAML:1.0:protocol\" InResponseTo=\"_fd1632b5dfa921623e7ca6f9ab727161\" IssueInstant=\"" + CommonUtils.formatForUtcTime(now) + "\" MajorVersion=\"1\" MinorVersion=\"1\" Recipient=\"https://example.com/test-client/secure/?TARGET=https%3A%2F%2Fexample.com%2Ftest-client%2Fsecure%2F\" ResponseID=\"_436dbb2cca5166af29250f431a07888f\">"
+ "<saml1p:Status><saml1p:StatusCode Value=\"saml1p:Success\"/></saml1p:Status>"
+ "<saml1:Assertion xmlns:saml1=\"urn:oasis:names:tc:SAML:1.0:assertion\" IssueInstant=\"" + CommonUtils.formatForUtcTime(now) + "\" Issuer=\"localhost\" MajorVersion=\"1\" MinorVersion=\"1\">"
+ "<saml1:Conditions NotBefore=\"" + CommonUtils.formatForUtcTime(range.getStart().toDate()) + "\" NotOnOrAfter=\"" + CommonUtils.formatForUtcTime(range.getEnd().toDate()) + "\">"
+ "<saml1:AudienceRestrictionCondition><saml1:Audience>https://example.com/test-client/secure/</saml1:Audience></saml1:AudienceRestrictionCondition></saml1:Conditions>"
+ "<saml1:AuthenticationStatement AuthenticationInstant=\"" + CommonUtils.formatForUtcTime(now) + "\" AuthenticationMethod=\"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\">"
+ "<saml1:Subject><saml1:NameIdentifier>testPrincipal</saml1:NameIdentifier><saml1:SubjectConfirmation><saml1:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:artifact</saml1:ConfirmationMethod></saml1:SubjectConfirmation></saml1:Subject></saml1:AuthenticationStatement><saml1:AttributeStatement><saml1:Subject><saml1:NameIdentifier>testPrincipal</saml1:NameIdentifier><saml1:SubjectConfirmation><saml1:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:artifact</saml1:ConfirmationMethod></saml1:SubjectConfirmation></saml1:Subject><saml1:Attribute AttributeName=\"uid\" AttributeNamespace=\"http://www.ja-sig.org/products/cas/\"><saml1:AttributeValue xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xs:string\">12345</saml1:AttributeValue>"
+ "</saml1:Attribute><saml1:Attribute AttributeName=\"accountState\" AttributeNamespace=\"http://www.ja-sig.org/products/cas/\">"
+ "<saml1:AttributeValue xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xs:string\">ACTIVE</saml1:AttributeValue>"
+ "</saml1:Attribute><saml1:Attribute AttributeName=\"eduPersonAffiliation\" AttributeNamespace=\"http://www.ja-sig.org/products/cas/\">"
+ "<saml1:AttributeValue xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xs:string\">employee</saml1:AttributeValue>"
+ "<saml1:AttributeValue xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xs:string\">staff</saml1:AttributeValue>"
+ "<saml1:AttributeValue xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"xs:string\">student</saml1:AttributeValue></saml1:Attribute></saml1:AttributeStatement></saml1:Assertion></saml1p:Response></soap11:Body></soap11:Envelope>";
server.content = response.getBytes(server.encoding);
try {
final Assertion a = this.validator.validate("test", "test");
assertEquals("testPrincipal", a.getPrincipal().getName());
} catch (final TicketValidationException e) {
fail(e.toString());
}
}
private Interval currentTimeRangeInterval() {
return new Interval(new DateTime(DateTimeZone.UTC).minus(5000), new DateTime(DateTimeZone.UTC).plus(200000000));
}
}