Connecting to SharePoint on Office 365 with Windows Phone 7

One of my colleagues recently asked me to look into connecting to a SharePoint site running on Office 365 from a Windows Phone 7 application. Now this is something that the phone has native support for but in this case he wanted to be able to build an application that had added functionality and then called into the SharePoint web service APIs to extract data from SharePoint.

Attempt number 1

At first I thought this would be really easy, add a reference to the web service, call the Logon API and then we’re away. Unfortunately that isn’t the case.

I found this blog article by Paul Stubbs which was a good starting point.

When I implemented this against Office 365 it all seemed good until I actually tried to logon, I kept getting a logon failure and couldn’t figure out why. Eventually I twigged, Office 365 uses federated identity. As a result the SharePoint logon APIs don’t recognise your credentials, it knows nothing about them.

Attempt number 2

I then traced out the flow of an actual logon and found that Office 365 uses login.microsoftonline.com to authenticate against. At least this is the case on my Office 365 site, if you federate with a different provider then you’ll have to adapt this solution appropriately.

Now the flow here is a fairly standard WS-Trust flow, you hit the SharePoint site, that redirects a couple of times internally and then goes off to microsoftonline.com passing it a bit of security information. This asks you for your username and password in a web page and then redirects you back to Office 365 with some more security information.

SharePoint then writes out a cookie with a security token in, as long as web service API requests pass this cookie they’ll work.

So this is simple, I pop up a web browser, hit SharePoint go through the logon flow, grab the cookie once it comes back to SharePoint and we’re good to go.

Except we’re not, the cookie gets written with HttpOnly which means that you cannot programatically get the cookie.

Enter the hack

The solution I came up with is a little brittle but it works ok for now for me. If you look at how the WS-Trust flow works as implemented by this secure token service (STS) after log on it writes out a form with POST data on it containing the WS-Trust security data, then it uses JavaScript to post this back to SharePoint.

So what I do is roughly this:

  1. Check if not logged on
  2. Pop up a web browser control
  3. Navigate to the SharePoint site home page which will redirect to the STS
  4. User enters credentials
  5. I hook up navigation events from the browser control and when I spot a navigate to the page with the POST data from the STS…
  6. Capture the security information from the form from the browser control
  7. Programatically POST the form back to SharePoint using a new CookieContainer and HttpWebRequest
  8. SharePoint completes the WS-Trust flow and issues the SP security cookie which goes into the new CookieContainer
  9. Attach the new CookieContainer to all web service API calls and it’s sorted

Now as mentioned this is a little brittle as if the STS changes how it works a bit then it’ll break but most of the time that’s unlikely, especially if you are running you own STS to federate your corporate logons, you’ll control it’s appearance.

I’ve included some sample code that does this, usual caveats apply. The DefaultSettings class has the SharePoint site address and SSL settings in there, change this to point at your site.

I’ve wrapped the CookieContainer in a SharepointServerContext class to isolate the using code from this. This class also contains other information about the connection the server such as it’s address etc. All of the web service API calls are executed by my own ISharepointCommand classes, these commands all take a SharepointServerContext so they know which server to talk to and can get to the CookieContainer for authentication.

Most of the logon flow code is in the code behind for the LogonView since it needs access to the browser control.

Next steps

There are bugs in the code, things are not complete, this is a work in progress but it does demonstrate the flow mentioned above. Currently if you authenticate and tick remember me it’ll break the next time you run since the flow will be different, it doesn’t cache cookies yet as that’s something else I need to work out so this is not a perfect approach. But hopefully it’ll help as a starting point for people looking at this.

You can download the sample project from here SharePointConnector. Good luck.

Update

After a little bit of investigation with the help of Rob de Beir (see comments below) we’ve discovered that the solution as posted only actually works on E1 Office 365 deployments, if you’re running P1 then it doesn’t work.

This is down to 4 things;

  1. P1 SOAP APIs only work over HTTP, E1 work over HTTPS
  2. There was a bug in the code (gasp) that meant setting SSL = false didn’t work
  3. The logon view didn’t process the POST address from the STS, it was hardcoded
  4. P1 sites have a path

Fortunately this is an easy fix, disable SSL in the DefaultSettings.cs, fix the bug (shown below) and make sure you set the host in the DefaultSettings.cs to have the path too e.g. mysite.sharepoint.com/teamsite.

To fix the bug simply find the line of code in GetListItems.cs where the BasicHttpBinding is created and change it to read;

new BasicHttpBinding(ServerContext.UseSsl ?
 BasicHttpSecurityMode.Transport :
 BasicHttpSecurityMode.None),

There is another bit that needs to change in the LogonView.xaml.cs file too. Replace the OnBrowserNavigated method with this one in order to correctly parse out the return address from the STS.

private void OnBrowserNavigated(object sender, NavigationEventArgs e)
{
    if (!IsLastPage(e.Uri))
        return;

    string content = Browser.SaveToString();
    Regex actionRegex = new Regex("<FORM .* action=(?'action'.*?) target.*",
                                  RegexOptions.Multiline |
                                  RegexOptions.IgnoreCase);
    Regex tokenRegex = new Regex("name=t value=(?'token'.*?) type=",
                                 RegexOptions.Multiline |
                                 RegexOptions.IgnoreCase);

    Group tokenMatch = tokenRegex.Match(content).Groups["token"];
    Group actionMatch = actionRegex.Match(content).Groups["action"];

    if (tokenMatch.Success && actionMatch.Success)
    {
        _token = tokenMatch.Value;
        _action = actionMatch.Value;

        Browser.Navigated -= OnBrowserNavigated;
        Browser.NavigateToString(Strings.LogonPageLoadingMessage);
        Logon();
    }
}

