SAML validation with XPath instead of OpenSAML.

This commit is contained in:
Marvin S. Addison 2015-02-20 09:18:05 -05:00
parent 0e78ca914c
commit cc46504126
14 changed files with 417 additions and 216 deletions

View File

@ -11,6 +11,18 @@
<name>Jasig CAS Client for Java - Core</name>
<dependencies>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>xml-security</groupId>
<artifactId>xmlsec</artifactId>
@ -19,20 +31,6 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml</artifactId>
<version>${opensaml.version}</version>
<type>jar</type>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
@ -99,6 +97,5 @@
<properties>
<spring.version>3.1.3.RELEASE</spring.version>
<opensaml.version>2.5.1-1</opensaml.version>
</properties>
</project>

View File

@ -54,7 +54,6 @@ 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<Boolean> DISABLE_XML_SCHEMA_VALIDATION = new ConfigurationKey<Boolean>("disableXmlSchemaValidation", Boolean.FALSE);
ConfigurationKey<String> IGNORE_PATTERN = new ConfigurationKey<String>("ignorePattern", null);
ConfigurationKey<String> IGNORE_URL_PATTERN_TYPE = new ConfigurationKey<String>("ignoreUrlPatternType", "REGEX");
ConfigurationKey<Class<? extends HostnameVerifier>> HOSTNAME_VERIFIER = new ConfigurationKey<Class<? extends HostnameVerifier>>("hostnameVerifier", null);

View File

