| // Copyright 2014 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| |
| package com.google.enterprise.adaptor.sharepoint; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Strings; |
| import com.google.enterprise.adaptor.sharepoint.SamlAuthenticationHandler.HttpPostClient; |
| import com.google.enterprise.adaptor.sharepoint.SamlAuthenticationHandler.HttpPostClientImpl; |
| import com.google.enterprise.adaptor.sharepoint.SamlAuthenticationHandler.PostResponseInfo; |
| import com.google.enterprise.adaptor.sharepoint.SamlAuthenticationHandler.SamlHandshakeManager; |
| |
| import java.io.IOException; |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.net.URL; |
| import java.net.URLEncoder; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| 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 org.w3c.dom.Document; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * SamlHandshakeManager implementation to support ADFS 2.0 |
| * to request ADFS authentication token and extract authentication cookie. |
| */ |
| public class AdfsHandshakeManager implements SamlHandshakeManager { |
| private static final Logger log |
| = Logger.getLogger(AdfsHandshakeManager.class.getName()); |
| |
| private static final String DEFAULT_LOGIN = "/_layouts/Authenticate.aspx"; |
| private static final String DEFAULT_TRUST = "/_trust"; |
| |
| protected final String login; |
| protected final String username; |
| protected final String password; |
| protected final String sharePointUrl; |
| protected final String stsendpoint; |
| protected final String stsrealm; |
| protected final HttpPostClient httpClient; |
| protected final String trustLocation; |
| private static final String reqXML |
| = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" |
| + "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" " |
| + "xmlns:a=\"http://www.w3.org/2005/08/addressing\" " |
| + "xmlns:u=\"http://docs.oasis-open.org/wss/2004/01/" |
| + "oasis-200401-wss-wssecurity-utility-1.0.xsd\"><s:Header>" |
| + "<a:Action s:mustUnderstand=\"1\">" |
| + "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>" |
| + "<a:ReplyTo><a:Address>" |
| + "http://www.w3.org/2005/08/addressing/anonymous</a:Address>" |
| + "</a:ReplyTo><a:To s:mustUnderstand=\"1\">" |
| + "%s</a:To>" // stsendpont |
| + "<o:Security s:mustUnderstand=\"1\" " |
| + "xmlns:o=\"http://docs.oasis-open.org/wss/2004/01/" |
| + "oasis-200401-wss-wssecurity-secext-1.0.xsd\">" |
| + "<o:UsernameToken><o:Username>%s</o:Username>" //username |
| + "<o:Password>%s</o:Password></o:UsernameToken>" //password |
| + "</o:Security></s:Header><s:Body>" |
| + "<t:RequestSecurityToken " |
| + "xmlns:t=\"http://schemas.xmlsoap.org/ws/2005/02/trust\">" |
| + "<wsp:AppliesTo xmlns:wsp=\"" |
| + "http://schemas.xmlsoap.org/ws/2004/09/policy\">" |
| + "<a:EndpointReference><a:Address>%s</a:Address>" //stsrealm |
| + "</a:EndpointReference></wsp:AppliesTo>" |
| + "<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey" |
| + "</t:KeyType>" |
| + "<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue" |
| + "</t:RequestType>" |
| + "<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>" |
| + "</t:RequestSecurityToken></s:Body></s:Envelope>"; |
| |
| @VisibleForTesting |
| AdfsHandshakeManager(String sharePointUrl, String username, |
| String password, String stsendpoint, String stsrealm, String login, |
| String trustLocation, HttpPostClient httpClient) { |
| this.sharePointUrl = sharePointUrl; |
| this.username = username; |
| this.password = password; |
| this.stsendpoint = stsendpoint; |
| this.stsrealm = stsrealm; |
| this.login = login; |
| this.trustLocation = trustLocation; |
| this.httpClient = httpClient; |
| } |
| |
| public static class Builder { |
| private final String username; |
| private final String password; |
| private final String sharePointUrl; |
| private final String stsendpoint; |
| private final String stsrealm; |
| private final HttpPostClient httpClient; |
| private String login; |
| private String trustLocation; |
| |
| public Builder(String sharePointUrl, String username, |
| String password, String stsendpoint, String stsrealm) { |
| this(sharePointUrl, username, password, stsendpoint, stsrealm, |
| new HttpPostClientImpl()); |
| } |
| |
| @VisibleForTesting |
| Builder(String sharePointUrl, String username, |
| String password, String stsendpoint, String stsrealm, |
| HttpPostClient httpClient) { |
| if (sharePointUrl == null || username == null || password == null |
| || stsendpoint == null || httpClient == null || stsrealm == null) { |
| throw new NullPointerException(); |
| } |
| this.sharePointUrl = sharePointUrl; |
| this.username = username; |
| this.password = password; |
| this.stsendpoint = stsendpoint; |
| this.stsrealm = stsrealm; |
| this.httpClient = httpClient; |
| this.login = sharePointUrl + DEFAULT_LOGIN; |
| this.trustLocation = sharePointUrl + DEFAULT_TRUST; |
| } |
| |
| public Builder setLoginUrl(String login) { |
| this.login = login; |
| return this; |
| } |
| |
| public Builder setTrustLocation(String trustLocation) { |
| this.trustLocation = trustLocation; |
| return this; |
| } |
| |
| public AdfsHandshakeManager build() { |
| if (Strings.isNullOrEmpty(trustLocation) |
| || Strings.isNullOrEmpty(login)) { |
| throw new NullPointerException(); |
| } |
| return new AdfsHandshakeManager(sharePointUrl, username, |
| password, stsendpoint, stsrealm, login, trustLocation, httpClient); |
| } |
| } |
| |
| @Override |
| public String requestToken() throws IOException { |
| String saml = generateSamlRequest(); |
| URL u = new URL(stsendpoint); |
| Map<String, String> requestHeaders = new HashMap<String, String>(); |
| requestHeaders.put("SOAPAction", stsendpoint); |
| requestHeaders.put("Content-Type", |
| "application/soap+xml; charset=utf-8"); |
| PostResponseInfo postResponse |
| = httpClient.issuePostRequest(u, requestHeaders, saml); |
| String result = postResponse.getPostContents(); |
| return extractToken(result); |
| } |
| |
| @Override |
| public String getAuthenticationCookie(String token) throws IOException { |
| URL u = new URL(trustLocation); |
| String param = "wa=wsignin1.0" |
| + "&wctx=" + URLEncoder.encode(login,"UTF-8") |
| + "&wresult=" + URLEncoder.encode(token, "UTF-8"); |
| |
| Map<String, String> requestHeaders = new HashMap<String, String>(); |
| requestHeaders.put("SOAPAction", stsendpoint); |
| PostResponseInfo postResponse |
| = httpClient.issuePostRequest(u, requestHeaders, param); |
| String cookie = postResponse.getPostResponseHeaderField("Set-Cookie"); |
| return cookie; |
| } |
| |
| private String generateSamlRequest() { |
| return String.format(reqXML, escapeCdata(stsendpoint), |
| escapeCdata(username), escapeCdata(password), escapeCdata(stsrealm)); |
| } |
| |
| @VisibleForTesting |
| String extractToken(String tokenResponse) throws IOException { |
| if (tokenResponse == null) { |
| throw new IOException("tokenResponse is null"); |
| } |
| try { |
| DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
| dbf.setNamespaceAware(true); |
| DocumentBuilder db = dbf.newDocumentBuilder(); |
| Document document |
| = db.parse(new InputSource(new StringReader(tokenResponse))); |
| NodeList nodes |
| = document.getElementsByTagNameNS( |
| "http://schemas.xmlsoap.org/ws/2005/02/trust", |
| "RequestSecurityTokenResponse"); |
| if (nodes.getLength() == 0) { |
| log.log(Level.WARNING, |
| "ADFS token not available in response {0}", tokenResponse); |
| throw new IOException("ADFS token not available in response"); |
| } |
| Node responseToken = nodes.item(0); |
| String token = getOuterXml(responseToken); |
| log.log(Level.FINER, "ADFS Authentication Token {0}", token); |
| return token; |
| } catch (ParserConfigurationException ex) { |
| throw new IOException("Error parsing result", ex); |
| } catch (SAXException ex) { |
| throw new IOException("Error parsing result", ex); |
| } |
| } |
| |
| private String getOuterXml(Node node) throws IOException { |
| try { |
| Transformer transformer |
| = TransformerFactory.newInstance().newTransformer(); |
| transformer.setOutputProperty("omit-xml-declaration", "yes"); |
| StringWriter writer = new StringWriter(); |
| transformer.transform(new DOMSource(node), new StreamResult(writer)); |
| return writer.toString(); |
| } catch (TransformerConfigurationException ex) { |
| throw new IOException(ex); |
| } catch (TransformerException ex) { |
| throw new IOException(ex); |
| } |
| } |
| |
| @VisibleForTesting |
| String escapeCdata(String input) { |
| if (Strings.isNullOrEmpty(input)) { |
| return ""; |
| } |
| return "<![CDATA[" + input.replace("]]>", "]]]]><![CDATA[>") + "]]>"; |
| } |
| } |