Add a backing field for the action.

private string _action;

And finally update Logon() to use the new action.

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_action);

That should fix it all to work on E1 as well as P1. Thanks to Rob for his help in debugging this, I’ll try and get updated source up soon.



							

7 thoughts on “Connecting to SharePoint on Office 365 with Windows Phone 7

  1. Thanks for this great article. Trying to get office 365 data through web service calls in the windows phone 7. Havne’t found any other source with a working example.

    I’m trying to get you rdsample project working. Have changed the Defaultsettings class, and get the Windoes live logon form. But after pressing the sign in button the program stops with the message “JavaScript required to sign in”.

    I change the logonview.xaml to but that does not make a difference.

    Do you have an idea what is could be the reason for this error?

  2. LOL,

    Just looked a little further in your code and saw the following lines:

    public void Start()
    {
    Browser.Navigated += OnBrowserNavigated;
    Browser.Navigating += OnBrowserNavigating;
    Browser.IsScriptEnabled = true;
    Browser.Navigate(new Uri(_serverContext.BaseUri + “_layouts/authenticate.aspx”));
    }

    So indeed the change I made in logonview.xaml : was useless, since are setting this in the code.

    Still thinking about what could be the problem that the browser asks for the Javascript required.

  3. I see that the scriptenabled is disabled in this code:

    private void OnBrowserNavigating(object sender, NavigatingEventArgs e)
    {
    if ((e.Uri.Host == StsHostName) &&
    HttpUtility.UrlDecode(e.Uri.Query).Contains(WSTrustSignin) &&
    (e.Uri.AbsolutePath.Contains(StsLastPage)))
    {
    Browser.IsScriptEnabled = false;
    }

    e.Cancel = false;
    }

    If I remark this deactivating line, I don’t get the javascript requred error, but get always the message back that the Id or password is wrong.

  4. Hoi,

    About the blog comments I made yesterday:

    Seems that yesterday I used the wrong ID to sign in into Office 365. When the Authentication is unsuccessful, you get the “requiring javascript” error I wrote about yesterday. When you comment out the “Browser.IsScriptEnabled = false;”, you can see that the logon has been unsuccessful.

    OK, so I manage to logon to the site, however getting the documents is not working yet, an error is raised on the ” _client.GetListItemsAsync(ListName, String.Empty, null, null, null, null,null, tracker);” The library ‘Shared Documents” exists on my site. Will investigate a little bit more to see the exact error message.

    The error message is now: “System.ServiceModel.ProtocolException was unhandled
    Message=The content type text/html; charset=utf-8 of the response message does not match the content type of the binding (text/xml; charset=utf-8).” But that is most likely SharePoint is returning a html message showing an error instead of returning the xml data.

    Is there a way to see the full error message in such a case? Only showing now: “The first 1024 bytes of the response were: ‘”

  5. The error message I receive is the following, but does not give me a direction where to look for a solution:

    System.ServiceModel.ProtocolException was unhandled
    Message=The content type text/html; charset=utf-8 of the response message does not match the content type of the binding (text/xml; charset=utf-8).
    If using a custom encoder, be sure that the IsContentTypeSupported method is implemented properly.
    The first 1024 bytes of the response were: ‘

    BODY { color: #000000; background-color: white; font-family: Verdana; margin-left: 0px; margin-top: 0px; }
    #content { margin-left: 30px; font-size: .70em; padding-bottom: 2em; }
    A:link { color: #336699; font-weight: bold; text-decoration: underline; }
    A:visited { color: #6699cc; font-weight: bold; text-decoration: underline; }
    A:active { color: #336699; font-weight: bold; text-decoration: underline; }
    A:hover { color: cc3300; font-weight: bold; text-decoration: underline; }
    P { color: #000000; margin-top: 0px; margin-bottom: 12px; font-family: Verdana; }
    pre { background-color: #e5e5cc; padding: 5px; font-family: Courier New; font-size: x-small; margin-top: -5px; border: 1px #f0f0e0 solid; }
    td { color: #000000; font-family: Verdana; font-size: .7em; }
    h2 { font-size: 1.5em; font-weight: bold; margin-top: 25px; margin-bottom: 10px; border-top: 1p’.
    StackTrace:
    at System.ServiceModel.Channels.HttpChannelUtilities.ValidateRequestReplyResponse(HttpWebRequest request, HttpWebResponse response, HttpChannelFactory factory, WebException responseException)
    at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.ProcessResponse(HttpWebResponse response, WebException responseException)
    at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.CompleteGetResponse(IAsyncResult result)
    at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
    at System.Net.Browser.ClientHttpWebRequest.c__DisplayClassa.b__8(Object state2)
    at System.Threading.ThreadPool.WorkItem.WaitCallback_Context(Object state)
    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
    at System.Threading.ThreadPool.WorkItem.doWork(Object o)
    at System.Threading.Timer.ring()

  6. Still trying to get the web service call to work, but no success.

    I Have set up an Office 365 trial for 30 days to test this code. Could I sent you a user account for this Office 365 site, so you can run your code on this site? Maybe you can see quickly what is happening?

    • Hi Rob, you’ve been busy 🙂

      I disable the script at that point in the browser to stop it from firing off the POST request back to Office 365 from the STS as I want to do that myself. It sounds like you’ve got the logon bit working though now and just need to get access to the shared documents list?

      I’ll send you an email with my contact details in so you can send me your logon info and I’ll take a look.

Comments are closed.