While coding up an iOS app with Xamarin I came across some odd behaviour when trying to programmatically sign in to O365 using HttpClient.

Xamarin

If you are not aware, Xamarin is built with Mono (the open source .NET Framework) and for all intents and purposes has the same namespace and language features as .NET 4.5. Excluding of course, all the Microsoft specific .NET parts.

HttpClient

OK I’ll set the scene. One of the requirements of my shiny new Xamarin app was to host a SharePoint site in a browser control. Authentication is normally taken care of by the O365 login page, but to make it even easier for the user I wanted the app do all of that heavy lifting programmatically, making sign in automatic. Being in the land of Xamarin and .NET (or is it heaven?) my natural choice was the System.Net namespace.

The first thing I did was create a HttpClient object passing to it a freshly new’d HttpClientHandler. The “handler” object is necessary in this case because we want it to hold a cookie container. It will gather and hold all the response/returned cookies for us. Mmmm cookies!

[sourcecode language=”csharp”]
var container = new CookieContainer ();
var handler = new HttpClientHandler ();
var client = new HttpClient (handler);

var content = new FormUrlEncodedContent (new [] {
new KeyValuePair<string,string> ("RelayState", System.Web.HttpUtility.UrlEncode("A*QfW….")),
new KeyValuePair<string,string> ("SAMLResponse", "PHNh…."),
});

try
{
handler.CookieContainer = container;
handler.AllowAutoRedirect = false;

var response = await client.PostAsync (
new Uri ("https://login.microsoftonline.com/login.srf"), content);

// …..
}
catch (Exception ex)
{
// System.Net.WebException: Invalid cookie domain
}
[/sourcecode]

Here you can see I’m doing a POST request to “login.microsoftonline.com” with some SAML authentication parameters. OK, so what’s wrong with this? Well upon calling PostAsync a WebException was thrown with a message indicating that an invalid cookie was encountered and could not be added to the cookie container. Further to this, the invalid nature of the cookie was its domain; specifically the “login” sub-domain. Weird.

Knowing already that this code runs fine on Microsoft Windows I was dumbfounded. I simply had no way to resolve this critical issue. After searching the internet I finally came across a Github pull-request from someone who had already encountered and fixed the problem in their own Mono branch. The down-side here is that the Xamarin team have not yet incorporated this fix 🙁

I’m Going Native

Given that my whole project was dangling precariously from the proverbial cliff, what now were my options?

  1. Branch Mono, make the fix, build and use my own .NET assemblies in iOS
  2. Implement a different approach programmatically

Option 1 was going to be my absolute last resort and probably the source of my next post. So I buckled my seatbelt, stowed my tray table, and raised my seat to the upright position in readiness for implementing Apple’s native foundation HTTP classes – still using C# of course!

While it’s true that this approach was far less elegant than the .NET way I could quickly see an advantage. Rather than having a container to collect the cookies, this native approach automatically stores the cookies in your app’s local cookie cache. Which in turn means that any other part of your app that needs those cookies can access them automatically too. e.g. A UIWebView control.

For this approach we need to use the following classes from the iOS SDK,

  • NSUrlConnection
  • NSMutableUrlRequest
  • NSUrlConnectionDelegate
  • NSData
  • NSError

We can tell these are native iOS classes because of the “NS” prefix. NeXTSTEP being the ancestor of OS X and iOS.

Let’s see how we might make a POST request using these classes,

[sourcecode language=”csharp”]
var request = new NSMutableUrlRequest (NSUrl.FromString(url));

request.HttpMethod = "POST";
request.ShouldHandleCookies = true;

var body = string.Empty; // add your POST parameters
if (!string.IsNullOrEmpty(body))
{
request.Body = NSData.FromString(body);
}

var signal = new Mutex (true);
var connectionInternal = new ConnectionDelegate (delegate (bool result, string error) {
signal.ReleaseMutex ();
});

var connection = NSUrlConnection.FromRequest (request, connectionInternal);

await Task.Run (() => { signal.WaitOne(); });

// ————————————— Class Delegate ————————————— //

public class ConnectionDelegate : NSUrlConnectionDelegate
{
readonly Action<bool, string> _complete;

public ConnectionDelegate (Action<bool, string> complete)
{
_complete = complete;
}

public override void ReceivedData (NSUrlConnection connection, NSData data)
{
// store the data response
}

public override void FinishedLoading (NSUrlConnection connection)
{
_complete (true, string.Empty);
}

public override void FailedWithError (NSUrlConnection connection, NSError error)
{
_complete (false, error.ToString());
}

public override void ReceivedResponse (NSUrlConnection connection, NSUrlResponse response)
{
var urlResponse = (NSHttpUrlResponse)response; // all the response headers are found here: urlResponse.AllHeaderFields
}

public override NSUrlRequest WillSendRequest (NSUrlConnection connection, NSUrlRequest request, NSUrlResponse response)
{
// don’t redirect
return !connection.OriginalRequest.Url.Equals (request.Url) ? null : connection.OriginalRequest;
}
}
[/sourcecode]

As you can see the request is asynchronous so I’m using Task/Await and a Mutex to signal when complete. There is a synchronous method I could have used (NSUrlConnection.SendSynchronousRequest) but I wanted to keep the UI responsive for the best user experience.

Here you can see the code has similarities to the System.Net.HttpClient approach except that we need to create our own derived class in order to respond to the POST request orchestration. Our derived class or “delegate” is responsible for,

  • Getting the headers from the response
    • ReceivedResponse
  • Getting the response content
    • ReceivedData
  • Ensuring that we aren’t redirected anywhere else
    • WillSendRequest
  • Signalling that we are all done
    • FinishedLoading

In summary, this implementation worked out fine and allowed my app to sign in to O365 without a hitch. Giddyup! Oh, and if you are wondering where to find all the cookies that have been cached for you, just look inside the NSHttpCookieStorage.SharedStorage.Cookies collection.

Category:
Application Development and Integration, Office 365, SharePoint
Tags:
, , , ,

Join the conversation! 1 Comment

Comments are closed.