diff --git a/cas-client-core/pom.xml b/cas-client-core/pom.xml index b0de44b..795b674 100644 --- a/cas-client-core/pom.xml +++ b/cas-client-core/pom.xml @@ -11,6 +11,18 @@ Jasig CAS Client for Java - Core + + commons-lang + commons-lang + 2.6 + + + + joda-time + joda-time + 2.7 + + xml-security xmlsec @@ -19,20 +31,6 @@ true - - org.opensaml - opensaml - ${opensaml.version} - jar - compile - - - org.slf4j - jcl-over-slf4j - - - - commons-codec commons-codec @@ -99,6 +97,5 @@ 3.1.3.RELEASE - 2.5.1-1 diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/configuration/ConfigurationKeys.java b/cas-client-core/src/main/java/org/jasig/cas/client/configuration/ConfigurationKeys.java index 9418151..b9838cb 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/configuration/ConfigurationKeys.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/configuration/ConfigurationKeys.java @@ -54,7 +54,6 @@ public interface ConfigurationKeys { ConfigurationKey CAS_SERVER_URL_PREFIX = new ConfigurationKey("casServerUrlPrefix", null); ConfigurationKey ENCODING = new ConfigurationKey("encoding", null); ConfigurationKey TOLERANCE = new ConfigurationKey("tolerance", 1000L); - ConfigurationKey DISABLE_XML_SCHEMA_VALIDATION = new ConfigurationKey("disableXmlSchemaValidation", Boolean.FALSE); ConfigurationKey IGNORE_PATTERN = new ConfigurationKey("ignorePattern", null); ConfigurationKey IGNORE_URL_PATTERN_TYPE = new ConfigurationKey("ignoreUrlPatternType", "REGEX"); ConfigurationKey> HOSTNAME_VERIFIER = new ConfigurationKey>("hostnameVerifier", null); diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java index 5f2ca61..e7cc375 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java @@ -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(); } /** diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/util/IOUtils.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/IOUtils.java new file mode 100644 index 0000000..7220465 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/util/IOUtils.java @@ -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 + } + } +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/util/MapNamespaceContext.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/MapNamespaceContext.java new file mode 100644 index 0000000..0dd773c --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/util/MapNamespaceContext.java @@ -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 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(); + 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 namespaceMap) { + this.namespaceMap = namespaceMap; + } + + public String getNamespaceURI(final String prefix) { + return namespaceMap.get(prefix); + } + + public String getPrefix(final String namespaceURI) { + for (final Map.Entry 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(); + } +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/util/XmlUtils.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/XmlUtils.java index f882b04..926ec91 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/util/XmlUtils.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/util/XmlUtils.java @@ -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 features = new HashMap(); + features.put(XMLConstants.FEATURE_SECURE_PROCESSING, true); + features.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + for (final Map.Entry 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. diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java index b5d5c2f..146280d 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java @@ -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. */ diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java index fab0581..59c4d88 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java @@ -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. diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java index 8c7a632..c43e958 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java @@ -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() diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java index 73aede0..78d37a3 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java @@ -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; } } diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java index 007fc5c..506d9a3 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java @@ -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 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 attributes = getAttributesFor(assertion, subject); - final Map personAttributes = new HashMap(); - 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 authenticationAttributes = new HashMap(); - 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 principalAttributes = new HashMap(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 items = new ArrayList(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 statements = assertion.getAuthenticationStatements(); - - if (statements.isEmpty()) { - return null; - } - - return statements.get(0); - } - - private List getAttributesFor(final org.opensaml.saml1.core.Assertion assertion, final Subject subject) { - final List attributes = new ArrayList(); - 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 list = new ArrayList(); - 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 = "" - + "" - + ticket - + ""; + 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(); + } } diff --git a/cas-client-core/src/main/resources/META-INF/cas/samlRequestTemplate.xml b/cas-client-core/src/main/resources/META-INF/cas/samlRequestTemplate.xml new file mode 100644 index 0000000..4247909 --- /dev/null +++ b/cas-client-core/src/main/resources/META-INF/cas/samlRequestTemplate.xml @@ -0,0 +1,8 @@ + + + + + %s + + + diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/util/CommonUtilsTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/util/CommonUtilsTests.java index 8b585b4..479a3f3 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/util/CommonUtilsTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/util/CommonUtilsTests.java @@ -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")); + } } diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java index 417db57..c648232 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java @@ -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()) + "\">" - + "https://example.com/test-client/secure/" + + "https://example.com/test-client/secure/" + + "" + "" - + "testPrincipalurn:oasis:names:tc:SAML:1.0:cm:artifacttestPrincipalurn:oasis:names:tc:SAML:1.0:cm:artifact12345" + + "testPrincipal" + + "urn:oasis:names:tc:SAML:1.0:cm:artifact" + + "" + + "testPrincipal" + + "urn:oasis:names:tc:SAML:1.0:cm:artifact" + + "12345" + "" + "ACTIVE" + "" @@ -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()); }