From 5b39bdf68a5a0e31313ecd2be4d1a66a65b8c6ec Mon Sep 17 00:00:00 2001 From: Jonathan Bauer Date: Fri, 28 Nov 2014 17:03:40 +0100 Subject: initial commit of 'ecp-client-lean' from KIT --- .gitignore | 6 + pom.xml | 31 ++++ .../java/edu/kit/scc/dei/ecplean/ECPAuthState.java | 13 ++ .../dei/ecplean/ECPAuthenticationException.java | 22 +++ .../kit/scc/dei/ecplean/ECPAuthenticationInfo.java | 47 ++++++ .../edu/kit/scc/dei/ecplean/ECPAuthenticator.java | 167 +++++++++++++++++++++ .../kit/scc/dei/ecplean/ECPAuthenticatorBase.java | 150 ++++++++++++++++++ .../java/edu/kit/scc/dei/ecplean/ECPIdPAuth.java | 82 ++++++++++ .../edu/kit/scc/dei/ecplean/NamespaceResolver.java | 39 +++++ 9 files changed, 557 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/edu/kit/scc/dei/ecplean/ECPAuthState.java create mode 100644 src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticationException.java create mode 100644 src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticationInfo.java create mode 100644 src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticator.java create mode 100644 src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticatorBase.java create mode 100644 src/main/java/edu/kit/scc/dei/ecplean/ECPIdPAuth.java create mode 100644 src/main/java/edu/kit/scc/dei/ecplean/NamespaceResolver.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00e038e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.settings +/.project +/.classpath +/target +*~ +*.swp diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2c3d9b8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + edu.kit.scc.dei + ecp-client-lean + 0.0.2-SNAPSHOT + Lean ECP Client + ECP Client w/o OpenSAML Libs + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + UTF-8 + + + + + + + org.apache.httpcomponents + httpclient + 4.1 + + + \ No newline at end of file diff --git a/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthState.java b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthState.java new file mode 100644 index 0000000..486af84 --- /dev/null +++ b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthState.java @@ -0,0 +1,13 @@ +package edu.kit.scc.dei.ecplean; + +public enum ECPAuthState { + + NOT_STARTED, + INITIAL_PAOS_SP, + AUTH_IDP, + RESPONSE_TO_SP, + AUTH_COMPLETED, + AUTH_DENIED, + AUTH_ERROR + +} diff --git a/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticationException.java b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticationException.java new file mode 100644 index 0000000..2fbfe96 --- /dev/null +++ b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticationException.java @@ -0,0 +1,22 @@ +package edu.kit.scc.dei.ecplean; + +public class ECPAuthenticationException extends Exception { + + private static final long serialVersionUID = 1L; + + public ECPAuthenticationException() { + } + + public ECPAuthenticationException(String message) { + super(message); + } + + public ECPAuthenticationException(Throwable cause) { + super(cause); + } + + public ECPAuthenticationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticationInfo.java b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticationInfo.java new file mode 100644 index 0000000..0fc8b90 --- /dev/null +++ b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticationInfo.java @@ -0,0 +1,47 @@ +package edu.kit.scc.dei.ecplean; + +import java.net.URI; + +public class ECPAuthenticationInfo { + + private String username; + private String password; + private URI idpEcpEndpoint; + private URI spUrl; + private ECPAuthState authState; + + public ECPAuthenticationInfo(String username, String password, + URI idpEcpEndpoint, URI spUrl) { + super(); + this.username = username; + this.password = password; + this.idpEcpEndpoint = idpEcpEndpoint; + this.spUrl = spUrl; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public URI getIdpEcpEndpoint() { + return idpEcpEndpoint; + } + + public URI getSpUrl() { + return spUrl; + } + + public ECPAuthState getAuthState() { + return authState; + } + + public void setAuthState(ECPAuthState authState) { + this.authState = authState; + } + + +} diff --git a/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticator.java b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticator.java new file mode 100644 index 0000000..f6d1bb7 --- /dev/null +++ b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticator.java @@ -0,0 +1,167 @@ +package edu.kit.scc.dei.ecplean; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathException; + +import org.apache.http.HttpResponse; +import org.apache.http.ParseException; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +public class ECPAuthenticator extends ECPAuthenticatorBase { + + public ECPAuthenticator(DefaultHttpClient client, String username, String password, + URI idpEcpEndpoint, URI spUrl) { + super(client); + + authInfo = new ECPAuthenticationInfo(username, password, idpEcpEndpoint, spUrl); + authInfo.setAuthState(ECPAuthState.NOT_STARTED); + } + + public ECPAuthenticator(String username, String password, + URI idpEcpEndpoint, URI spUrl) { + this(new DefaultHttpClient(), username, password, idpEcpEndpoint, spUrl); + } + + public void authenticate() throws ECPAuthenticationException { + logger.info("Starting authentication"); + + logger.info("Contacting SP " + authInfo.getSpUrl()); + authInfo.setAuthState(ECPAuthState.INITIAL_PAOS_SP); + setChanged(); + notifyObservers(authInfo); + + logger.info("Sending initial SP Request"); + + HttpGet httpGet = new HttpGet(authInfo.getSpUrl().toString()); + httpGet.setHeader("Accept", "text/html; application/vnd.paos+xml"); + httpGet.setHeader("PAOS", "ver='urn:liberty:paos:2003-08';'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp'"); + + HttpResponse httpResponse; + String responseBody; + try { + httpResponse = client.execute(httpGet); + responseBody = EntityUtils.toString(httpResponse.getEntity()); + } catch (ClientProtocolException e) { + logger.debug("Initial SP Request failed"); + throw new ECPAuthenticationException(e); + } catch (ParseException e) { + logger.debug("Initial SP Request failed"); + throw new ECPAuthenticationException(e); + } catch (IOException e) { + logger.debug("Initial SP Request failed"); + throw new ECPAuthenticationException(e); + } + + Document initResponse; + try { + initResponse = buildDocumentFromString(responseBody); + } catch (IOException e) { + logger.debug("Parsing SP Request failed"); + throw new ECPAuthenticationException(e); + } catch (ParserConfigurationException e) { + logger.debug("Parsing SP Request failed"); + throw new ECPAuthenticationException(e); + } catch (SAXException e) { + logger.debug("Parsing SP Request failed"); + throw new ECPAuthenticationException(e); + } + + String relayState; + try { + relayState = (String) queryDocument(initResponse, "//ecp:RelayState", XPathConstants.STRING); + } catch (XPathException e) { + logger.debug("Could not find relay state in PAOS answer from SP"); + throw new ECPAuthenticationException(e); + } + logger.info("Got relayState: " + relayState); + String responseConsumerUrl; + try { + responseConsumerUrl = (String) queryDocument(initResponse, "/S:Envelope/S:Header/paos:Request/@responseConsumerURL", XPathConstants.STRING); + } catch (XPathException e) { + logger.debug("Could not find response consumer url in PAOS answer from SP"); + throw new ECPAuthenticationException(e); + } + logger.info("Got responseConsumerUrl: " + responseConsumerUrl); + + Node firstChild = initResponse.getDocumentElement().getFirstChild(); + initResponse.getDocumentElement().removeChild(firstChild); + + Document idpResponse = authenticateIdP(initResponse); + + String assertionConsumerUrl; + try { + assertionConsumerUrl = (String) queryDocument(idpResponse, "/S:Envelope/S:Header/ecp:Response/@AssertionConsumerServiceURL", XPathConstants.STRING); + } catch (XPathException e) { + logger.debug("Could not find assertion consumer url in answer from IdP"); + throw new ECPAuthenticationException(e); + } + logger.info("Got assertionConsumerUrl: " + assertionConsumerUrl); + + if (! assertionConsumerUrl.equals(responseConsumerUrl)) { + throw new ECPAuthenticationException("Assertion- and ResponseConsumerURL don't match"); + } + + idpResponse.getDocumentElement().getFirstChild().getFirstChild().setTextContent(relayState); + + logger.info("Sending Assertion to SP"); + HttpPost httpPost = new HttpPost(assertionConsumerUrl); + httpPost.setHeader("Content-Type", "application/vnd.paos+xml"); + try { + httpPost.setEntity(new StringEntity(documentToString(idpResponse))); + httpResponse = client.execute(httpPost); + responseBody = EntityUtils.toString(httpResponse.getEntity()); + } catch (UnsupportedEncodingException e) { + logger.debug("Could not post assertion back to SP"); + throw new ECPAuthenticationException(e); + } catch (TransformerConfigurationException e) { + logger.debug("Could not post assertion back to SP"); + throw new ECPAuthenticationException(e); + } catch (ClientProtocolException e) { + logger.debug("Could not post assertion back to SP"); + throw new ECPAuthenticationException(e); + } catch (ParseException e) { + logger.debug("Could not post assertion back to SP"); + throw new ECPAuthenticationException(e); + } catch (TransformerException e) { + logger.debug("Could not post assertion back to SP"); + throw new ECPAuthenticationException(e); + } catch (IOException e) { + logger.debug("Could not post assertion back to SP"); + throw new ECPAuthenticationException(e); + } + + logger.info("Requesting original URL"); + httpGet = new HttpGet(authInfo.getSpUrl().toString()); + try { + httpResponse = client.execute(httpGet); + responseBody = EntityUtils.toString(httpResponse.getEntity()); + + logger.info(responseBody); + } catch (ClientProtocolException e) { + logger.debug("Could not request original URL"); + throw new ECPAuthenticationException(e); + } catch (ParseException e) { + logger.debug("Could not request original URL"); + throw new ECPAuthenticationException(e); + } catch (IOException e) { + logger.debug("Could not request original URL"); + throw new ECPAuthenticationException(e); + } + + } +} diff --git a/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticatorBase.java b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticatorBase.java new file mode 100644 index 0000000..7e080f6 --- /dev/null +++ b/src/main/java/edu/kit/scc/dei/ecplean/ECPAuthenticatorBase.java @@ -0,0 +1,150 @@ +package edu.kit.scc.dei.ecplean; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.Observable; + +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathException; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.ParseException; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public abstract class ECPAuthenticatorBase extends Observable { + + protected static Log logger = LogFactory.getLog(ECPAuthenticatorBase.class); + protected ECPAuthenticationInfo authInfo; + protected DefaultHttpClient client; + protected DocumentBuilderFactory documentBuilderFactory; + protected XPathFactory xpathFactory; + protected NamespaceResolver namespaceResolver; + protected TransformerFactory transformerFactory; + + public ECPAuthenticatorBase(DefaultHttpClient client) { + this.client = client; + + documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + + xpathFactory = XPathFactory.newInstance(); + namespaceResolver = new NamespaceResolver(); + namespaceResolver.addNamespace("ecp", "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"); + namespaceResolver.addNamespace("S", "http://schemas.xmlsoap.org/soap/envelope/"); + namespaceResolver.addNamespace("paos", "urn:liberty:paos:2003-08"); + + transformerFactory = TransformerFactory.newInstance(); + } + + public ECPAuthenticatorBase() { + this(new DefaultHttpClient()); + } + + protected Document authenticateIdP(Document idpRequest) + throws ECPAuthenticationException { + logger.info("Sending initial IdP Request"); + client.getCredentialsProvider().setCredentials( + new AuthScope(authInfo.getIdpEcpEndpoint().getHost(), authInfo.getIdpEcpEndpoint().getPort()), + new UsernamePasswordCredentials(authInfo.getUsername(), authInfo.getPassword())); + HttpPost httpPost = new HttpPost(authInfo.getIdpEcpEndpoint().toString()); + HttpResponse httpResponse; + + try { + httpPost.setEntity(new StringEntity(documentToString(idpRequest))); + httpResponse = client.execute(httpPost); + + if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { + throw new ECPAuthenticationException("User not authorized"); + } + } catch (UnsupportedEncodingException e) { + logger.debug("Could not submit PAOS request to IdP"); + throw new ECPAuthenticationException(e); + } catch (TransformerConfigurationException e) { + logger.debug("Could not submit PAOS request to IdP"); + throw new ECPAuthenticationException(e); + } catch (ClientProtocolException e) { + logger.debug("Could not submit PAOS request to IdP"); + throw new ECPAuthenticationException(e); + } catch (TransformerException e) { + logger.debug("Could not submit PAOS request to IdP"); + throw new ECPAuthenticationException(e); + } catch (IOException e) { + logger.debug("Could not submit PAOS request to IdP"); + throw new ECPAuthenticationException(e); + } + + String responseBody; + try { + responseBody = EntityUtils.toString(httpResponse.getEntity()); + return buildDocumentFromString(responseBody); + } catch (ParseException e) { + logger.debug("Could not read response from IdP"); + throw new ECPAuthenticationException(e); + } catch (IOException e) { + logger.debug("Could not read response from IdP"); + throw new ECPAuthenticationException(e); + } catch (SAXException e) { + logger.debug("Could not read response from IdP"); + throw new ECPAuthenticationException(e); + } catch (ParserConfigurationException e) { + logger.debug("Could not read response from IdP"); + throw new ECPAuthenticationException(e); + } + } + + protected Document buildDocumentFromString(String input) + throws IOException, ParserConfigurationException, SAXException { + DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder(); + return builder.parse(new InputSource(new StringReader(input))); + } + + protected Object queryDocument(Document xmlDocument, String expression, + QName returnType) throws XPathException { + XPath xpath = xpathFactory.newXPath(); + xpath.setNamespaceContext(namespaceResolver); + XPathExpression xPathExpression = xpath.compile(expression); + return xPathExpression.evaluate(xmlDocument, returnType); + } + + protected String documentToString(Document xmlDocument) + throws TransformerConfigurationException, TransformerException { + Transformer transformer = transformerFactory.newTransformer(); + + StreamResult result = new StreamResult(new StringWriter()); + DOMSource source = new DOMSource(xmlDocument); + transformer.transform(source, result); + + return result.getWriter().toString(); + } + + public DefaultHttpClient getHttpClient() { + return client; + } + +} \ No newline at end of file diff --git a/src/main/java/edu/kit/scc/dei/ecplean/ECPIdPAuth.java b/src/main/java/edu/kit/scc/dei/ecplean/ECPIdPAuth.java new file mode 100644 index 0000000..0eb035b --- /dev/null +++ b/src/main/java/edu/kit/scc/dei/ecplean/ECPIdPAuth.java @@ -0,0 +1,82 @@ +package edu.kit.scc.dei.ecplean; + +import java.io.IOException; +import java.net.URI; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathException; + +import org.apache.http.impl.client.DefaultHttpClient; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +public class ECPIdPAuth extends ECPAuthenticatorBase { + + public ECPIdPAuth(String username, String password, + URI idpEcpEndpoint) { + this(new DefaultHttpClient(), username, password, idpEcpEndpoint); + } + + public ECPIdPAuth(DefaultHttpClient client, String username, String password, + URI idpEcpEndpoint) { + super(client); + + authInfo = new ECPAuthenticationInfo(username, password, idpEcpEndpoint, null); + authInfo.setAuthState(ECPAuthState.NOT_STARTED); + } + + public String authenticate(String paosMessage) throws ECPAuthenticationException { + + Document initResponse; + try { + initResponse = buildDocumentFromString(paosMessage); + } catch (IOException e) { + logger.debug("Parsing SP Request failed"); + throw new ECPAuthenticationException(e); + } catch (ParserConfigurationException e) { + logger.debug("Parsing SP Request failed"); + throw new ECPAuthenticationException(e); + } catch (SAXException e) { + logger.debug("Parsing SP Request failed"); + throw new ECPAuthenticationException(e); + } + + String relayState; + try { + relayState = (String) queryDocument(initResponse, "//ecp:RelayState", XPathConstants.STRING); + } catch (XPathException e) { + logger.debug("Could not find relay state in PAOS answer from SP"); + throw new ECPAuthenticationException(e); + } + logger.info("Got relayState: " + relayState); + String responseConsumerUrl; + try { + responseConsumerUrl = (String) queryDocument(initResponse, "/S:Envelope/S:Header/paos:Request/@responseConsumerURL", XPathConstants.STRING); + } catch (XPathException e) { + logger.debug("Could not find response consumer url in PAOS answer from SP"); + throw new ECPAuthenticationException(e); + } + logger.info("Got responseConsumerUrl: " + responseConsumerUrl); + + Node firstChild = initResponse.getDocumentElement().getFirstChild(); + initResponse.getDocumentElement().removeChild(firstChild); + + Document idpResponse = authenticateIdP(initResponse); + idpResponse.getDocumentElement().getFirstChild().getFirstChild().setTextContent(relayState); + + try { + return documentToString(idpResponse); + } catch (TransformerConfigurationException e) { + logger.debug("documentToString failed"); + throw new ECPAuthenticationException(e); + } catch (TransformerException e) { + logger.debug("documentToString failed"); + throw new ECPAuthenticationException(e); + } + } + +} diff --git a/src/main/java/edu/kit/scc/dei/ecplean/NamespaceResolver.java b/src/main/java/edu/kit/scc/dei/ecplean/NamespaceResolver.java new file mode 100644 index 0000000..455923e --- /dev/null +++ b/src/main/java/edu/kit/scc/dei/ecplean/NamespaceResolver.java @@ -0,0 +1,39 @@ +package edu.kit.scc.dei.ecplean; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.xml.namespace.NamespaceContext; + +public class NamespaceResolver implements NamespaceContext { + + private Map prefixMap; + private Map uriMap; + + public NamespaceResolver() { + prefixMap = new HashMap(); + uriMap = new HashMap(); + } + + public void addNamespace(String prefix, String uri) { + prefixMap.put(prefix, uri); + uriMap.put(uri, prefix); + } + + @Override + public String getNamespaceURI(String prefix) { + return prefixMap.get(prefix); + } + + @Override + public String getPrefix(String namespaceURI) { + return uriMap.get(namespaceURI); + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + return prefixMap.keySet().iterator(); + } + +} -- cgit v1.2.3-55-g7522