SAML validation with XPath instead of OpenSAML.
This commit is contained in:
parent
6f7fe61780
commit
e998985732
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue