Xamarin Forms - Quick and Dirty Cookies Access (iOS and Android)

January 24, 2016

On one of my previous gigs I was building a Xamarin Forms app targeting iOS and Android, and needed to be able to access an OAuth-based cookie obtained from a WebView-based login flow.

The cookie in question needed to be attached to some pseudo-REST-based HTTP requests (made using the standard .NET HttpClient) to GET and POST data to an ASP.NET Web API serviced back-end system.

In order to maximise the cross-platform availability (and testability) of the library making those requests, it was built as a portable class library (PCL).

And because it needed to be xplat, there was no way to directly access the iOS and Android platform-specific Cookie managers.

On both iOS and Android, both platforms have a shared “Cookie Store” which is used by web browsers installed on the device. This includes the Xamarin Forms xplat WebView implementation, since this is just a cross-platform “placeholder” for the native web view’s custom renderer. So if the native browser has stored a cookie, the WebView can pick it up, and vice versa.

The good news is that the platspec cookie managers are easily exposed on both iOS and Android platforms (on iOS it’s a static instance of NSHttpCookieStorage.SharedStorage.Cookies and on Android it’s a static instance of Android.WebKit.CookieManager). More importantly they don’t even need a WebView for you to be able to get at the device’s cookies.

This means it’s really easy to provide a cross-platform solution using dependency injection.

What Can It Do?

My use case was pretty simple:

  • read the cookie(s) for a site I had logged in with,
  • extract the oauth-related one(s) I needed,
    • use them in the HttpClient headers to auth my requests with the back-end
  • delete those cookie(s) when done, from the cookie manager (to enforce a log-out function)

The rest of this post only deals with the methods I required, but I think in most cases that will suit your requirements too. And there’s ample documentation on how to use the respective cookie managers, should you need to expand your functionality.

The Cross-Platform (‘Shared’) Interface

In order to be able to pass the platspec instance of the cookie manager down to the cross-platform PCL (which actually uses it), I need to use a dependency injection. I used Splat for this, because it’s awesome, but obviously you can use whichever DI container you’re most comfortable with.

I created a shared IPlatformCookieStore interface which I can then use to register the actual platspec cookie manager instance in my DI container’s registry. The interface looks like this:

  public interface IPlatformCookieStore
  {
    /// <summary>

    /// List of cookies pulled from the cookie storage manager

    /// on the device/platform you're on. Can be quite an expensive call.

    /// </summary>

    IEnumerable<Cookie> CurrentCookies { get; }

    /// <summary>

    /// Debug method, just lists all cookies in the <see cref="CurrentCookies"/> list

    /// </summary>

    void DumpAllCookiesToLog();

    /// <summary>

    /// Clear cookies for site/url (otherwise auth tokens for your provider 

    /// will hang around after a logout, which causes problems if you want 

    /// to log in as someone else)

    /// </summary>

    void DeleteAllCookiesForSite(string url);
  }

Pretty straightforward: Give me a list of the current cookies. Let me dump all the cookies to a log so I can see what’s in them. And let me delete cookies from the platspec instance.

Then, a quick and dirty wrapper class with those interfaced methods wrapped around the iOS Cookie Manager (NSHttpCookieStorage.SharedStorage.Cookies).:

IOSCookieStore Implementation

  public class IOSCookieStore : IPlatformCookieStore
  {
    private readonly object _refreshLock = new object();

    public IEnumerable<Cookie> CurrentCookies
    {
        get { return RefreshCookies(); }
    }

    public IOSCookieStore(string url = "")
    {
    }

    private IEnumerable<Cookie> RefreshCookies()
    {
      lock (_refreshLock)
      {
        foreach (var cookie in NSHttpCookieStorage.SharedStorage.Cookies)
        {
          yield return new Cookie
          {
            Comment = cookie.Comment,
            Domain = cookie.Domain,
            HttpOnly = cookie.IsHttpOnly,
            Name = cookie.Name,
            Path = cookie.Path,
            Secure = cookie.IsSecure,
            Value = cookie.Value,
            /// TODO expires? / expired?

            Version = Convert.ToInt32(cookie.Version)
          };
        }
      }
    }

    public void DumpAllCookiesToLog()
    {
      if (!CurrentCookies.Any())
      {
        LogDebug("No cookies in your iOS cookie store. Srsly? No cookies? At all?!?");
      }
      CurrentCookies.ToList()
                    .ForEach(cookie => 
                            LogDebug(string.Format("Cookie dump: {0} = {1}", 
                                                    cookie.Name, 
                                                    cookie.Value)));
    }

    public void DeleteAllCookiesForSite(string url)
    {
      var cookieStorage = NSHttpCookieStorage.SharedStorage;
      foreach(var cookie in cookieStorage.CookiesForUrl(new NSUrl(url)).ToList())
      {
          cookieStorage.DeleteCookie(cookie);     
      }
      // you MUST call the .Sync method or those cookies may hang around for a bit

      NSUserDefaults.StandardUserDefaults.Synchronize();
    }

  }

The LogDebug method is just abstract I’ve created here for demo purposes, this could just as easily point to your logging library instance, or something like Console.WriteLine().

