RIA Services Authentication Out-Of-Browser


RIA Services does not support Out-Of-Browser (OOB) Forms authentication using the Client networking stack out of the box. The Client stack is often recommend for OOB scenarios as it provides a greater level of control over requests. The reason authentication does not work on the Client networking stack is that it handles cookies differently than the Browser networking stack. However, with a little coaxing RIA Authentication can be made to work in this scenario as well.

The first step is to switch to using the Client networking stack. The solution that follows cannot be applied when you’re using the Browser networking stack. It’s a good idea to take a look at how to enable the Client stack in your application. Often, it will only require the addition of these two lines in the App constructor.

WebRequest.RegisterPrefix("http://", WebRequestCreator.ClientHttp);
WebRequest.RegisterPrefix("https://", WebRequestCreator.ClientHttp);

The simple solution to this problem is to force each DomainContext to use a shared CookieContainer. When the Authentication DomainContext gets authenticated, it will store the cookies there and all subsequent operations will pass them back to the server. To make it simple to add cookie sharing to an application, I put together a static utility class.

/// <summary>
/// Utility for enabling Authentication using the ClientHttp networking stack.
/// </summary>
public static class ClientHttpAuthenticationUtility
{
    private static readonly SharedCookieHttpBehavior SharedCookieBehavior =
        new SharedCookieHttpBehavior();

    public static void ShareCookieContainer(DomainContext context)
    {
        PropertyInfo channelFactoryProperty =
            context.DomainClient.GetType().GetProperty("ChannelFactory");
        if (channelFactoryProperty == null)
        {
            throw new InvalidOperationException(
                "There is no 'ChannelFactory' property on the DomainClient.");
        }
        ChannelFactory factory = (ChannelFactory)
            channelFactoryProperty.GetValue(context.DomainClient, null);
        ((CustomBinding)factory.Endpoint.Binding).Elements.Insert(
            0, new HttpCookieContainerBindingElement());
        factory.Endpoint.Behaviors.Add(
            ClientHttpAuthenticationUtility.SharedCookieBehavior);
    }

    private class SharedCookieHttpBehavior : WebHttpBehavior
    {
        private readonly SharedCookieMessageInspector _inspector =
            new SharedCookieMessageInspector();

        public override void ApplyClientBehavior(
            ServiceEndpoint endpoint, 
            ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(this._inspector);
        }
    }

    private class SharedCookieMessageInspector : IClientMessageInspector
    {
        private readonly CookieContainer _cookieContainer =
            new CookieContainer();

        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            // do nothing
        }

        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            // make sure the channel uses the shared cookie container
            channel.GetProperty<IHttpCookieContainerManager>().CookieContainer =
                this._cookieContainer;
            return null;
        }
    }
}

Now to get this working, we need to apply this solution to each DomainContext. The easiest way to do this is to add a call to the partial OnCreated method generated for each Silverlight context.

namespace Web
{
    public partial class AuthenticationContext
    {
        partial void OnCreated()
        {
            ClientHttpAuthenticationUtility.ShareCookieContainer(this);
        }
    }

    namespace Services
    {
        public partial class MyDomainContext
        {
            partial void OnCreated()
            {
                ClientHttpAuthenticationUtility.ShareCookieContainer(this);
            }
        }
    }
}

It’s important to make sure the namespaces match when implementing partial methods. I included this snippet in my client project (within SampleNamespace) and added methods for SampleNamespace.Web.AuthenticationContext and SampleNamespace.Web.Services.MyDomainContext.