@ -23,6 +23,7 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
@ -32,6 +33,11 @@ import org.jasig.cas.client.ssl.HttpURLConnectionFactory;
import org.jasig.cas.client.ssl.HttpsURLConnectionFactory;
import org.jasig.cas.client.validation.ProxyList;
import org.jasig.cas.client.validation.ProxyListEditor;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -58,14 +64,22 @@ public final class CommonUtils {
private static final HttpURLConnectionFactory DEFAULT_URL_CONNECTION_FACTORY = new HttpsURLConnectionFactory();
private static final DateTimeFormatter ISO_FORMAT = ISODateTimeFormat.dateTimeNoMillis();
private CommonUtils() {
// nothing to do
}
public static String formatForUtcTime(final Date date) {
final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat.format(date);
return ISO_FORMAT.print(new DateTime(date).withZone(DateTimeZone.UTC));
}
public static Date parseUtcDate(final String date) {
if (isEmpty(date)) {
return null;
}
return ISODateTimeFormat.dateTimeParser().parseDateTime(date).toDate();
}
/**

View File

@ -0,0 +1,74 @@
package org.jasig.cas.client.util;
import java.io.*;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
/**
* IO utility class.
*
* @author Marvin S. Addison
* @since 3.3.1
*/
public class IOUtils {
/** UTF-8 character set. */
public static final Charset UTF8 = Charset.forName("UTF-8");
private IOUtils() { /** Utility class pattern. */ }
/**
* Reads all data from the given stream as UTF-8 character data and closes it on completion or errors.
*
* @param in Input stream containing character data.
*
* @return String of all data in stream.
*
* @throws IOException On IO errors.
*/
public static String readString(final InputStream in) throws IOException {
return readString(in, UTF8);
}
/**
* Reads all data from the given stream as character data in the given character set and closes it on completion
* or errors.
*
* @param in Input stream containing character data.
* @param charset Character set of data in stream.
*
* @return String of all data in stream.
*
* @throws IOException On IO errors.
*/
public static String readString(final InputStream in, final Charset charset) throws IOException {
final Reader reader = new InputStreamReader(in, charset);
final StringBuilder builder = new StringBuilder();
final CharBuffer buffer = CharBuffer.allocate(2048);
try {
while (reader.read(buffer) > -1) {
buffer.flip();
builder.append(buffer);
}
} finally {
closeQuietly(reader);
}
return builder.toString();
}
/**
* Unconditionally close a {@link Closeable} resource. Errors on close are ignored.
*
* @param resource Resource to close.
*/
public static void closeQuietly(final Closeable resource) {
try {
if (resource != null) {
resource.close();
}
} catch (final IOException e) {
//ignore
}
}
}

View File

@ -0,0 +1,62 @@
package org.jasig.cas.client.util;
import javax.xml.namespace.NamespaceContext;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Namespace context implementation backed by a map of XML prefixes to namespace URIs.
*
* @author Marvin S. Addison
* @since 3.3.1
*/
public class MapNamespaceContext implements NamespaceContext {
private final Map<String, String> namespaceMap;
/**
* Creates a new instance from an array of namespace delcarations.
*
* @param namespaceDeclarations An array of namespace declarations of the form prefix->uri.
*/
public MapNamespaceContext(final String ... namespaceDeclarations) {
namespaceMap = new HashMap<String, String>();
int index;
String key;
String value;
for (final String decl : namespaceDeclarations) {
index = decl.indexOf('-');
key = decl.substring(0, index);
value = decl.substring(index + 2);
namespaceMap.put(key, value);
}
}
/**
* Creates a new instance from a map.
*
* @param namespaceMap Map of XML namespace prefixes (keys) to URIs (values).
*/
public MapNamespaceContext(final Map<String, String> namespaceMap) {
this.namespaceMap = namespaceMap;
}
public String getNamespaceURI(final String prefix) {
return namespaceMap.get(prefix);
}
public String getPrefix(final String namespaceURI) {
for (final Map.Entry<String, String> entry : namespaceMap.entrySet()) {
if (entry.getValue().equalsIgnoreCase(namespaceURI)) {
return entry.getKey();
}
}
return null;
}
public Iterator getPrefixes(final String namespaceURI) {
return Collections.singleton(getPrefix(namespaceURI)).iterator();
}
}

View File

@ -19,17 +19,24 @@
package org.jasig.cas.client.util;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.xpath.*;
/**
* Common utilities for easily parsing XML without duplicating logic.
@ -45,6 +52,93 @@ public final class XmlUtils {
*/
private final static Logger LOGGER = LoggerFactory.getLogger(XmlUtils.class);
/**
* Creates a new namespace-aware DOM document object by parsing the given XML.
*
* @param xml XML content.
*
* @return DOM document.
*/
public static Document newDocument(final String xml) {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
final Map<String, Boolean> features = new HashMap<String, Boolean>();
features.put(XMLConstants.FEATURE_SECURE_PROCESSING, true);
features.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
for (final Map.Entry<String, Boolean> entry : features.entrySet()) {
try {
factory.setFeature(entry.getKey(), entry.getValue());
} catch (ParserConfigurationException e) {
LOGGER.warn("Failed setting XML feature {}: {}", entry.getKey(), e);
}
}
factory.setNamespaceAware(true);
try {
return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
} catch (Exception e) {
throw new RuntimeException("XML parsing error: " + e);
}
}
/**
* Compiles the given XPath expression.
*
* @param expression XPath expression.
* @param nsContext XML namespace context for resolving namespace prefixes in XPath expressions.
*
* @return Compiled XPath expression.
*/
public static XPathExpression compileXPath(final String expression, final NamespaceContext nsContext) {
try {
final XPath xPath = XPathFactory.newInstance().newXPath();
xPath.setNamespaceContext(nsContext);
return xPath.compile(expression);
} catch (XPathExpressionException e) {
throw new IllegalArgumentException("Invalid XPath expression");
}
}
/**
* Evaluates the given XPath expression as a string result.
*
* @param expression XPath expression.
* @param nsContext XML namespace context for resolving namespace prefixes in XPath expressions.
* @param document DOM document on which to evaluate expression.
*
* @return Evaluated XPath expression as a string.
*/
public static String evaluateXPathString(
final String expression, final NamespaceContext nsContext, final Document document) {
try {
return (String) compileXPath(expression, nsContext).evaluate(document, XPathConstants.STRING);
} catch (XPathExpressionException e) {
throw new RuntimeException("XPath evaluation error", e);
}
}
/**
* Evaluates the given XPath expression as a node list result.
*
* @param expression XPath expression.
* @param nsContext XML namespace context for resolving namespace prefixes in XPath expressions.
* @param document DOM document on which to evaluate expression.
*
* @return Evaluated XPath expression as a node list.
*/
public static NodeList evaluateXPathNodeList(
final String expression, final NamespaceContext nsContext, final Document document) {
try {
return (NodeList) compileXPath(expression, nsContext).evaluate(document, XPathConstants.NODESET);
} catch (XPathExpressionException e) {
throw new RuntimeException("XPath evaluation error", e);
}
}
/**
* Get an instance of an XML reader from the XMLReaderFactory.
*
@ -62,6 +156,7 @@ public final class XmlUtils {
}
}
/**
* Retrieve the text for a group of elements. Each text element is an entry
* in a list.

View File

@ -34,10 +34,6 @@ public abstract class AbstractCasProtocolUrlBasedTicketValidator extends Abstrac
super(casServerUrlPrefix);
}
protected final void setDisableXmlSchemaValidation(final boolean disable) {
// nothing to do
}
/**
* Retrieves the response from the server by opening a connection and merely reading the response.
*/

View File

@ -90,13 +90,6 @@ public abstract class AbstractUrlBasedTicketValidator implements TicketValidator
*/
protected abstract String getUrlSuffix();
/**
* Disable XML Schema validation. Note, setting this to true may not be reversable. Defaults to false. Setting it to false
* after setting it to true may not have any affect.
*
* @param disabled whether to disable or not.
*/
protected abstract void setDisableXmlSchemaValidation(boolean disabled);
/**
* Constructs the URL to send the validation request to.

View File

@ -51,7 +51,7 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal
private static final String[] RESERVED_INIT_PARAMS = new String[]{ARTIFACT_PARAMETER_NAME.getName(), SERVER_NAME.getName(), SERVICE.getName(), RENEW.getName(), LOGOUT_PARAMETER_NAME.getName(),
ARTIFACT_PARAMETER_OVER_POST.getName(), EAGERLY_CREATE_SESSIONS.getName(), ENCODE_SERVICE_URL.getName(), SSL_CONFIG_FILE.getName(), ROLE_ATTRIBUTE.getName(), IGNORE_CASE.getName(),
CAS_SERVER_LOGIN_URL.getName(), GATEWAY.getName(), AUTHENTICATION_REDIRECT_STRATEGY_CLASS.getName(), GATEWAY_STORAGE_CLASS.getName(), CAS_SERVER_URL_PREFIX.getName(), ENCODING.getName(),
TOLERANCE.getName(), DISABLE_XML_SCHEMA_VALIDATION.getName(), IGNORE_PATTERN.getName(), IGNORE_URL_PATTERN_TYPE.getName(), HOSTNAME_VERIFIER.getName(), HOSTNAME_VERIFIER_CONFIG.getName(),
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(), FRONT_LOGOUT_PARAMETER_NAME.getName(), RELAY_STATE_PARAMETER_NAME.getName()

View File

@ -47,12 +47,10 @@ public class Saml11TicketValidationFilter extends AbstractTicketValidationFilter
validator.setTolerance(tolerance);
validator.setRenew(getBoolean(ConfigurationKeys.RENEW));
final HttpURLConnectionFactory factory = new HttpsURLConnectionFactory(getHostnameVerifier(),
getSSLConfig());
final HttpURLConnectionFactory factory = new HttpsURLConnectionFactory(getHostnameVerifier(), getSSLConfig());
validator.setURLConnectionFactory(factory);
validator.setEncoding(getString(ConfigurationKeys.ENCODING));
validator.setDisableXmlSchemaValidation(getBoolean(ConfigurationKeys.DISABLE_XML_SCHEMA_VALIDATION));
return validator;
}
}

View File

@ -22,29 +22,22 @@ import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.authentication.AttributePrincipalImpl;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.IOUtils;
import org.jasig.cas.client.util.MapNamespaceContext;
import org.jasig.cas.client.util.XmlUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import org.opensaml.Configuration;
import org.opensaml.DefaultBootstrap;
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.opensaml.xml.schema.XSAny;
import org.opensaml.xml.schema.XSString;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.namespace.NamespaceContext;
/**
* TicketValidator that can understand validating a SAML artifact. This includes the SOAP request/response.
@ -54,34 +47,59 @@ import org.w3c.dom.Element;
*/
public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator {
static {
try {
// Check for prior OpenSAML initialization to prevent double init
// that would overwrite existing OpenSAML configuration
if (Configuration.getParserPool() == null) {
DefaultBootstrap.bootstrap();
}
} catch (final ConfigurationException e) {
throw new RuntimeException(e);
}
}
/** Authentication attribute containing SAML AuthenticationMethod attribute value. */
public static final String AUTH_METHOD_ATTRIBUTE = "samlAuthenticationStatement::authMethod";
/** SAML 1.1 request template. */
private static final String SAML_REQUEST_TEMPLATE;
/** SAML 1.1. namespace context. */
private static final NamespaceContext SAML_NS_CONTEXT = new MapNamespaceContext(
"soap->http://schemas.xmlsoap.org/soap/envelope/",
"sa->urn:oasis:names:tc:SAML:1.0:assertion",
"sp->urn:oasis:names:tc:SAML:1.0:protocol");
/** XPath expression to extract Assertion validity start date. */
private static final String XPATH_ASSERTION_DATE_START = "//sa:Assertion/sa:Conditions/@NotBefore";
/** XPath expression to extract Assertion validity end date. */
private static final String XPATH_ASSERTION_DATE_END = "//sa:Assertion/sa:Conditions/@NotOnOrAfter";
/** XPath expression to extract NameIdentifier. */
private static final String XPATH_NAME_ID = "//sa:AuthenticationStatement/sa:Subject/sa:NameIdentifier";
/** XPath expression to extract authentication method. */
private static final String XPATH_AUTH_METHOD = "//sa:AuthenticationStatement/@AuthenticationMethod";
/** XPath expression to extract attributes. */
private static final String XPATH_ATTRIBUTES = "//sa:AttributeStatement/sa:Attribute";
private static final String HEX_CHARS = "0123456789abcdef";
/** Time tolerance to allow for time drifting. */
private long tolerance = 1000L;
private final BasicParserPool basicParserPool;
private final Random random;
private final IdentifierGenerator identifierGenerator;
/** Class initializer. */
static {
try {
SAML_REQUEST_TEMPLATE = IOUtils.readString(
Saml11TicketValidator.class.getResourceAsStream("/META-INF/cas/samlRequestTemplate.xml"));
} catch (IOException e) {
throw new IllegalStateException("Cannot load SAML request template from classpath", e);
}
}
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);
random = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Cannot find required SHA1PRNG algorithm");
}
}
@ -96,95 +114,62 @@ public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator
urlParameters.put("TARGET", service);
}
@Override
protected void setDisableXmlSchemaValidation(final boolean disabled) {
if (disabled) {
this.basicParserPool.setSchema(null);
}
}
protected byte[] getBytes(final String text) {
try {
return CommonUtils.isNotBlank(getEncoding()) ? text.getBytes(getEncoding()) : text.getBytes();
} catch (final Exception e) {
return text.getBytes();
}
}
protected Assertion parseResponseFromServer(final String response) throws TicketValidationException {
try {
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.");
final Document document = XmlUtils.newDocument(response);
final Date assertionValidityStart = CommonUtils.parseUtcDate(
XmlUtils.evaluateXPathString(XPATH_ASSERTION_DATE_START, SAML_NS_CONTEXT, document));
final Date assertionValidityEnd = CommonUtils.parseUtcDate(
XmlUtils.evaluateXPathString(XPATH_ASSERTION_DATE_END, SAML_NS_CONTEXT, document));
if (!isValidAssertion(assertionValidityStart, assertionValidityEnd)) {
throw new TicketValidationException("Invalid SAML assertion");
}
for (final org.opensaml.saml1.core.Assertion assertion : assertions) {
if (!isValidAssertion(assertion)) {
continue;
}
final AuthenticationStatement authenticationStatement = getSAMLAuthenticationStatement(assertion);
if (authenticationStatement == null) {
throw new TicketValidationException("No AuthentiationStatement found in SAML Assertion.");
}
final Subject subject = authenticationStatement.getSubject();
if (subject == null) {
throw new TicketValidationException("No Subject found in SAML Assertion.");
}
final List<Attribute> attributes = getAttributesFor(assertion, subject);
final Map<String, Object> personAttributes = new HashMap<String, Object>();
for (final Attribute samlAttribute : attributes) {
final List<?> values = getValuesFrom(samlAttribute);
personAttributes.put(samlAttribute.getAttributeName(), values.size() == 1 ? values.get(0) : values);
}
final AttributePrincipal principal = new AttributePrincipalImpl(subject.getNameIdentifier()
.getNameIdentifier(), personAttributes);
final Map<String, Object> authenticationAttributes = new HashMap<String, Object>();
authenticationAttributes.put("samlAuthenticationStatement::authMethod",
authenticationStatement.getAuthenticationMethod());
final DateTime notBefore = assertion.getConditions().getNotBefore();
final DateTime notOnOrAfter = assertion.getConditions().getNotOnOrAfter();
final DateTime authenticationInstant = authenticationStatement.getAuthenticationInstant();
return new AssertionImpl(principal, notBefore.toDate(), notOnOrAfter.toDate(),
authenticationInstant.toDate(), authenticationAttributes);
final String nameId = XmlUtils.evaluateXPathString(XPATH_NAME_ID, SAML_NS_CONTEXT, document);
if (nameId == null) {
throw new TicketValidationException("SAML assertion does not contain NameIdentifier element");
}
} catch (final UnmarshallingException e) {
throw new TicketValidationException(e);
} catch (final XMLParserException e) {
throw new TicketValidationException(e);
final String authMethod = XmlUtils.evaluateXPathString(XPATH_AUTH_METHOD, SAML_NS_CONTEXT, document);
final NodeList attributes = XmlUtils.evaluateXPathNodeList(XPATH_ATTRIBUTES, SAML_NS_CONTEXT, document);
final Map<String, Object> principalAttributes = new HashMap<String, Object>(attributes.getLength());
Element attribute;
NodeList values;
String name;
for (int i = 0; i < attributes.getLength(); i++) {
attribute = (Element) attributes.item(i);
name = attribute.getAttribute("AttributeName");
logger.trace("Processing attribute {}", name);
values = attribute.getElementsByTagNameNS("*", "AttributeValue");
if (values.getLength() == 1) {
principalAttributes.put(name, values.item(0).getTextContent());
} else {
final Collection<Object> items = new ArrayList<Object>(values.getLength());
for (int j = 0; j < values.getLength(); j++) {
items.add(values.item(j).getTextContent());
}
principalAttributes.put(name, items);
}
}
return new AssertionImpl(
new AttributePrincipalImpl(nameId, principalAttributes),
assertionValidityStart,
assertionValidityEnd,
new Date(),
Collections.singletonMap(AUTH_METHOD_ATTRIBUTE, (Object) authMethod));
} catch (final Exception e) {
throw new TicketValidationException("Error processing SAML response", 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 org.opensaml.saml1.core.Assertion assertion) {
final DateTime notBefore = assertion.getConditions().getNotBefore();
final DateTime notOnOrAfter = assertion.getConditions().getNotOnOrAfter();
private boolean isValidAssertion(final Date notBefore, final Date notOnOrAfter) {
if (notBefore == null || notOnOrAfter == null) {
logger.debug("Assertion has no bounding dates. Will not process.");
logger.debug("Assertion is not valid because it does not have bounding dates.");
return false;
}
final DateTime currentTime = new DateTime(DateTimeZone.UTC);
final Interval validityRange = new Interval(notBefore.minus(this.tolerance), notOnOrAfter.plus(this.tolerance));
final Interval validityRange = new Interval(
new DateTime(notBefore).minus(this.tolerance),
new DateTime(notOnOrAfter).plus(this.tolerance));
if (validityRange.contains(currentTime)) {
logger.debug("Current time is within the interval validity.");
@ -192,93 +177,41 @@ public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator
}
if (currentTime.isBefore(validityRange.getStart())) {
logger.debug("skipping assertion that's not yet valid...");
return false;
logger.debug("Assertion is not yet valid");
} else {
logger.debug("Assertion is expired");
}
logger.debug("skipping expired assertion...");
return false;
}
private AuthenticationStatement getSAMLAuthenticationStatement(final org.opensaml.saml1.core.Assertion assertion) {
final List<AuthenticationStatement> statements = assertion.getAuthenticationStatements();
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 attributes;
}
private List<?> getValuesFrom(final Attribute attribute) {
final List<Object> list = new ArrayList<Object>();
for (final Object o : attribute.getAttributeValues()) {
if (o instanceof XSAny) {
list.add(((XSAny) o).getTextContent());
} else if (o instanceof XSString) {
list.add(((XSString) o).getValue());
} else {
list.add(o.toString());
}
}
return list;
}
protected String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
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>";
final String request = String.format(
SAML_REQUEST_TEMPLATE,
generateId(),
CommonUtils.formatForUtcTime(new Date()),
ticket);
HttpURLConnection conn = null;
DataOutputStream out = null;
BufferedReader in = null;
try {
conn = this.getURLConnectionFactory().buildHttpURLConnection(validationUrl.openConnection());
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "text/xml");
conn.setRequestProperty("Content-Length", Integer.toString(MESSAGE_TO_SEND.length()));
conn.setRequestProperty("Content-Type", "text/xml");
conn.setRequestProperty("Content-Length", Integer.toString(request.length()));
conn.setRequestProperty("SOAPAction", "http://www.oasis-open.org/committees/security");
conn.setUseCaches(false);
conn.setDoInput(true);
conn.setDoOutput(true);
out = new DataOutputStream(conn.getOutputStream());
out.writeBytes(MESSAGE_TO_SEND);
out.flush();
in = new BufferedReader(CommonUtils.isNotBlank(getEncoding()) ? new InputStreamReader(
conn.getInputStream(), Charset.forName(getEncoding())) : new InputStreamReader(
conn.getInputStream()));
final StringBuilder buffer = new StringBuilder(256);
final Charset charset = CommonUtils.isNotBlank(getEncoding()) ?
Charset.forName(getEncoding()) : IOUtils.UTF8;
conn.getOutputStream().write(request.getBytes(charset));
conn.getOutputStream().flush();
String line;
while ((line = in.readLine()) != null) {
buffer.append(line);
}
return buffer.toString();
return IOUtils.readString(conn.getInputStream(), charset);
} catch (final IOException e) {
throw new RuntimeException(e);
throw new RuntimeException("IO error sending HTTP request to /samlValidate", e);
} finally {
CommonUtils.closeQuietly(out);
CommonUtils.closeQuietly(in);
if (conn != null) {
conn.disconnect();
}
@ -288,4 +221,16 @@ public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator
public void setTolerance(final long tolerance) {
this.tolerance = tolerance;
}
private String generateId() {
final byte[] data = new byte[16];
random.nextBytes(data);
final StringBuilder id = new StringBuilder(33);
id.append('_');
for (int i = 0; i < data.length; i++) {
id.append(HEX_CHARS.charAt((data[i] & 0xF0) >> 4));
id.append(HEX_CHARS.charAt(data[i] & 0x0F));
}
return id.toString();
}
}

View File

@ -0,0 +1,8 @@
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:oasis:names:tc:SAML:1.0:protocol">
<soap:Header/>
<soap:Body>
<Request MajorVersion="1" MinorVersion="1" RequestID="%s" IssueInstant="%s">
<AssertionArtifact>%s</AssertionArtifact>
</Request>
</soap:Body>
</soap:Envelope>

View File

@ -21,6 +21,8 @@ package org.jasig.cas.client.util;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import junit.framework.TestCase;
import org.jasig.cas.client.PublicTestHttpServer;
import org.jasig.cas.client.ssl.HttpsURLConnectionFactory;
@ -192,4 +194,9 @@ public final class CommonUtilsTests extends TestCase {
public void testUrlEncode() {
assertEquals("this+is+a+very+special+parameter+with+%3D%25%2F", CommonUtils.urlEncode("this is a very special parameter with =%/"));
}
public void testParseUtcDate() {
final Date expected = new Date(1424437961025L);
assertEquals(expected, CommonUtils.parseUtcDate("2015-02-20T08:12:41.025-0500"));
}
}

View File

@ -21,6 +21,7 @@ package org.jasig.cas.client.validation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Date;
import org.jasig.cas.client.PublicTestHttpServer;
import org.jasig.cas.client.util.CommonUtils;
@ -117,11 +118,17 @@ public final class Saml11TicketValidatorTests extends AbstractTicketValidatorTes
+ "\" NotOnOrAfter=\""
+ CommonUtils.formatForUtcTime(range.getEnd().toDate())
+ "\">"
+ "<saml1:AudienceRestrictionCondition><saml1:Audience>https://example.com/test-client/secure/</saml1:Audience></saml1:AudienceRestrictionCondition></saml1:Conditions>"
+ "<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: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/\">"
@ -132,7 +139,13 @@ public final class Saml11TicketValidatorTests extends AbstractTicketValidatorTes
server.content = response.getBytes(server.encoding);
try {
final Assertion a = this.validator.validate("test", "test");
assertEquals(
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
a.getAttributes().get(Saml11TicketValidator.AUTH_METHOD_ATTRIBUTE));
assertEquals("testPrincipal", a.getPrincipal().getName());
assertEquals("12345", a.getPrincipal().getAttributes().get("uid"));
assertEquals("ACTIVE", a.getPrincipal().getAttributes().get("accountState"));
assertEquals(3, ((Collection) a.getPrincipal().getAttributes().get("eduPersonAffiliation")).size());
} catch (final TicketValidationException e) {
fail(e.toString());
}