The solution: Formbased access to sharepoint by WebDav

In the first post of this serie I describe the problem which can be read here. In the second post of this serie I explained how the problem was analyzed using Live HTTP Headers which can be read here.

In this post I want to present you a possible solution how to achieve a succesful login on a sharepoint server which uses form based authorization. The WebDavSession from the ITHit component has unfortunately no public interface to set cookies from the outside (although I’m in contact with the developers there to see if they could provide some kind of access method to inject cookies). Therefore the internal cookie container of the WebDavSession must be extracted using System.Reflection.

The code below shows how the CookieContainer is extracted by using reflection from the WebDavSession reference webDavSession. The flag external mode is automatically set if the client did provide credentials without using the domain part, so we can assume that the client wants to connect to the external zone of the sharepoint.

this.cookieContainerField = this.webDavSession.GetType().GetFields(BindingFlags.NonPublic |  BindingFlags.Instance).
Where(info => info.FieldType  == typeof(CookieContainer)).Single();
this.webDavSessionCookieContainer =  (CookieContainer)this.cookieContainerField.GetValue(this.webDavSession);
if (this.ExternalMode)
{
this.webDavSessionCookieContainer = new  CookieContainer();
this.cookieContainerField.SetValue(this.webDavSession,  this.webDavSessionCookieContainer);
}

Before every call to the webDavClient we must now check if we need to inject a cookie into the cookie container. I usually do this kind of code by using function delegates which also allows me to simplify exception handling which otherwise would occur multiple times in the code. Such a function delegate could for example look like the following:

private TResult InjectBeforeExecute<T, TResult>(Func<T,  TResult> functionToExecute, T arg1)
{
try
{
this.InjectCookie();
return functionToExecute(arg1);
}
catch (XmlException  ex)
{
Log.Error(ex.Message,  ex);
throw new UnauthorizedAccessException(ex.Message,  ex);
}
// more catch and throw if necessary
}

Now comes the heart of the solution: The injection method.

private void InjectCookie()
{
Uri workingDir = new Uri(this.WorkingDirectoryExternal);
if (this.ExternalMode && !this.webDavSessionCookieContainer.GetCookies(workingDir).Cast<Cookie>().Any(cookie => cookie.Domain == workingDir.Host & !cookie.Expired))
{
HttpWebRequest request = WebRequest.Create(this.WorkingDirectoryExternal) as HttpWebRequest;
if (request != null)
{
string responseData = GetResponseData(request);

string viewstate = ExtractViewsStateData(responseData, "__VIEWSTATE");
string eventValidation = ExtractViewsStateData(responseData, "__EVENTVALIDATION");

NetworkCredential credential = this.Credentials.GetCredential(null, string.Empty);

string username = HttpUtility.UrlEncode(credential.UserName);
string password = HttpUtility.UrlEncode(credential.Password);

string postData = this.GetPostData(viewstate, eventValidation, username, password);

CookieContainer cookieContainer = this.GetCookieContainer(postData);
CookieCollection collection = cookieContainer.GetCookies(
new Uri(this.WorkingDirectoryExternal));
if (collection.Count > 0)
{
foreach (Cookie cookie in collection)
{
Log.Debug(cookie.Value);
}
}
else
{
Log.Debug("No decent cookie found!");
}

this.webDavSessionCookieContainer.Add(collection);
this.cookieContainerField.SetValue(this.webDavSession, this.webDavSessionCookieContainer);
}
}
}

Line 4 practically does nothing more than check if we are currently working in external mode and if there is already a cookie associated with the external working domain which has not been expired. If there’s no such cookie or the present cookie is expired we must get a new fresh cookie from the server and therefore execute the injection logic.

Line 6 creates a new WebRequest to the external address of the external sharepoint zone. The request is passed to the method GetResponseData which simply assigns default credentials to the WebRequest and then reads the response stream into a stream reader and passes the read string to the calling injection method. The string response data practically represents the html site of the login form which was analyzed in the previous posts.

private static string GetResponseData(WebRequest request)
{
request.Credentials = CredentialCache.DefaultCredentials;
string responseData;
using (StreamReader responseReader = new
StreamReader(request.GetResponse().GetResponseStream()))
{
responseData = responseReader.ReadToEnd();
}

return responseData;
}

