SharePoint forms authentication support
Code Review : https://codereview.appspot.com/12523046/
diff --git a/Authentication.wsdl b/Authentication.wsdl
new file mode 100644
index 0000000..496d7d3
--- /dev/null
+++ b/Authentication.wsdl
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://schemas.microsoft.com/sharepoint/soap/" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" targetNamespace="http://schemas.microsoft.com/sharepoint/soap/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
+  <wsdl:types>
+    <s:schema elementFormDefault="qualified" targetNamespace="http://schemas.microsoft.com/sharepoint/soap/">
+      <s:element name="Login">
+        <s:complexType>
+          <s:sequence>
+            <s:element minOccurs="0" name="username" type="s:string" />
+            <s:element minOccurs="0" name="password" type="s:string" />
+          </s:sequence>
+        </s:complexType>
+      </s:element>
+      <s:element name="LoginResponse">
+        <s:complexType>
+          <s:sequence>
+            <s:element name="LoginResult" type="tns:LoginResult" />
+          </s:sequence>
+        </s:complexType>
+      </s:element>
+      <s:complexType name="LoginResult">
+        <s:sequence>
+          <s:element minOccurs="0" name="CookieName" type="s:string" />
+          <s:element name="ErrorCode" type="tns:LoginErrorCode" />
+          <s:element minOccurs="0" maxOccurs="1" name="TimeoutSeconds" type="s:int" />
+        </s:sequence>
+      </s:complexType>
+      <s:simpleType name="LoginErrorCode">
+        <s:restriction base="s:string">
+          <s:enumeration value="NoError" />
+          <s:enumeration value="NotInFormsAuthenticationMode" />
+          <s:enumeration value="PasswordNotMatch" />
+        </s:restriction>
+      </s:simpleType>
+      <s:element name="Mode">
+        <s:complexType />
+      </s:element>
+      <s:element name="ModeResponse">
+        <s:complexType>
+          <s:sequence>
+            <s:element name="ModeResult" type="tns:AuthenticationMode" />
+          </s:sequence>
+        </s:complexType>
+      </s:element>
+      <s:simpleType name="AuthenticationMode">
+        <s:restriction base="s:string">
+          <s:enumeration value="None" />
+          <s:enumeration value="Windows" />
+          <s:enumeration value="Passport" />
+          <s:enumeration value="Forms" />
+        </s:restriction>
+      </s:simpleType>
+    </s:schema>
+  </wsdl:types>
+  <wsdl:message name="LoginSoapIn">
+    <wsdl:part name="parameters" element="tns:Login" />
+  </wsdl:message>
+  <wsdl:message name="LoginSoapOut">
+    <wsdl:part name="parameters" element="tns:LoginResponse" />
+  </wsdl:message>
+  <wsdl:message name="ModeSoapIn">
+    <wsdl:part name="parameters" element="tns:Mode" />
+  </wsdl:message>
+  <wsdl:message name="ModeSoapOut">
+    <wsdl:part name="parameters" element="tns:ModeResponse" />
+  </wsdl:message>
+  <wsdl:portType name="AuthenticationSoap">
+    <wsdl:operation name="Login">
+      <wsdl:input message="tns:LoginSoapIn" />
+      <wsdl:output message="tns:LoginSoapOut" />
+    </wsdl:operation>
+    <wsdl:operation name="Mode">
+      <wsdl:input message="tns:ModeSoapIn" />
+      <wsdl:output message="tns:ModeSoapOut" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="AuthenticationSoap" type="tns:AuthenticationSoap">
+    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="Login">
+      <soap:operation soapAction="http://schemas.microsoft.com/sharepoint/soap/Login" style="document" />
+      <wsdl:input>
+        <soap:body use="literal" />
+      </wsdl:input>
+      <wsdl:output>
+        <soap:body use="literal" />
+      </wsdl:output>
+    </wsdl:operation>
+    <wsdl:operation name="Mode">
+      <soap:operation soapAction="http://schemas.microsoft.com/sharepoint/soap/Mode" style="document" />
+      <wsdl:input>
+        <soap:body use="literal" />
+      </wsdl:input>
+      <wsdl:output>
+        <soap:body use="literal" />
+      </wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:binding name="AuthenticationSoap12" type="tns:AuthenticationSoap">
+    <soap12:binding transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="Login">
+      <soap12:operation soapAction="http://schemas.microsoft.com/sharepoint/soap/Login" style="document" />
+      <wsdl:input>
+        <soap12:body use="literal" />
+      </wsdl:input>
+      <wsdl:output>
+        <soap12:body use="literal" />
+      </wsdl:output>
+    </wsdl:operation>
+    <wsdl:operation name="Mode">
+      <soap12:operation soapAction="http://schemas.microsoft.com/sharepoint/soap/Mode" style="document" />
+      <wsdl:input>
+        <soap12:body use="literal" />
+      </wsdl:input>
+      <wsdl:output>
+        <soap12:body use="literal" />
+      </wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+   <wsdl:service name="Authentication">
+    <wsdl:port name="AuthenticationSoap" binding="tns:AuthenticationSoap">
+      <soap:address location="http://entpoint05/_vti_bin/Authentication.asmx" />
+    </wsdl:port>
+    <wsdl:port name="AuthenticationSoap12" binding="tns:AuthenticationSoap12">
+      <soap12:address location="http://entpoint05/_vti_bin/Authentication.asmx" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
\ No newline at end of file
diff --git a/build.xml b/build.xml
index 663aef2..f8281cb 100644
--- a/build.xml
+++ b/build.xml
@@ -154,10 +154,23 @@
       <arg value="UserGroup.wsdl"/>
       <arg value="UserGroup.wsdl"/>
     </exec>
