Wednesday, September 26, 2012

Getting at Office 365 SharePoint Lists In Windows Phone 7 Silverlight Apps

[Edit 7/8/2013:  I've tweaked this to note that this was for WP7.  WP8 has a whole new set of SDKs for SP integration, and it's a different, and much easier story to use them.]

As promised from my presentation at SharePoint Saturday New Hampshire, I owe a code listing on the meaty part of the chat...  the Office 365 authentication component, especially.  It allows a Windows Phone Silverlight app to access things the lists.asmx service behind the Windows Live ID authentication.  (Frankly, the technique is the same, no matter what kind of client you're using, but the demo I was doing was using Silverlight 4 for Windows Phone 7.

I also owe slides:


Here's the activity rundown:

1)  The client app ("Windows Phone App") makes a SAML SOAP request to https://login.microsoftonline.com/extSTS.srf
2)  The SAML response comes back, allowing the app to parse the SAML token.
3)  Make another call, this time to  {your Office365 team site}/_forms/default.aspx?wa=wsignin1, posting the token.
4) The response that comes back need only be checked for errors, the magic is in the cookie container.  It contains an HTTPOnly token (which the development tools do a terribly good job of hiding.)
5)  Assign your cookie container from your previous result to the ListSoapClient that you're using to make your service calls from.
6)  Profit!



I broke up the "Activation" line on the client side to point out that the calls are Async.

In any case, I have a very rough SPAuthenticationHelper class that I also promised to post.

Here's an example of how you can use it:

    class SPTasksList
    {
 
        SPAuthenticationHelper _authenticationHelper;
        ListsSoapClient _listsClient;
        bool isBusy = false;

        TaskItem currentUpdate = null;
        string _taskListUri = "http://spsnh.sharepoint.com/TeamSite/Lists/Tasks/AllItems.aspx";

        public SPTasksList()
        {
            _authenticationHelper = new SPAuthenticationHelper(_taskListUri);
            _listsClient = new ListsSoapClient();
            _listsClient.GetListItemsCompleted += new EventHandler<GetListItemsCompletedEventArgs>(_listsClient_GetTasksListCompleted);
            _listsClient.UpdateListItemsCompleted += new EventHandler<UpdateListItemsCompletedEventArgs>(_listsClient_UpdateListItemsCompleted);
        }
 

        public void BeginGetTasksList()
        {
            if (!_authenticationHelper.IsAuthenticated)
            {
                _authenticationHelper.OnAuthenticated += new EventHandler<EventArgs>(_authenticationHelper_OnAuthenticated_GetTasks);
                _authenticationHelper.SigninAsync(Configuration.UserName, Configuration.Password);
            }
            else if (!isBusy)
            {
                isBusy = true;
                XElement query = XElement.Parse("Completed");
                string ListName = "Tasks";
                string ViewId = "{f717e507-7c6e-4ece-abf2-8e38e0204e45}";
                _listsClient.GetListItemsAsync(ListName, ViewId, query, null, null, null, null);
            }
        }

        void _authenticationHelper_OnAuthenticated_UpdateTask(object sender, EventArgs e)
        {
            _listsClient.CookieContainer = _authenticationHelper.Cookies;
            BeginUpdateTask(currentUpdate);
        }

......
} 

I ported this from a few other examples I found online to Silverlight for Windows Phone.  I apologize,  I haven't had time to polish it, and I'm having a hard time with the embedded SOAP litteral, but here's the SPAuthenticationHelper class:





using System;
using System.Net;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
 
namespace SPSNH_SPConnector.Implementation
{
    public class SPAuthenticationHelper
    {
        public CookieContainer Cookies { get; set; }
        public bool IsAuthenticated { get; private set; }
        public event EventHandler<EventArgs> OnAuthenticated;
 
        private bool _isAuthenticationInProgress = false;
 
        const string _authUrl="https://login.microsoftonline.com/extSTS.srf";
        const string _login="/_forms/default.aspx?wa=wsignin1.0";
       
        //namespaces in the SAML response
        const string _nsS = "http://www.w3.org/2003/05/soap-envelope";
        const string _nswst = "http://schemas.xmlsoap.org/ws/2005/02/trust";
        const string _nswsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
        const string _nswsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
        const string _nswsa = "http://www.w3.org/2005/08/addressing";
        const string _nssaml = "urn:oasis:names:tc:SAML:1.0:assertion";
        const string _nswsp = "http://schemas.xmlsoap.org/ws/2004/09/policy";
        const string _nspsf = "http://schemas.microsoft.com/Passport/SoapServices/SOAPFault";
        const string _samlXml = @"<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"">https://login.microsoftonline.com/extSTS.srf</a:To> <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>{0}</o:Username> <o:Password>{1}</o:Password> </o:UsernameToken> </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>{2}</a:Address> </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>";

