SharePoint FBA & Reactive Extensions
October 25, 2010 4 Comments
Some of the dangers with new toys is to overuse them, I fear I might be doing that with reactive extensions but oh well.. It’s all about the learning
Today I did a test to do Form Based Authentication with SharePoint using reactive extensions. This is the first attempt, it can probably be made more streamlined as I figure out more about Rx.
Oh, apologize for the syntax coloring in the code, I really need to find some better CSS for this…
I have two classes: One for doing the FBA (which also holds the cookie jar as we will get a cookie once the FBA is successful and need to pass this along to the next web service call) and one for doing web service calls.
public class FormsBasedAuthentication { public static CookieContainer CookieJar = new CookieContainer(); private const string AuthEnvelope = @"<?xml version=""1.0"" encoding=""utf-8""?> <soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/""> <soap:Body> <Login xmlns=""http://schemas.microsoft.com/sharepoint/soap/""> <username>{0}</username> <password>{1}</password> </Login> </soap:Body> </soap:Envelope>"; public static IObservable<string> Authenticate(string server, string username, string password) { Uri authService = new Uri(string.Format("{0}/_vti_bin/authentication.asmx", server)); HttpWebRequest authRequest = WebRequest.Create(authService) as HttpWebRequest; authRequest.CookieContainer = CookieJar; authRequest.Headers["SOAPAction"] = "http://schemas.microsoft.com/sharepoint/soap/Login"; authRequest.ContentType = "text/xml; charset=utf-8"; authRequest.Method = "POST"; return (from request in Observable.FromAsyncPattern<Stream>(authRequest.BeginGetRequestStream, authRequest.EndGetRequestStream)().Do(stream => { UTF8Encoding encoding = new UTF8Encoding(); string envelope = string.Format(AuthEnvelope, username, password); byte[] bytes = encoding.GetBytes(envelope); stream.Write(bytes, 0, bytes.Length); stream.Close(); }) from response in Observable.FromAsyncPattern<WebResponse>(authRequest.BeginGetResponse, authRequest.EndGetResponse)() select HandleResponse(response, authRequest)); } private static string HandleResponse(WebResponse response, HttpWebRequest request) { var httpResponse = response as HttpWebResponse; if (httpResponse != null){ var result = XDocument.Load(response.GetResponseStream()); var code = result.Descendants(XName.Get("ErrorCode", "http://schemas.microsoft.com/sharepoint/soap/")).FirstOrDefault(); if (code != null) return code.Value; } return "ErrorOccured"; } }
This will return an observable that will return the authentication result – if the login was successful, the published value will be NoError.
Next we have our web service class:
public class BaseWebService { private static string soapEnvelope = @"<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'> <soap:Body>{0}</soap:Body></soap:Envelope>"; private XDocument soapEnvelopeXml; public string BaseUrl { get; set; } #region Helper methods private XDocument CreateSoapEnvelope(string content) { var envelope = string.Format(soapEnvelope, content); XDocument soapEnvelopeXml = XDocument.Parse(envelope); return soapEnvelopeXml; } private HttpWebRequest CreateWebRequest(string url, string action) { Uri serviceUrl = new Uri(url, UriKind.Absolute); var webRequest = WebRequest.Create(serviceUrl) as HttpWebRequest; webRequest.Headers["SOAPAction"] = action; webRequest.ContentType = "text/xml;charset=\"utf-8\""; webRequest.Accept = "text/xml"; webRequest.Method = "POST"; webRequest.CookieContainer = FormsBasedAuthentication.CookieJar; return webRequest; } #endregion public IObservable<XDocument> CallService<T>(string url, string action, string soapContent) { soapEnvelopeXml = CreateSoapEnvelope(soapContent); HttpWebRequest webRequest = CreateWebRequest(url, action); var call = (from request in Observable.FromAsyncPattern<Stream>(webRequest.BeginGetRequestStream, webRequest.EndGetRequestStream)().Do(stream => { soapEnvelopeXml.Save(stream); stream.Close(); }) from response in Observable.FromAsyncPattern<WebResponse>(webRequest.BeginGetResponse, webRequest.EndGetResponse)() select Handle(response)); return call; } private XDocument Handle(WebResponse response){ var doc = XDocument.Load(response.GetResponseStream()); return doc; } }
This produces an observable which will publish a XDocument with the result of the web service call. As you can see, we are using our cookie jar from the FBA class (see the CreateWebRequest method) so that our authentication token gets passed along.
To put this together and do an authenticated call to the Lists.asmx web service in SharePoint, we need two small methods.
private static void GetList(string authStatus){ if (authStatus == "NoError"){ BaseWebService webService = new BaseWebService(); webService.CallService<XDocument>(“<url>/_vti_bin/Lists.asmx”, ActionGetList, string.Format(SoapGetList, "Pages")).Subscribe(Handle); } }
This method gets the published value from the FBA class and checks that we can proceed with our next call.
Oh, must not forget the SoapAction and content of the envelope:
private const string ActionGetList = "http://schemas.microsoft.com/sharepoint/soap/GetList"; private const string SoapGetList = @"<GetList xmlns='http://schemas.microsoft.com/sharepoint/soap/' xmlns:i='http://www.w3.org/2001/XMLSchema-instance'><listName>{0}</listName></GetList>";
private static void Handle(XDocument doc){ Console.WriteLine(doc.ToString()); }
And this one handles the result from the call to Lists.asmx (not doing much at this point).
So, to start it all off, we need to get our FBA observable set up:
var auth = FormsBasedAuthentication.Authenticate("<url to sharepoint site>", "<user>", "<password>");
Next we start it off:
auth.Do(GetList).Start();
What this does is that our FBA observable will call GetList for every published value that the FBA observable gives us (which should be just one) and to fire the entire process off we turn the ignition and .Start()
This is all then done asynchronously.