package edu.kit.scc.dei.ecplean; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; 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.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.auth.BasicScheme; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.w3c.dom.Document; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; public abstract class ECPAuthenticatorBase extends Observable { protected static Logger logger = LogManager.getLogger(ECPAuthenticatorBase.class); protected ECPAuthenticationInfo authInfo; protected CloseableHttpClient client; protected DocumentBuilderFactory documentBuilderFactory; protected XPathFactory xpathFactory; protected NamespaceResolver namespaceResolver; protected TransformerFactory transformerFactory; protected boolean retryWithoutAt; public ECPAuthenticatorBase(CloseableHttpClient client) { this.client = client == null ? HttpClients.createSystem() : 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(null); } private CloseableHttpResponse exec(Document idpRequest, String user, String pass) throws ECPAuthenticationException { final HttpHost httpHost = HttpHost.create(authInfo.getSpUrl()); // setup basic authentication final UsernamePasswordCredentials userCredentials = new UsernamePasswordCredentials(user, pass.toCharArray()); final BasicScheme basicAuth = new BasicScheme(); basicAuth.initPreemptive(userCredentials); // create local HTTP context for basic authentication final HttpClientContext httpContext = HttpClientContext.create(); httpContext.resetAuthExchange(httpHost, basicAuth); // create POST request to IdP final HttpPost httpPost = new HttpPost(authInfo.getIdpEcpEndpoint().toString()); // fill content of POST request try { httpPost.setEntity(new StringEntity(documentToString(idpRequest))); } catch (TransformerException e1) { logger.warn("Error setting XML payload of IdP POST"); throw new ECPAuthenticationException(e1); } // set content type of POST request httpPost.setHeader(HttpHeaders.CONTENT_TYPE, "text/xml; charset=utf-8"); // set basic authentication header for POST request try { httpPost.setHeader(HttpHeaders.AUTHORIZATION, basicAuth.generateAuthResponse(httpHost, httpPost, httpContext)); } catch (AuthenticationException e) { logger.warn("Error setting Authentication header for IdP POST"); throw new ECPAuthenticationException(e); } // send POST request to IdP try { return client.execute(httpPost, httpContext); } catch (Exception e) { httpPost.reset(); logger.error("Could not submit PAOS request to IdP"); throw new ECPAuthenticationException(e); } } protected Document authenticateIdP(Document idpRequest) throws ECPAuthenticationException { logger.info("Sending initial IdP Request"); CloseableHttpResponse httpResponse = null; String user = authInfo.getUsername(); String pass = authInfo.getPassword(); int at = user.lastIndexOf('@'); boolean failed = false; try { httpResponse = exec(idpRequest, user, pass); failed = (httpResponse.getCode() == HttpStatus.SC_UNAUTHORIZED); } catch (ECPAuthenticationException e) { logger.debug("Could not submit PAOS request to IdP"); if (at == -1) throw new ECPAuthenticationException(e); failed = true; } if (at != -1 && failed && retryWithoutAt) { // Retrying without the @ in the username is desired user = user.substring(0, at); try { httpResponse = exec(idpRequest, user, pass); } catch (ECPAuthenticationException e) { logger.debug("Could not submit PAOS request to IdP"); throw new ECPAuthenticationException(e); } } String responseBody; try { responseBody = EntityUtils.toString(httpResponse.getEntity()); } catch (RuntimeException | IOException | ParseException e) { logger.debug("Could not read response from IdP"); throw new ECPAuthenticationException(e); } try { return buildDocumentFromString(responseBody); } catch (IOException | SAXException | ParserConfigurationException | RuntimeException e) { logger.debug("Could not parse XML response from IdP:\n" + responseBody); throw new ECPAuthenticationException(e); } } protected Document buildDocumentFromString(String input) throws IOException, ParserConfigurationException, SAXException { DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder(); builder.setEntityResolver(new EntityResolver() { @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { return new InputSource(new StringReader("")); } }); 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 CloseableHttpClient getHttpClient() { return client; } public void setRetryWithoutAt(boolean b) { this.retryWithoutAt = b; } }