        Uri _uri;  
        HttpWebRequest _getTokenRequest = HttpWebRequest.CreateHttp(_authUrl);
        HttpWebRequest _submitTokenRequest = null;
        string _token;
 
        public SPAuthenticationHelper(string uri)
        {
            _uri = new Uri(uri);
            Cookies = new CookieContainer();
        }
 
        public void SigninAsync(string userName, string password)
        {
            if (!_isAuthenticationInProgress)
            {
                _isAuthenticationInProgress = true;
                getTokenAsync(userName, password);
            }
        }
 
       
        private  void getTokenAsync(string userName, string password)
        {
            string tokenRequestXml = string.Format(_samlXml, userName, password, _uri.Host);
 
            _getTokenRequest.Method = "POST";
            _getTokenRequest.BeginGetRequestStream(new AsyncCallback(Get_GetToken_RequestStreamCallback), tokenRequestXml);
        }
 
        private void Get_GetToken_RequestStreamCallback(IAsyncResult result)
        {
            string tokenRequestXml = (string)result.AsyncState;
            var reqstream = _getTokenRequest.EndGetRequestStream(result);
            using (StreamWriter w = new StreamWriter(reqstream))
            {
                w.Write(tokenRequestXml);
                w.Flush();
            }
            _getTokenRequest.BeginGetResponse(new AsyncCallback(Get_GetToken_ResponseStreamCallback), null);
        }
 
 
 
        private void Get_GetToken_ResponseStreamCallback(IAsyncResult result)
        {
            _token = null;
 
            var response = _getTokenRequest.EndGetResponse(result);
 
            var xDoc = XDocument.Load(response.GetResponseStream());
 
            var body=xDoc.Descendants(XName.Get("Body", _nsS)).FirstOrDefault();
            if (body != null)
            {
                var fault = body.Descendants(XName.Get("Fault", _nsS)).FirstOrDefault();
                if (fault != null)
                {
                    var error=fault.Descendants(XName.Get("text", _nspsf)).FirstOrDefault();
                    if (error != null)
                        throw new Exception(error.Value);
                }
                else
                {
                    var token = body.Descendants(XName.Get("BinarySecurityToken", _nswsse)).FirstOrDefault();
                    if (token != null)
                    {
                        _token = token.Value;
                        SubmitTokenAsync();
                    }
                }
            }           
        }
 

        private  void SubmitTokenAsync()
        {
 
            UriBuilder bldr = new UriBuilder(_uri.Scheme, _uri.Host, _uri.Port);
            _submitTokenRequest = HttpWebRequest.CreateHttp(bldr.Uri + _login);
            _submitTokenRequest.CookieContainer = Cookies;
            _submitTokenRequest.Method = "POST";
            _submitTokenRequest.BeginGetRequestStream(new AsyncCallback(Get_SubmitToken_RequestStreamCallback), null);
        }
 
        private void Get_SubmitToken_RequestStreamCallback(IAsyncResult result)
        {
            var requestStream = _submitTokenRequest.EndGetRequestStream(result);
            using (StreamWriter w = new StreamWriter(requestStream))
            {
                w.Write(_token);
                w.Flush();
            }
            _submitTokenRequest.BeginGetResponse(new AsyncCallback(Get_SubmitToken_ResponseCallback), null);
        }
 
        private void Get_SubmitToken_ResponseCallback(IAsyncResult result)
        {
            UriBuilder bldr = new UriBuilder(_uri.Scheme, _uri.Host, _uri.Port);
 
            var response = _submitTokenRequest.EndGetResponse(result);
            string responseString = (new StreamReader(response.GetResponseStream())).ReadToEnd();
           
            bldr.Path = null;
            Cookies = _submitTokenRequest.CookieContainer;//.GetCookies(bldr.Uri);
            _isAuthenticationInProgress = false;
            IsAuthenticated = true;
            if (OnAuthenticated != null)
            {
                EventArgs args = new EventArgs();
                OnAuthenticated(this, args);
            }
        }
    }
}
 
 


Post a Comment