// Copyright 2015 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.experimental;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.enterprise.adaptor.Config;
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;
// TODO: Move this class under examples directory once this implementation
// is finalized and validated in field.
/**
* SamlHandshakeManager implementation to support ADFS 2.0 +
* Live authentication to request ADFS authentication token and
* extract authentication cookie from Live authentication.
*/
public class LiveAdfsHandshakeManager implements SamlHandshakeManager {
private static final Logger log
= Logger.getLogger(LiveAdfsHandshakeManager.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
LiveAdfsHandshakeManager(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 LiveAdfsHandshakeManager getInstance(
Map<String, String> config) {
String username = config.get("sharepoint.username");
String password = config.get("sharepoint.password");
String stsendpoint = config.get("sharepoint.sts.endpoint");
String stsrealm = config.get("sharepoint.sts.realm");
String sharePointUrl = config.get("sharepoint.server");
String login = config.containsKey("sharepoint.sts.login")
? config.get("sharepoint.sts.login") : sharePointUrl + DEFAULT_LOGIN;
String trustLocation = config.containsKey("sharepoint.sts.trustLocation")
? config.get("sharepoint.sts.trustLocation")
: sharePointUrl + DEFAULT_TRUST;
return new LiveAdfsHandshakeManager(sharePointUrl, username, password,
stsendpoint, stsrealm, login, trustLocation, new HttpPostClientImpl());
}
@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 = "wctx=MEST=0&LoginOptions=2&wa=wsignin1.0&wp=MBI"
+ "&wreply=" + URLEncoder.encode(login,"UTF-8")
+ "&wresult=" + URLEncoder.encode(token, "UTF-8");
log.log(Level.FINER, "Step 1A: Making HTTP request @ {0} with data {1}",
new Object[] {trustLocation, param});
Map<String, String> requestHeaders = new HashMap<String, String>();
requestHeaders.put("SOAPAction", stsendpoint);
PostResponseInfo postResponse
= httpClient.issuePostRequest(u, requestHeaders, param);
String location = postResponse.getPostResponseHeaderField("Location");
log.log(Level.FINER, "Step 1B: Extracted redirect location {0}", location);
String loginCookie = postResponse.getPostResponseHeaderField("Set-Cookie");
URL defaultUrl = new URL(login);
requestHeaders.clear();
requestHeaders.put("Cookie", loginCookie);
String data = location.substring(location.indexOf("t="));
log.log(Level.FINER, "Step 2A: Making HTTP request @ {0} with data {1}",
new Object[] {login, data});
postResponse = httpClient.issuePostRequest(
defaultUrl, requestHeaders, data);
location = postResponse.getPostResponseHeaderField("Location");
log.log(Level.FINER, "Step 2B: Extracted redirect location {0}", location);
log.log(Level.FINER, "Step 3A: Making HTTP request @ {0}", location);
postResponse = httpClient.issuePostRequest(
new URL(location), requestHeaders, "");
String loginToken = postResponse.getPostContents().substring(
postResponse.getPostContents().indexOf("value=\"")+ 7,
postResponse.getPostContents().lastIndexOf("\""));
log.log(Level.FINER, "Step 3B: Extracted login token {0}", loginToken);
log.log(Level.FINER, "Step 4A: Making HTTP request @ {0} with data {1}",
new Object[] {defaultUrl, "t="+loginToken});
postResponse = httpClient.issuePostRequest(
defaultUrl, requestHeaders, "t="+loginToken);
String cookie = postResponse.getPostResponseHeaderField("Set-Cookie");
log.log(Level.FINER, "Step 4B: Extracted login cookie {0}", 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[>") + "]]>";
}
}