+    <exec executable="wsimport">
+      <arg value="-s"/>
+      <arg value="${generate.dir}"/>
+      <arg value="-d"/>
+      <arg value="${build-generate.dir}"/>
+      <arg value="-p"/>
+      <arg value="com.microsoft.schemas.sharepoint.soap.authentication"/>
+      <arg value="-wsdllocation"/>
+      <arg value="Authentication.wsdl"/>
+      <arg value="Authentication.wsdl"/>
+    </exec>
     <copy file="SiteData.wsdl"
       todir="${build-generate.dir}/com/microsoft/schemas/sharepoint/soap/"/>
     <copy file="UserGroup.wsdl"
       todir="${build-generate.dir}/com/microsoft/schemas/sharepoint/soap/directory/"/>
+    <copy file="Authentication.wsdl"
+      todir="${build-generate.dir}/com/microsoft/schemas/sharepoint/soap/authentication"/>
     <copy file="UserProfileService.wsdl"
       todir="${build-generate.dir}/com/microsoft/webservices/sharepointportalserver/userprofileservice/"/>
     <copy file="UserProfileChangeService.wsdl"
diff --git a/src/com/google/enterprise/adaptor/sharepoint/FormsAuthenticationHandler.java b/src/com/google/enterprise/adaptor/sharepoint/FormsAuthenticationHandler.java
new file mode 100644
index 0000000..6ab0290
--- /dev/null
+++ b/src/com/google/enterprise/adaptor/sharepoint/FormsAuthenticationHandler.java
@@ -0,0 +1,141 @@
+// Copyright 2012 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.microsoft.schemas.sharepoint.soap.authentication.AuthenticationMode;
+import com.microsoft.schemas.sharepoint.soap.authentication.AuthenticationSoap;
+import com.microsoft.schemas.sharepoint.soap.authentication.LoginErrorCode;
+import com.microsoft.schemas.sharepoint.soap.authentication.LoginResult;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.ws.BindingProvider;
+import javax.xml.ws.WebServiceException;
+import javax.xml.ws.handler.MessageContext;
+
+
+/**
+ * Helper class to handle forms authentication.
+ */
+class FormsAuthenticationHandler {
+  /** SharePoint's namespace. */
+  private static final String XMLNS
+      = "http://schemas.microsoft.com/sharepoint/soap/";
+  
+  private static final Logger log
+      = Logger.getLogger(FormsAuthenticationHandler.class.getName());
+  // Default time out for forms authentication with .NET is 30 mins
+  private static final long DEFAULT_COOKIE_TIMEOUT_SECONDS = 30 * 60;
+
+  private final String userName;
+  private final String password;
+  private final ScheduledExecutorService executor;
+  private final Runnable refreshRunnable = new RefreshRunnable();  
+  private final List<String> authenticationCookiesList 
+      = new CopyOnWriteArrayList<String>();
+  private AuthenticationMode authenticationMode;
+  private final AuthenticationSoap authenticationClient;
+  
+  @VisibleForTesting    
+  FormsAuthenticationHandler(String userName, String password,
+      ScheduledExecutorService executor,
+      AuthenticationSoap authenticationClient) {
+    if (userName == null || password == null || executor == null || 
+        authenticationClient == null) {
+      throw new NullPointerException();
+    }
+    this.userName = userName;
+    this.password = password;   
+    this.executor = executor;
+    this.authenticationClient = authenticationClient;
+  }  
+
+  public List<String> getAuthenticationCookies() {
+    return Collections.unmodifiableList(authenticationCookiesList);
+  }
+  
+  public boolean isFormsAuthentication() {
+    return authenticationMode == AuthenticationMode.FORMS;
+  }
+  
+  private void refreshCookies() throws IOException {
+    log.log(Level.FINE, "AuthenticationMode = {0}", authenticationMode);
+    if (authenticationMode != AuthenticationMode.FORMS) {
+      return;
+    }
+    
+    LoginResult result;
+    try {
+      result = authenticationClient.login(userName, password);
+    } catch (WebServiceException ex) {
+      log.log(Level.WARNING,
+          "Possible SP2013 environment with windows authentication", ex);
+      authenticationMode = AuthenticationMode.WINDOWS;
+      return;
+    }
+    log.log(Level.FINE, 
+        "Login Cookie Expiration in = {0}", result.getTimeoutSeconds());
+    if (result.getErrorCode() != LoginErrorCode.NO_ERROR) {
+      throw new IOException("Forms authentication failed with authentication "
+          + "web service with Error Code " + result.getErrorCode());
+    }
+    @SuppressWarnings("unchecked")
+    Map<String, Object> responseHeaders
+        = (Map<String, Object>) ((BindingProvider) authenticationClient)
+        .getResponseContext().get(MessageContext.HTTP_RESPONSE_HEADERS);
+    log.log(Level.FINEST, "Response headers: {0}", responseHeaders);
+    @SuppressWarnings("unchecked")
+    String cookies = ((List<String>) responseHeaders.get("Set-cookie")).get(0);
+    if (authenticationCookiesList.isEmpty()) {
+      authenticationCookiesList.add(cookies);
+    } else {
+      authenticationCookiesList.set(0, cookies);
+    }
+    long cookieTimeOut = (result.getTimeoutSeconds() == null) ? 
+        DEFAULT_COOKIE_TIMEOUT_SECONDS : result.getTimeoutSeconds();
+
+    long rerunAfter = (cookieTimeOut + 1) / 2;
+    executor.schedule(refreshRunnable, rerunAfter, TimeUnit.SECONDS);
+    log.log(Level.FINEST,
+        "Authentication Cookie is {0}", authenticationCookiesList);   
+ }
+ 
+  public void start() throws IOException {
+    authenticationMode = authenticationClient.mode();
+    refreshCookies();
+  }
+
+  private class RefreshRunnable implements Runnable {
+    @Override
+    public void run() {
+      try {
+        refreshCookies();
+      } catch(IOException ex) {
+        log.log(Level.WARNING, 
+            "Error refreshing forms authentication cookies", ex);        
+        executor.schedule(this, 5, TimeUnit.MINUTES);
+      }
+    }
+  }
+}
diff --git a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
index a777510..a280a96 100644
--- a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
+++ b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
@@ -36,6 +36,7 @@
 import com.google.enterprise.adaptor.sharepoint.SiteDataClient.CursorPaginator;
 import com.google.enterprise.adaptor.sharepoint.SiteDataClient.Paginator;
 import com.google.enterprise.adaptor.sharepoint.SiteDataClient.XmlProcessingException;