DroidCookieStore Implementation

  public class DroidCookieStore : IPlatformCookieStore
  {
    private readonly string _url;
    private readonly object _refreshLock = new object();

    public IEnumerable<Cookie> CurrentCookies
    {
      get { return RefreshCookies(); }
    }

    public DroidCookieStore(string url = "")
    {
      if (string.IsNullOrWhiteSpace(url))
      {
        throw new ArgumentNullException("url", "On Android, 'url' cannot be empty, 
        please provide a base URL for it to use when loading related cookies");
      }
      _url = url;
    }

    private IEnumerable<Cookie> RefreshCookies()
    {
      lock(_refreshLock)
      {
        // .GetCookie returns ALL cookies related to the URL as a single, long 

        // string which we have to split and parse

        var allCookiesForUrl = CookieManager.Instance.GetCookie(_url);

        if(string.IsNullOrWhiteSpace(allCookiesForUrl))
        {
          LogDebug(string.Format("No cookies found for '{0}'. Exiting.", _url));
          yield return new Cookie("none", "none");
        }
        else
        {
          LogDebug(string.Format("\r\n===== CookieHeader : '{0}'\r\n", allCookiesForUrl));

          var cookiePairs = allCookiesForUrl.Split(' ');
          foreach(var cookiePair in cookiePairs.Where(cp => cp.Contains("=")))
          {
            // yeah, I know, but this is a quick-and-dirty, remember? ;)

            var cookiePieces = cookiePair.Split(new[] {'='}, StringSplitOptions.RemoveEmptyEntries);
            if(cookiePieces.Length >= 2)
            {
              cookiePieces[0] = cookiePieces[0].Contains(":")
                ? cookiePieces[0].Substring(0, cookiePieces[0].IndexOf(":"))
                : cookiePieces[0];

              // strip off trailing ';' if it's there (some implementations 

              // on droid have it, some do not)

              cookiePieces[1] = cookiePieces[1].EndsWith(";")
                ? cookiePieces[1].Substring(0, cookiePieces[1].Length - 1)
                : cookiePieces[1];

              yield return new Cookie()
                                {
                                  Name = cookiePieces[0],
                                  Value = cookiePieces[1],
                                  Path = "/",
                                  Domain = new Uri(_url).DnsSafeHost,
                                };
            }
          }
        }
      }
    }

    public void DumpAllCookiesToLog()
    {
      // same as for iOS

    }


    public void DeleteAllCookiesForSite(string url)
    {
      // TODO remove specific cookies by name...?

      // coz this may be a bit scorched-earth...

      CookieManager.Instance.RemoveAllCookie();
    }
  }
  

Now we add our shared IPlatformCookieStore to our DI container’s registry (as I mentioned earlier, I use Splat, but you can use whatever you like).

iOS

In my iOS project’s AppDelegate, in the FinishedLaunching method:

  public override bool FinishedLaunching(UIApplication app, NSDictionary options)
  {
      Locator.CurrentMutable.RegisterConstant(new IOSCookieStore(), typeof(IPlatformCookieStore));
              
      global::Xamarin.Forms.Forms.Init();

      LoadApplication(new App());
      
      return base.FinishedLaunching(app, options);
  }

Locator is just my static Splat DI instance

Android

In my Android project’s MainActivity, in the OnCreate method:

  protected override void OnCreate(Bundle bundle)
  {
      base.OnCreate(bundle);

      Locator.CurrentMutable.Register(() => new DroidCookieStore("http://your.web.url"), 
                                            typeof(IPlatformCookieStore));

      global::Xamarin.Forms.Forms.Init(this, bundle);

      LoadApplication(new App());
  }

Note that the constructor for the DroidCookieStore requires a website base URL for proper initialisation

Wrapping Up

Now all that remains is to access your platspec cookie store from your shared cross-platform code by interacting with the instance you registered with your DI container.

How and where you use it will depend on your implementation. A simple (contrived) solution can be done something like the code below (NB: I’ve not compiled the class below, but the intent should be clear):

  public class LameCookieStoreDemo()
  {
    private IPlatformCookieStore _cookieStore;
    private System.Net.Cookie _oauthCookie;
    
    public LameCookieManagerDemo()
    {
      _cookieStore = Splat.Locator.Current.GetService<IPlatformCookieStore>();
    }
    
    public void RunMeWhenWebViewIsDoneNavigating()
    {
      _cookieStore.DumpAllCookiesToLog();
      _oauthCookie = _cookieStore.CurrentCookies
                                .FirstOrDefault(cc => cc.Name == "oauthCookieName");
      if (_oauthCookie != null)
      {
        // do stuff with it, like hand it off to your Web 

        // Api's REST/RPC call, or whatever

      }
    }
    
    public void LogoutByDeletingOAuthCookies()
    {
      _cookieStore.DeleteAllCookiesForSite("http://your.web.url");
    }
      
  }

Another option is to inherit from the xplat WebView control in Xamarin Forms, and add an IPlatformCookieStore object to it. You can then go hunting for your cookies whenever (for example) the WebView.OnNavigated or OnNavigating methods fire.

So where is it?

The source code is on GitHub as a gist, at https://gist.github.com/wislon/260438ee77e8de9e4ffc. I haven’t had time to pull all the bits together into a buildable, standalone project, but this will definitely be enough to get you going.

License

The code in this gist/repo is released under the free-for-all MIT License, so if you want to copy it and do better stuff with it, go right ahead! :)


comments powered by Disqus