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("https://", 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
Anonymous
May 25, 2010
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.Anonymous
May 25, 2010
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.Anonymous
July 12, 2010
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...Anonymous
July 22, 2010
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. StimenticAnonymous
July 25, 2010
Re, My bad, the problem I described was due to the asp sessionstate. StimenticAnonymous
September 26, 2010
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.Anonymous
September 26, 2010
@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).Anonymous
September 27, 2010
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"), 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; } }Anonymous
September 29, 2010
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!Anonymous
September 29, 2010
@Allen You need to add this code to the client.Anonymous
November 24, 2010
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, AllenAnonymous
November 24, 2010
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. AllenAnonymous
December 01, 2010
@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.Anonymous
December 10, 2010
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!Anonymous
December 14, 2010
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!Anonymous
December 15, 2010
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)Anonymous
December 15, 2010
Got it. Compiles. Made a typo.Anonymous
February 05, 2011
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); } }Anonymous
February 27, 2011
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.Anonymous
April 03, 2011
Has anyone got a real solution to fixing this OOB Mac issue with RIA WCF? This is killin meAnonymous
May 02, 2011
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?Anonymous
May 25, 2011
Allen, i will say like Matt, can you show us what code you put in the Application_AuthenticateRequest Thanks;Anonymous
July 30, 2011
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.Anonymous
August 01, 2011
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.Anonymous
December 06, 2011
The comment has been removedAnonymous
December 08, 2011
Hi, I am using this code, but unfortunatelly if (cookieContainer.Count == 0) always is true. where is the problem? Thank you. ViktorAnonymous
December 09, 2011
Found same problem www.go4answers.com/.../silverlight-4-iclientmessageinspector-44866.aspx Any idea how to persis cookie for Remember Me?