Comments (27)

  1. Aditya Voleti says:

    Hi Kyle,

    I am using SL OOB for a small project with Forms Authentication and did not have to do what you describe above. I run the project, it shows me the screen and I can log in. Is it that my project appears to be working but in fact its not?

    Thanks.

  2. kylemc says:

    No, your project is likely working just fine. This post mostly addresses issues related to the client networking stack. If you didn't use WebRequest.RegisterPrefix method like I mentioned above you're probably not using the client stack. No need to change if you like the way it's working now.

  3. Koala says:

    Hi, Kyle.

    The last time you use "ClientHttpAuthenticationUtility.ShareCookieContainer(this), you're using it on a class named "DomainService1".

    The namespace "Services" is Ok, but I suppose you want to say "DomainContext1"…

    Just to avoid confusion…

  4. Stimentic says:

    Hi Kyle,

    Thanks for your post, I've learned that I can add a Wcf behavior in Silverlight, which I thought I can't because you can not add it via the .ClientConfig file… stupid me.

    Anyway, I've tried your solution to manage cookies using the silverlight http stack, and I have to say that it works… But not as excpected. Because sharing a CookieContainer makes your wcf request sending serialized; A wcf queries is only sent to the server after the previous one is came back, which is excatly what I want to avoid using the silverlight http stack ^^

    Have you got any idea how to bypass this issue ? I'm stuck.

    Stimentic

  5. Stimentic says:

    Re,

    My bad, the problem I described was due to the asp sessionstate.

    Stimentic

  6. Ismael says:

    Problem is you cannot use WebContext.Current.Authentication.LoadUser() to automatically login an user with a persistent cookie. And the CookieContainer doesn't seem to be Serializable either.

  7. kylemc says:

    @Ismael

    Good point. You might be able to serialize the cookie if it were made available to the client (there's a flag you can set when you create the cookie on the server).

  8. cadessi says:

    It would have been easier if the cookies were exposed directly thru the CookieContainer, but this work. Still  don't like the need to hardcode the url in the SetCookies() method though, as this will easily change (development, staing, production):

         private class SharedCookieMessageInspector : IClientMessageInspector

           {

               private string cookie;

               private CookieContainer cookieContainer;

               private IsolatedStorageSettings store = IsolatedStorageSettings.ApplicationSettings;

               public SharedCookieMessageInspector()

               {

                   cookieContainer = new CookieContainer();

                   if (store.Contains("cookies"))

                   {

                       cookie = store["cookies"] as string;

                       if (cookie != null)

                       {

                           // FIXME: Url is hardcoded…

                           cookieContainer.SetCookies(new Uri(@"http://localhost:5023&quot;), cookie);

                       }

                   }

               }

               public void AfterReceiveReply(ref Message reply, object correlationState)

               {

                   // User possibly logged out

                   if (cookieContainer.Count == 0)

                   {

                       // Clear cookies

                       if (store.Contains("cookies"))

                       {

                           store.Remove("cookies");

                           store.Save();

                       }

                   }

                   else

                   {

                       HttpResponseMessageProperty httpResponse =

                           reply.Properties[HttpResponseMessageProperty.Name] as HttpResponseMessageProperty;

                       if (httpResponse != null)

                       {

                           string cookie = httpResponse.Headers["Set-Cookie"];

                           if (!string.IsNullOrEmpty(cookie))

                           {

                               if (store.Contains("cookies"))

                                   store["cookies"] = cookie;

                               else

                                   store.Add("cookies", cookie);

                               store.Save();

                           }

                       }

                   }                

               }

               public object BeforeSendRequest(ref Message request, IClientChannel channel)

               {

                   channel.GetProperty<IHttpCookieContainerManager>().CookieContainer = this.cookieContainer;

                   return null;

               }

           }

  9. Allen says:

    Thank you for this post.  I have a question regarding the second part of the post where you say "now to get this working" and then provide code that needs to be applied to each context.  Where does this code go?

    Thanks!

  10. kylemc says:

    @Allen

    You need to add this code to the client.

  11. Allen says:

    Kyle,

    Thanks again for this great post.  We have implemented this method and it works perfectly on Windows, but all of our RIA services calls are failing on a MAC when running in Out of Browser and using ClientHTTP.  We aren't getting any specific error messages other than the "Load Failed" error message.  We are storing a piece of information necessary to construct their connection string for the DomainService in the UserData of the AuthTicket.  We think that this is what is failing, but can't really see what is happening.

    Is there any reason that this would not work on a Mac using OOB?

    Thanks,

    Allen

  12. Allen says:

    Kyle,

    We did confirm that the Name and IsAuthenticated properties of the Identity being passed to our DomainService using this method on a Mac are not correct (Name is blank and IsAuthenticated is false).  The app runs fine inside the browser on a Mac and runs In Browser and OOB on Windows without issue, but doesn't seem to transfer the cookies from the Authentication Context (we can see through our logging a successful login in and creation of the AuthTicket) to the subsequent RIA calls on a Mac.

    We are stumped and appreciate any help or insight you might have.

    Allen

  13. kylemc says:

    @Allen

    This sounds like a complex scenario. We've never had any issues with cookies when testing on the Mac, but there could be something here. If you have a small repro, could you contact me via the 'Email Blog Author' link above? I may be able to help you a little more that way.

  14. Allen Sowles says:

    Kyle,

    Thanks so much for the response.  We were able to track down the problem by analyzing Nikhil Kothari's book club example.  The problem is that there wasn't a principal associated with the HttpContext.  By adding some code to the Application_AuthenticateRequest method in the global.asax that makes sure that we have a principal assigned to the HttpContext by extracting it from the FormsAuthentication ticket, we were able to fix the problem and now we can run in and out of browser, using RIA Authentication on both a PC and a MAC!  

    Thanks!

  15. Sijmen Koffeman says:

    Hi Kyle,

    I'm very sorry but I think I'm missing some parts of the puzzle here.

    Trying this sample with a generated AuthenticationContext class (in the …g.cs file); where do I put the partial OnCreated? In a seperate class file?

    Maybe you have the full solution available?

    Thanks!

  16. kylemc says:

    Yeah, just put them in another file on the client. As long as the generated class and the partial class are in the same assembly things should work fine. (Sorry, I don't have the project for this one)

  17. Sijmen Koffeman says:

    Got it. Compiles. Made a typo.

  18. sladapter says:

    Thanks Kyle and cadessi.

    In order for "RememberMe" to work we need cadessi's SharedCookieMessageInspector code. But we don't need to hard code the uri for the cookie.  The following code should work:

    public SharedCookieMessageInspector()

               {

                   cookieContainer = new CookieContainer();

                   if (store.Contains("cookies"))

                   {

                       cookie = store["cookies"] as string;

                       if (cookie != null)                                      

                           cookieContainer.SetCookies(new Uri(Application.Current.Host.Source, "../"), cookie);                                          

                   }

               }

  19. Matt Hudson says:

    Hey Allen, can you post the code that you added to Application_AuthenticateRequest.  I looked at Nikhil's Boook Club but I don't see anything in the Global.asax that adds the generic principal to the HTTPContext.  I'm having a problem with Forms Authentication on a Mac as well.

  20. Curtis Conner says:

    Has anyone got a real solution to fixing this OOB Mac issue with RIA WCF?  This is killin me

  21. James Reategui says:

    I'm having the same issue with OOB on the Mac. Using the latest WCF RIA Services SP1 and the built in Silverlight Business Application. I did notices the following, which makes no sense:

    If I load the app in a Mac from the browser, then login, and once logged in install the app. Now close the browser and load the app OOB and now I can login. Tried logging out and back in as different user and still works. Don't get it. What could be the difference on a Mac when installing the app while logged in?

  22. Seb31 says:

    Allen,

    i will say like Matt, can you show us what code you put in the Application_AuthenticateRequest

    Thanks;

  23. Dan M. says:

    All,

    Here is the code I put in Application_AuthenticateReqest — to no avail (still fails on the Mac with the Client Stack in OOB mode for authenticated calls):

    // Patch the HttpContext Principal to allow authenticated OOB access on the Apple platform

    protected void Application_AuthenticateRequest(object sender, EventArgs e)

    {

               // Extract the forms authentication cookie

               string cookieName = FormsAuthentication.FormsCookieName;

               HttpCookie authCookie = Context.Request.Cookies[cookieName];

               if (authCookie != null)

               {

                   // We have an authentication cookie.

                   FormsAuthenticationTicket authTicket = null;

                   try

                   {

                       authTicket = FormsAuthentication.Decrypt(authCookie.Value);

                   }

                   catch (Exception ex)

                   {

                       // Log exception details

                       Debug.WriteLine(ex.Message);

                   }

                   if (authTicket != null)

                   {

                       // Cookie decrypted OK

                       // When the ticket was created, the UserData property was assigned a pipe delimited string of role names.

                       string[] roles = authTicket.UserData.Split('|');

                       // Create an Identity object

                       FormsIdentity id = new FormsIdentity(authTicket);

                       // This principal will flow throughout the request.

                       GenericPrincipal principal = new GenericPrincipal(id, roles);

                       // Attach the new principal object to the current HttpContextobject

                       Context.User = principal;

                   }

               }

    }

    Code works on Windows in both OOB and browser mode.

    I am using RIA 4.0 SP1, and I do not share the experience of James (doesn't make any difference whether the application is installed before or after login; it does not work either way)

    Any thoughts as to what I am doing wrong?

    Thanks.

  24. Dan M. says:

    And finally the good news – after two intense days of troubleshooting, we solved the Mac OOB authentication problem for good – the solution turned out to be much, much simpler than we initially thought.

    It turns out that the default setting for forms authentication in Web.Config is "Device Default" — and even though that setting translates into “UseCookies” for a Windows client, it blocks (most) authentication cookies in OOB mode on the Mac (!)

    The solution is simple and elegant – set the forms authentication mode to "UseCookies" explicitly in Web.Config, like so:

    <system.web>

      <authentication mode="Forms">

        <forms cookieless="UseCookies" name="..ASPXAUTH" />

       </authentication>

    </system.web>

    And (magically) the Mac OOB client will start working just like a Windows OOB client with forms authentication using the Client Stack. No need for any of the "black magic" Application_AuthenticateRequest  stuff in Global.asax.cs, etc.

    Of course, all the other preconditions detailed by Kyle in this blog must be met: enable the client stack and set up a shared cookie container between the authentication context and the data context(s).

    Enjoy,

    Dan M.

    Midwest Software Technologies, Inc.

  25. Gerardo says:

    Hi Kyle

    My scenario it's similar and also a little bit different from the explained into your article, so I'll apreciate very much any help.

    I'm consuming a WCF RIA Service from a WinForm client; and our problems starts when we try to Authenticate using a Custom Authentication method describen on this post

    http://www.astaticstate.com/…/silverlight-custom-authentication.html

    Seems that the missing thing it's explained on your article, but following your solution we are stuck on the first line

    WebRequest.RegisterPrefix("http://&quot;, WebRequestCreator.ClientHttp);

    e

    On WinForms I havent visibility to System.Net.Browser assembly, so I can't use WebRequestCreator enum.

    Any idea?

    Thanks In advance

  26. shatokhin says:

    Hi, I am using this code, but unfortunatelly  if (cookieContainer.Count == 0) always is true. where is the problem?

    Thank you.

    Viktor

  27. shatokhin says:

    Found same problem http://www.go4answers.com/…/silverlight-4-iclientmessageinspector-44866.aspx

    Any idea how to persis cookie for Remember Me?

Skip to main content