+import com.microsoft.schemas.sharepoint.soap.authentication.AuthenticationSoap;
 
 import com.microsoft.schemas.sharepoint.soap.ContentDatabase;
 import com.microsoft.schemas.sharepoint.soap.ContentDatabases;
@@ -87,9 +88,11 @@
 import java.util.regex.Pattern;
 
 import javax.xml.namespace.QName;
+import javax.xml.ws.BindingProvider;
 import javax.xml.ws.EndpointReference;
 import javax.xml.ws.Holder;
 import javax.xml.ws.Service;
+import javax.xml.ws.handler.MessageContext;
 import javax.xml.ws.wsaddressing.W3CEndpointReferenceBuilder;
 
 /**
@@ -101,6 +104,10 @@
   private static final Charset CHARSET = Charset.forName("UTF-8");
   private static final String XMLNS_DIRECTORY
       = "http://schemas.microsoft.com/sharepoint/soap/directory/";
+  
+  /** SharePoint's namespace. */
+  private static final String XMLNS
+      = "http://schemas.microsoft.com/sharepoint/soap/";
 
   /**
    * The data element within a self-describing XML blob. See
@@ -225,6 +232,9 @@
   private ExecutorService executor;
   private boolean xmlValidation;
   private long maxIndexableSize;
+  
+  private ScheduledThreadPoolExecutor scheduledExecutor 
+      = new ScheduledThreadPoolExecutor(1);
   /** Authenticator instance that authenticates with SP. */
   /**
    * Cached value of whether we are talking to a SP 2010 server or not. This
@@ -237,6 +247,8 @@
    * held while waiting on I/O.
    */
   private final Object refreshMemberIdMappingLock = new Object();