Line 11 and 12 calls the helper method ExtractViewStateData with the “__EVENTVALIDATION” and the “__VIEWSTATE” field which extracts the hidden fields showed in the analyzation state out of the response data.

private static string ExtractViewsStateData(string responseString, string viewStateNameDelimiter)
{
string valueDelimiter = "value=\"";
int viewStateNamePosition = responseString.IndexOf(viewStateNameDelimiter);
int viewStateValuePosition = responseString.IndexOf(valueDelimiter, viewStateNamePosition);
int viewStateStartPosition = viewStateValuePosition + valueDelimiter.Length;
int viewStateEndPosition = responseString.IndexOf("\"", viewStateStartPosition);
return HttpUtility.UrlEncodeUnicode(responseString.Substring(viewStateStartPosition, viewStateEndPosition - viewStateStartPosition));
}

Line 16 and 17 shows how the username and password provided by the ICredential interface must be encoded. If you don’t encode the username and password with url encoding the server will never give you the authorization rights.

Line 19 shows the call to the method GetPostData. This method actually constructs the post string by using configuration data which is injected from unity application block. For simplicity I show you the hardcoded post string.


/// <remarks>
/// 2: Username input field name,
/// 3: Username,
/// 4: Password input field name,
/// 5: Password
/// 6: Login input field name,
/// 7: Login input field value,
/// 8: Appendix
/// </remarks>
/// </summary>
private const string PostRequestFormat = "__LASTFOCUS=&__VIEWSTATE={0}&__EVENTTARGET=&__EVENTARGUMENT=&__EVENTVALIDATION={1}&{2}={3}&{4}={5}&{6}={7}{8}";

private string GetPostData(string viewstate, string eventValidation, string username, string password)
{
return string.Format(
CultureInfo.InvariantCulture,
PostRequestFormat,
viewstate,
eventValidation,
"ctl00$PlaceHolderMain$login$UserName",
username,
"ctl00$PlaceHolderMain$login$password",
password,
"ctl00$PlaceHolderMain$login$login",
"Sign In",
"&__spDummyText1=&__spDummyText2=");
}

Line 21 shows the call to GetCookieContainer which can be done when the post data is constructed. The method GetCookieContainer does nothing more than creating a new WebRequest to the login.aspx website by using the method POST and the x-www-form-urlencoded content type. A new cookie container is associated to this webrequest which then will have the authorization cookie in it when the authorization was successful. The data to be sent must be encoded by using Encoding.ASCII.GetBytes and the request content length must be set to the length of the data to be sent. Then the whole data is written into the request stream. When the request stream is written and closed the response stream must be read which will contain the authorization cookie on the property CookieContainer. This cookie container is passed to the calling method.

private CookieContainer GetCookieContainer(string postData)
{
HttpWebRequest request;
request = (HttpWebRequest) WebRequest.Create("https://portal.testsite.ch/_layouts/login.aspx?ReturnUrl=%2f");
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
CookieContainer cookies = new CookieContainer();
request.CookieContainer = cookies;
string args = postData;
byte[] dataToSend = Encoding.ASCII.GetBytes(args);
request.ContentLength = dataToSend.Length;
using (Stream st = request.GetRequestStream())
{
st.Write(dataToSend, 0, dataToSend.Length);
}

using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
StreamReader sr = new StreamReader(response.GetResponseStream());
string resp = sr.ReadToEnd();
}

return request.CookieContainer;
}

At the end in line 22 the CookieCollection which belongs to the external sharepoint zone must be extracted from the cookie container. Line 36 shows how this cookie collection is appended to the webDavSessionCookieContainer. Finally in line 37 we must set the field of the WebDavSession’s cookie container with the new cookie container containing the authorization cookie. Now we are successfully authenticated and are able to call WebDav methods on our session with the form based credentials.

And we lived happily ever after with the form based authentication and webdav on sharepoint! I hope this helped to understand this authentication issue.

About the author

Daniel Marbach

3 comments

Recent Posts