+  
+  private FormsAuthenticationHandler authenticationHandler;
 
   public SharePointAdaptor() {
     this(new SoapFactoryImpl(), new HttpClientImpl(),
@@ -299,9 +311,15 @@
     ntlmAuthenticator = new NtlmAuthenticator(username, password);
     // Unfortunately, this is a JVM-wide modification.
     Authenticator.setDefault(ntlmAuthenticator);
-
+    URL virtualServerUrl = new URL(virtualServer);
+    ntlmAuthenticator.addPermitForHost(virtualServerUrl);
+    String authenticationEndPoint 
+        =  virtualServer + "/_vti_bin/Authentication.asmx";
+    authenticationHandler = new FormsAuthenticationHandler(username,
+        password, scheduledExecutor,
+        soapFactory.newAuthentication(authenticationEndPoint));
+    authenticationHandler.start();
     executor = executorFactory.call();
-
     try {
       SiteDataClient virtualServerSiteDataClient =
           getSiteAdaptor(virtualServer, virtualServer).getSiteDataClient();
@@ -320,13 +338,18 @@
   @Override
   public void destroy() {
     executor.shutdown();
+    scheduledExecutor.shutdown();
     try {
       executor.awaitTermination(10, TimeUnit.SECONDS);
+      scheduledExecutor.awaitTermination(10, TimeUnit.SECONDS);
     } catch (InterruptedException ex) {
       Thread.currentThread().interrupt();
     }
+    
     executor.shutdownNow();
+    scheduledExecutor.shutdownNow();
     executor = null;
+    scheduledExecutor = null;
     rareModCache = null;
     Authenticator.setDefault(null);
     ntlmAuthenticator = null;
@@ -485,10 +508,15 @@
       ntlmAuthenticator.addPermitForHost(new URL(web));
       String endpoint = web + "/_vti_bin/SiteData.asmx";
       SiteDataSoap siteDataSoap = soapFactory.newSiteData(endpoint);
-
+      
       String endpointUserGroup = site + "/_vti_bin/UserGroup.asmx";
       UserGroupSoap userGroupSoap = soapFactory.newUserGroup(endpointUserGroup);
-
+      // JAX-WS RT 2.1.4 doesn't handle headers correctly and always assumes the
+      // list contains precisely one entry, so we work around it here.
+      if (authenticationHandler.isFormsAuthentication()) {
+        addFormsAuthenticationCookies((BindingProvider) siteDataSoap);
+        addFormsAuthenticationCookies((BindingProvider) userGroupSoap);        
+      }
       siteAdaptor = new SiteAdaptor(site, web, siteDataSoap, userGroupSoap,
           new MemberIdMappingCallable(site),
           new SiteUserIdMappingCallable(site));
@@ -497,6 +525,12 @@
     }
     return siteAdaptor;
   }
+  
+  private void addFormsAuthenticationCookies(BindingProvider port) {
+    port.getRequestContext().put(MessageContext.HTTP_REQUEST_HEADERS,
+        Collections.singletonMap("Cookie", 
+            authenticationHandler.getAuthenticationCookies()));
+  }
 
   static URI spUrlToUri(String url) throws IOException {
     // Because SP is silly, the path of the URI is unencoded, but the rest of
@@ -693,7 +727,9 @@
       List<GroupPrincipal> denyGroups = new ArrayList<GroupPrincipal>();
       for (PolicyUser policyUser : vs.getPolicies().getPolicyUser()) {
         // TODO(ejona): special case NT AUTHORITY\LOCAL SERVICE.
-        String loginName = policyUser.getLoginName();
+        String loginName = decodeClaim(policyUser.getLoginName(),
+            policyUser.getLoginName(), false);
+        log.log(Level.FINER, "Policy User Login Name = {0}", loginName);
         long grant = policyUser.getGrantMask().longValue();
         if ((necessaryPermissionMask & grant) == necessaryPermissionMask) {
           permitUsers.add(new UserPrincipal(loginName));
@@ -1179,7 +1215,8 @@
       log.entering("SiteAdaptor", "getFileDocContent",
           new Object[] {request, response});
       URI displayUrl = docIdToUri(request.getDocId());
-      FileInfo fi = httpClient.issueGetRequest(displayUrl.toURL());
+      FileInfo fi = httpClient.issueGetRequest(displayUrl.toURL(),
+          authenticationHandler.getAuthenticationCookies());
       if (fi == null) {
         response.respondNotFound();
         return;
@@ -1646,7 +1683,7 @@
         return loginName.substring(7);
       // AD Group
       } else if (loginName.startsWith("c:0+.w|")) {
-        return name;
+        return name.startsWith("c:0+.w|") ? name.substring(7) : name;
       } else if (loginName.equals("c:0(.s|true")) {
         return "Everyone";
       } else if (loginName.equals("c:0!.s|windows")) {
@@ -1853,12 +1890,14 @@
      *
      * @return {@code null} if not found, {@code FileInfo} instance otherwise
      */
-    public FileInfo issueGetRequest(URL url) throws IOException;
+    public FileInfo issueGetRequest(URL url, List<String> authenticationCookies)
+        throws IOException;
   }
 
   static class HttpClientImpl implements HttpClient {
     @Override
-    public FileInfo issueGetRequest(URL url) throws IOException {
+    public FileInfo issueGetRequest(URL url, List<String> authenticationCookies)
+        throws IOException {
       // Handle Unicode. Java does not properly encode the GET.
       try {
         url = new URL(url.toURI().toASCIIString());
@@ -1866,6 +1905,10 @@
         throw new IOException(ex);
       }
       HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+     
+      for (String cookie : authenticationCookies) {
+        conn.addRequestProperty("Cookie", cookie);
+      }
       conn.setDoInput(true);
       conn.setDoOutput(false);
       if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
@@ -1900,18 +1943,24 @@
     public SiteDataSoap newSiteData(String endpoint) throws IOException;
 
     public UserGroupSoap newUserGroup(String endpoint);
+    
+    public AuthenticationSoap newAuthentication(String endpoint);
   }
 
   @VisibleForTesting
   static class SoapFactoryImpl implements SoapFactory {
     private final Service siteDataService;
     private final Service userGroupService;
+    private final Service authenticationService;
 
     public SoapFactoryImpl() {
       this.siteDataService = SiteDataClient.createSiteDataService();
       this.userGroupService = Service.create(
           UserGroupSoap.class.getResource("UserGroup.wsdl"),
           new QName(XMLNS_DIRECTORY, "UserGroup"));
+      this.authenticationService = Service.create(
+          AuthenticationSoap.class.getResource("Authentication.wsdl"),
+          new QName(XMLNS, "Authentication"));
     }
 
     @Override
@@ -1927,6 +1976,14 @@
           .address(endpoint).build();
       return userGroupService.getPort(endpointRef, UserGroupSoap.class);
     }
+    
+    @Override
+    public AuthenticationSoap newAuthentication(String endpoint) {
+      EndpointReference endpointRef = new W3CEndpointReferenceBuilder()
+          .address(endpoint).build();
+      return 
+          authenticationService.getPort(endpointRef, AuthenticationSoap.class);
+    }
   }
 
   private static class NtlmAuthenticator extends Authenticator {
diff --git a/src/com/google/enterprise/adaptor/sharepoint/SharePointUserProfileAdaptor.java b/src/com/google/enterprise/adaptor/sharepoint/SharePointUserProfileAdaptor.java
index 620410b..ae73523 100644
--- a/src/com/google/enterprise/adaptor/sharepoint/SharePointUserProfileAdaptor.java
+++ b/src/com/google/enterprise/adaptor/sharepoint/SharePointUserProfileAdaptor.java
@@ -12,6 +12,7 @@
 import com.google.enterprise.adaptor.PollingIncrementalAdaptor;
 import com.google.enterprise.adaptor.Request;
 import com.google.enterprise.adaptor.Response;
+import com.microsoft.schemas.sharepoint.soap.authentication.AuthenticationSoap;
 
 import com.microsoft.webservices.sharepointportalserver.userprofilechangeservice.ArrayOfUserProfileChangeData;
 import com.microsoft.webservices.sharepointportalserver.userprofilechangeservice.UserProfileChangeData;
@@ -53,6 +54,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -60,9 +63,11 @@
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.ws.BindingProvider;
 import javax.xml.ws.EndpointReference;
 import javax.xml.ws.Service;
 import javax.xml.ws.WebServiceException;
+import javax.xml.ws.handler.MessageContext;
 import javax.xml.ws.wsaddressing.W3CEndpointReferenceBuilder;
 
 /**
@@ -75,6 +80,10 @@
   private static final Map<String, String> SP_GSA_PROPERTY_MAPPINGS;
 
   private static final Charset encoding = Charset.forName("UTF-8");
+  
+  /** SharePoint's namespace. */
+  private static final String AUTH_XMLNS
+      = "http://schemas.microsoft.com/sharepoint/soap/";
 
   private static final String XMLNS =
       "http://microsoft.com/webservices/SharePointPortalServer/UserProfileService";
@@ -120,6 +129,8 @@
   private String userProfileChangeToken;
   private boolean setAcl = true;
   private UserProfileServiceClient userProfileServiceClient;
+  private ScheduledThreadPoolExecutor scheduledExecutor 
+      = new ScheduledThreadPoolExecutor(1);
 
   public static void main(String[] args) {
     AbstractAdaptor.main(new SharePointUserProfileAdaptor(), args);
@@ -177,12 +188,19 @@
     ntlmAuthenticator = new NtlmAuthenticator(username, password);
     // Unfortunately, this is a JVM-wide modification.
     Authenticator.setDefault(ntlmAuthenticator);
+    String authenticationEndPoint 
+        =  virtualServer + "/_vti_bin/Authentication.asmx";
+    FormsAuthenticationHandler authenticationHandler 
+        = new FormsAuthenticationHandler(username, password, scheduledExecutor,
+        userProfileServiceFactory.newAuthentication(authenticationEndPoint));
+    authenticationHandler.start();
     log.log(Level.FINEST, "Initializing User profile Service Client for {0}",
         virtualServer + USER_PROFILE_SERVICE_ENDPOINT);
     userProfileServiceClient = new UserProfileServiceClient(
         userProfileServiceFactory.newUserProfileService(
             virtualServer + USER_PROFILE_SERVICE_ENDPOINT,
-            virtualServer + USER_PROFILE_CHANGE_SERVICE_ENDPOINT));
+            virtualServer + USER_PROFILE_CHANGE_SERVICE_ENDPOINT,
+            authenticationHandler.getAuthenticationCookies()));
     userProfileChangeToken =
         userProfileServiceClient.userProfileServiceWS.getCurrentChangeToken();
   }
@@ -190,6 +208,13 @@
   @Override
   public void destroy() {
     Authenticator.setDefault(null);
+    scheduledExecutor.shutdown();
+    try {     
+      scheduledExecutor.awaitTermination(10, TimeUnit.SECONDS);
+    } catch (InterruptedException ex) {
+      Thread.currentThread().interrupt();
+    }
+    scheduledExecutor.shutdownNow();
   }
 
   @Override
@@ -232,13 +257,16 @@
   @VisibleForTesting
   interface UserProfileServiceFactory {
     public UserProfileServiceWS newUserProfileService(String endpoint,
-        String endpointChangeService);
+        String endpointChangeService, List<String> cookies);
+    public AuthenticationSoap newAuthentication(String endpoint);
+    
   }
 
   private static class UserProfileServiceFactoryImpl
       implements UserProfileServiceFactory {
     private final Service userProfileServiceSoap;
     private final Service userProfileChangeServiceSoap;
+    private final Service authenticationService;
 
     public UserProfileServiceFactoryImpl() {
       URL urlUserProfileService =
@@ -246,28 +274,51 @@
       QName qname = new QName(XMLNS, "UserProfileService");
       this.userProfileServiceSoap = Service.create(
           urlUserProfileService, qname);
-
-
       URL urlUserProfileChangeService =
           UserProfileChangeServiceSoap.class.getResource(
               "UserProfileChangeService.wsdl");
       QName qnameChange = new QName(XMLNS_CHANGE, "UserProfileChangeService");
       this.userProfileChangeServiceSoap = Service.create(
           urlUserProfileChangeService, qnameChange);
+      this.authenticationService = Service.create(
+          AuthenticationSoap.class.getResource("Authentication.wsdl"),
+          new QName(AUTH_XMLNS, "Authentication"));
     }
 
     @Override
     public UserProfileServiceWS newUserProfileService(String endpoint,
-        String endpointChangeService) {
+        String endpointChangeService, List<String> cookies) {
       EndpointReference endpointRef = new W3CEndpointReferenceBuilder()
           .address(endpoint).build();
       EndpointReference endpointChangeRef = new W3CEndpointReferenceBuilder()
           .address(endpointChangeService).build();
+      // JAX-WS RT 2.1.4 doesn't handle headers correctly and always assumes the
+      // list contains precisely one entry, so we work around it here.
+      if (!cookies.isEmpty()) {
+        addFormsAuthenticationCookies(
+            (BindingProvider) userProfileServiceSoap, cookies);
+        addFormsAuthenticationCookies(
+            (BindingProvider) userProfileChangeServiceSoap, cookies);
+      }
       return new SharePointUserProfileServiceWS(userProfileServiceSoap.
           getPort(endpointRef, UserProfileServiceSoap.class),
           userProfileChangeServiceSoap.getPort(endpointChangeRef,
               UserProfileChangeServiceSoap.class));
     }
+    
+    private void addFormsAuthenticationCookies(BindingProvider port, 
+        List<String> cookies) {
+      port.getRequestContext().put(MessageContext.HTTP_REQUEST_HEADERS,
+          Collections.singletonMap("Cookie", cookies));
+    }
+
+    @Override
+    public AuthenticationSoap newAuthentication(String endpoint) {
+      EndpointReference endpointRef = new W3CEndpointReferenceBuilder()
+          .address(endpoint).build();
+      return 
+          authenticationService.getPort(endpointRef, AuthenticationSoap.class);      
+    }
   }
 
   @VisibleForTesting
diff --git a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
index b82a63f..eef07b9 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
@@ -31,6 +31,10 @@
 import com.google.enterprise.adaptor.UserPrincipal;
 import com.google.enterprise.adaptor.sharepoint.SharePointAdaptor.SiteUserIdMappingCallable;
 import com.google.enterprise.adaptor.sharepoint.SharePointAdaptor.SoapFactory;
+import com.microsoft.schemas.sharepoint.soap.authentication.AuthenticationMode;
+import com.microsoft.schemas.sharepoint.soap.authentication.AuthenticationSoap;
+import com.microsoft.schemas.sharepoint.soap.authentication.LoginErrorCode;
+import com.microsoft.schemas.sharepoint.soap.authentication.LoginResult;
 
 import com.microsoft.schemas.sharepoint.soap.ObjectType;
 import com.microsoft.schemas.sharepoint.soap.SPContentDatabase;
@@ -91,6 +95,8 @@
  * Test cases for {@link SharePointAdaptor}.
  */
 public class SharePointAdaptorTest {
+  private static final String AUTH_ENDPOINT
+      = "http://localhost:1/_vti_bin/Authentication.asmx";
   private static final String VS_ENDPOINT
       = "http://localhost:1/_vti_bin/SiteData.asmx";
   private static final ContentExchange VS_CONTENT_EXCHANGE
@@ -181,6 +187,7 @@
       };
   private final MockSoapFactory initableSoapFactory
       = MockSoapFactory.blank()
+      .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
       .endpoint(VS_ENDPOINT, MockSiteData.blank()
           .register(VS_CONTENT_EXCHANGE));
 
@@ -315,6 +322,7 @@
   @Test
   public void testGetDocContentWrongServer() throws Exception {
     SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
         .endpoint(VS_ENDPOINT, MockSiteData.blank()
             .register(VS_CONTENT_EXCHANGE)
             .register(new SiteAndWebExchange(
@@ -335,6 +343,7 @@
   public void testGetDocContentWrongPage() throws Exception {
     final String wrongPage = "http://localhost:1/wrongPage";
     SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
         .endpoint(VS_ENDPOINT, MockSiteData.blank()
             .register(VS_CONTENT_EXCHANGE)
             .register(new SiteAndWebExchange(
@@ -355,6 +364,7 @@
   @Test
   public void testGetDocContentVirtualServer() throws Exception {
     SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
         .endpoint(VS_ENDPOINT, MockSiteData.blank()
             .register(VS_CONTENT_EXCHANGE)
             .register(CD_CONTENT_EXCHANGE));
@@ -390,6 +400,7 @@
   @Test
   public void testGetDocContentSiteCollection() throws Exception {
     SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
         .endpoint(VS_ENDPOINT, MockSiteData.blank()
             .register(VS_CONTENT_EXCHANGE)
             .register(SITES_SITECOLLECTION_SAW_EXCHANGE))
@@ -443,6 +454,7 @@
   @Test
   public void testGetDocContentSiteCollectionWithAdGroup() throws Exception {
     SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
         .endpoint(VS_ENDPOINT, MockSiteData.blank()
             .register(VS_CONTENT_EXCHANGE)
             .register(SITES_SITECOLLECTION_SAW_EXCHANGE))
@@ -481,6 +493,7 @@
         + "<permission memberid='13' mask='756052856929' />"
         + "<permission memberid='14' mask='756052856929' /></permissions>";
     SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
         .endpoint(VS_ENDPOINT, MockSiteData.blank()
             .register(VS_CONTENT_EXCHANGE)
             .register(SITES_SITECOLLECTION_SAW_EXCHANGE))
@@ -516,6 +529,7 @@
       throws Exception {
     ReferenceSiteData siteData = new ReferenceSiteData();
     SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
         .endpoint(VS_ENDPOINT, MockSiteData.blank()
             .register(VS_CONTENT_EXCHANGE)
             .register(SITES_SITECOLLECTION_SAW_EXCHANGE))
@@ -682,7 +696,8 @@
     adaptor = new SharePointAdaptor(initableSoapFactory,
         new HttpClient() {
       @Override
-      public FileInfo issueGetRequest(URL url) {
+      public FileInfo issueGetRequest(URL url,
+          List<String> authenticationCookies) {
         assertEquals(
           "http://localhost:1/sites/SiteCollection/Lists/Custom%20List/"
             + "Attachments/2/1046000.pdf",
@@ -1192,6 +1207,7 @@
       }
     };
     SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
         .endpoint(VS_ENDPOINT, countingSiteData);
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedHttpClient(), executorFactory);
@@ -1280,6 +1296,7 @@
       }
     };
     SoapFactory siteDataFactory = MockSoapFactory.blank()
+        .endpoint(AUTH_ENDPOINT, new MockAuthenticationSoap())
         .endpoint(VS_ENDPOINT, countingSiteData);
     adaptor = new SharePointAdaptor(siteDataFactory,
         new UnsupportedHttpClient(), executorFactory);
@@ -1418,7 +1435,8 @@
 
   private static class UnsupportedHttpClient implements HttpClient {
     @Override
-    public FileInfo issueGetRequest(URL url) {
+    public FileInfo issueGetRequest(URL url,
+        List<String> authenticationCookies) {
       throw new UnsupportedOperationException();
     }
   }
@@ -1723,6 +1741,57 @@
       return delegate().getRolesAndPermissionsForSite();
     }
   }
+  
+  private static class MockAuthenticationSoap extends 
+      UnsupportedAuthenticationSoap {
+    @Override
+    public LoginResult login(String string, String string1) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AuthenticationMode mode() {
+      return AuthenticationMode.WINDOWS;
+    }
+  }
+  
+  private static class UnsupportedAuthenticationSoap 
+      extends DelegatingAuthenticationSoap {
+    private final String endpoint;
+
+    public UnsupportedAuthenticationSoap() {
+      this(null);
+    }
+
+    public UnsupportedAuthenticationSoap(String endpoint) {
+      this.endpoint = endpoint;
+    }
+
+    @Override
+    protected AuthenticationSoap delegate() {
+      if (endpoint == null) {
+        throw new UnsupportedOperationException();
+      } else {
+        throw new UnsupportedOperationException("Endpoint: " + endpoint);
+      }
+    }
+  }
+
+
+  private abstract static class DelegatingAuthenticationSoap 
+      implements AuthenticationSoap {
+    protected abstract AuthenticationSoap delegate();
+
+    @Override
+    public LoginResult login(String username, String password) {
+      return delegate().login(username, password);
+    }
+
+    @Override
+    public AuthenticationMode mode() {
+      return delegate().mode();
+    }    
+  }
 
   /**
    * Throw UnsupportedOperationException for all calls.
@@ -1745,28 +1814,38 @@
     private final String expectedEndpoint;
     private final SiteDataSoap siteData;
     private final UserGroupSoap userGroup;
+    private final AuthenticationSoap authentication;
     private final MockSoapFactory chain;
 
     private MockSoapFactory(String expectedEndpoint, SiteDataSoap siteData,
-        UserGroupSoap userGroup, MockSoapFactory chain) {
+        UserGroupSoap userGroup, AuthenticationSoap authentication,
+        MockSoapFactory chain) {
       this.expectedEndpoint = expectedEndpoint;
       this.siteData = siteData;
       this.userGroup = userGroup;
+      // Tests will always use windows authentication.
+      this.authentication = authentication;
       this.chain = chain;
     }
 
     public static MockSoapFactory blank() {
-      return new MockSoapFactory(null, null, null, null);
+      return new MockSoapFactory(null, null, null, null, null);
     }
 
     public MockSoapFactory endpoint(String expectedEndpoint,
         SiteDataSoap siteData) {
-      return new MockSoapFactory(expectedEndpoint, siteData, null, this);
+      return new MockSoapFactory(expectedEndpoint, siteData, null, null, this);
     }
 
     public MockSoapFactory endpoint(String expectedEndpoint,
         UserGroupSoap userGroup) {
-      return new MockSoapFactory(expectedEndpoint, null, userGroup, this);
+      return new MockSoapFactory(expectedEndpoint, null, userGroup, null, this);
+    }
+    
+    public MockSoapFactory endpoint(String expectedEndpoint,
+        AuthenticationSoap authentication) {
+      return new MockSoapFactory(
+          expectedEndpoint, null, null, authentication, this);
     }
 
     @Override
@@ -1779,6 +1858,17 @@
       }
       return chain.newSiteData(endpoint);
     }
+    
+    @Override 
+    public AuthenticationSoap newAuthentication(String endpoint) {
+      if (chain == null) {
+        fail("Could not find endpoint " + endpoint);
+      }
+      if (expectedEndpoint.equals(endpoint) && authentication != null) {
+        return authentication;
+      }
+      return chain.newAuthentication(endpoint);
+    }
 
     @Override
     public UserGroupSoap newUserGroup(String endpoint) {
diff --git a/test/com/google/enterprise/adaptor/sharepoint/SharePointUserProfileAdaptorTest.java b/test/com/google/enterprise/adaptor/sharepoint/SharePointUserProfileAdaptorTest.java
index ce42836..3ae4b78 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/SharePointUserProfileAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/SharePointUserProfileAdaptorTest.java
@@ -1,9 +1,8 @@
 package com.google.enterprise.adaptor.sharepoint;
 
-import com.google.enterprise.adaptor.Acl;
 import static org.junit.Assert.*;
 
-
+import com.google.enterprise.adaptor.Acl;
 import com.google.enterprise.adaptor.Config;
 import com.google.enterprise.adaptor.DocId;
 import com.google.enterprise.adaptor.GroupPrincipal;
@@ -11,6 +10,9 @@
 import com.google.enterprise.adaptor.sharepoint.SharePointUserProfileAdaptor.UserProfileServiceClient;
 import com.google.enterprise.adaptor.sharepoint.SharePointUserProfileAdaptor.UserProfileServiceFactory;
 import com.google.enterprise.adaptor.sharepoint.SharePointUserProfileAdaptor.UserProfileServiceWS;
+import com.microsoft.schemas.sharepoint.soap.authentication.AuthenticationMode;
+import com.microsoft.schemas.sharepoint.soap.authentication.AuthenticationSoap;
+import com.microsoft.schemas.sharepoint.soap.authentication.LoginResult;
 
 import com.microsoft.webservices.sharepointportalserver.userprofilechangeservice.ArrayOfUserProfileChangeData;
 import com.microsoft.webservices.sharepointportalserver.userprofilechangeservice.UserProfileChangeData;
@@ -580,7 +582,7 @@
     }
     @Override
     public UserProfileServiceWS newUserProfileService(String endpoint,
-        String endpointChangeService) {
+        String endpointChangeService, List<String> cookies) {
       return proxy;
     }
 
@@ -594,5 +596,22 @@
     public void addChangeLogForUser(String userName) {
       proxy.addChangeLogForUser(userName);
     }
+
+    @Override
+    public AuthenticationSoap newAuthentication(String endpoint) {
+      return new MockAuthenticationSoap();
+    }
+  }
+  
+  private static class MockAuthenticationSoap implements AuthenticationSoap {
+    @Override
+    public LoginResult login(String string, String string1) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AuthenticationMode mode() {
+      return AuthenticationMode.WINDOWS;
+    }    
   }
 }