Using the ADO.NET Data Services Silverlight client library in x-domain and out of browser scenarios – II (Forms Authentication)

Click here to download sample application  

Using the ADO.NET Data Services Silverlight client library in x-domain and out of browser scenarios – II (Forms Authentication)
In this blog post, we will talk about using the Silverlight Client Library against a Data Service that is secured with Asp.Net Forms Authentication
In short, the whole process of authenticating against a Forms Authentication protected Data Service looks like this.
image

Server Setup

  1. Setup Forms Authentication on the Data Service Server
  2. Enable the WCF Authentication Service by following the reference here : How to: Enable the WCF Authentication Service
  3. Exclude the following resources from requiring authentication ,

3.1 The WCF Authentication Service
3.2 The ClientAccessPolicy.xml File
Ex:

 <!-- The ClientAccessPolicy.xml file is required for the client to confirm if the server allows X-Domain callers.
       This should be downloadable without authenticating-->
 <location path="clientaccesspolicy.xml">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>
 <!--This should be downloadable without authenticating.-->
  <location path="AuthenticationService.svc">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>

4. If the DataServiceHost does not have a global.asax file, add one to the project.
5. In the Global.asax file, we need to listen on the AuthenticationService.CreatingCookie event to customize the FormsAuth Cookie that the service creates.

Why do we need to customize the FormsAuth cookie?
The WCF Authentication service by default creates HTTPOnly Cookies.
This means that the cookie isn’t accessible by client-script.
It generally isn’t a problem when the client application is running in the same domain as the Website,
as the browser handles cookie management for us transparently.
When the client is not in the same domain as the Website, and we use the ClientHttpWebRequest networking stack,
we are unable to access any cookies marked as HttpOnly.
To work around this limitation, we recreate the FormsAuth cookie with HttpOnly set to false in the CreatingCookie event handler.
For more details: How to: Customize the Authentication Cookie from the WCF Authentication Service
Example code:

 protected void Application_Start(object sender, EventArgs e)
{
//Handle the CreatingCookie event so that we can create a custom cookie with HttpOnly set to false.
//AuthenticationService.CreatingCookie on MSDN :
//https://msdn.microsoft.com/enus/library/system.web.applicationservices.authenticationservice.creatingcookie.aspx
AuthenticationService.CreatingCookie += new EventHandler<CreatingCookieEventArgs>(CreateSilverlightCompatibleHttpCookie);
}

 /// <summary>
/// Creates a HttpCookie that can be read by the managed CookieContainer in ClientHttpWebRequest in Silverlight
/// </summary>
/// <param name="sender">The calling context for this event</param>
/// <param name="e">a property bag containing useful information about the HttpCookie to create</param>
void CreateSilverlightCompatibleHttpCookie(object sender, System.Web.ApplicationServices.CreatingCookieEventArgs e)
{
  int cookieVersion = 1;
  //The time at which the cookie was issued by the server
  DateTime cookieIssueDate = DateTime.Now;
  //The relative time from now when the cookie will expire and the client will have to re-authenticate.
  DateTime cookieExpiryDate = DateTime.Now.AddMinutes(30);
  //The Forms Auth ticket which uniquely identifies a user 
  //FormsAuthenticationTicket on MSDN : https://msdn.microsoft.com/en-us/library/system.web.security.formsauthenticationticket.aspx
  FormsAuthenticationTicket ticket = new FormsAuthenticationTicket
                (cookieVersion,
                 e.UserName,
                 cookieIssueDate,
                 cookieExpiryDate,
                 e.IsPersistent, /*Indicates whether the authentication cookie should be retained beyond the current session*/
                 e.CustomCredential,
                 FormsAuthentication.FormsCookiePath);
 //Creates a string containing an encrypted forms-authentication ticket suitable for use in an HTTP cookie.
 //FormsAuthentication.Encrypt on MSDN : https://msdn.microsoft.com/en-us/library/system.web.security.formsauthentication.encrypt.aspx
  string encryptedTicket = FormsAuthentication.Encrypt(ticket);
  HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
  //set HttpOnly to false so that the managed CookieContainer can read the FormsAuth cookie from the response.
  cookie.HttpOnly = false;
  cookie.Expires = cookieExpiryDate;
  HttpContext.Current.Response.Cookies.Add(cookie);
  e.CookieIsSet = true;
 }

Silverlight Client Setup

We will follow an adapter pattern which is responsible for logging in the user and injecting the FormsAuth cookie as the client library makes requests.
To start with, add a Service reference to the WCF Authentication service in the Silverlight Client application or use the one in the sample.

The FormsAuthAdapter will use the client side proxy generated for the WCF Authentication service to login the user
and hook into any attached DataServiceContext instance’s SendingRequest event to inject the FormsAuth cookie.
An instance of the FormsAuthenticationAdapter is declared at the application level.

 public partial class App : Application
{
/// <summary>
/// FormsAuthenticationAdapter instance to manage authentication against a WCF Authentication Service
/// </summary>
public static FormsAuthenticationAdapter FormsAuthAdapter;

This is initialized when the Application starts.

 private void Application_Startup(object sender, StartupEventArgs e)
{
string authServiceUri = String.Empty;
//extract the AuthenticationService Uri from the App.XAML file
if (this.Resources.Contains("AuthenticationServiceUri"))
{
authServiceUri = this.Resources["AuthenticationServiceUri"].ToString();
}
FormsAuthAdapter = new FormsAuthenticationAdapter(new Uri(authServiceUri, UriKind.RelativeOrAbsolute));
//The FormsAuthCookieName  should be the same value as declared in the Web.config of the server
//ex: If your web.config on the server requiring Forms Authentication is :
//<authentication mode="Forms">
//  <forms loginUrl="LoginForm.aspx" name=".ASPXFormsAUTH" protection="All" path="/" />
//</authentication>
    FormsAuthAdapter.FormsAuthCookieName = ".ASPXFormsAUTH";
    this.RootVisual = new MainPage();
    //Uncomment the below 2 lines to show the Loginwindow on application startup
    //LoginWindow login = new LoginWindow();
    //login.Show();
}

This is what our client application looks like:
image

As you can see, along with the “Install me” button, we now have a “Login” button.

When the page loads, we attach an instance of the DataServiceContext which we want to get the FormsAuth Cookie injected while
making requests to the Data Service.

 //Attach the DataServiceContext instance so that we can inject the FormsAuth cookie for each request
App.FormsAuthAdapter.Attach(publicationContext);
Where the Attach Method’s signature is :
/// <summary>
/// Injects the FormsAuth cookie when the contextInstance makes a request to the DataService
/// </summary>
/// <param name="contextInstance">The DataServiceContext instance to observe</param>
public void Attach(DataServiceContext contextInstance)

Clicking the login button on the main page opens up a ChildWindow instance that we created which emulates the Login Screen.

Login button click handler:

 void LoginUser(object sender, RoutedEventArgs e)
{
    LoginWindow login = new LoginWindow();
    login.Show();
    //The LoginWindow only closes if Authentication succeeds
    login.Closing += (s, eArgs) =>
    {
       /*If auth succeeds,hide the button*/
       btnLogin.Visibility = Visibility.Collapsed;
    };
  }

image
The LoginWindow’s “Login” button uses the application wide FormsAuthenticationAdapter instance, discussed above, to login the user.

 private void LoginUser(object sender, RoutedEventArgs e)
{
App.FormsAuthAdapter.LoginAsync(txtUserName.Text, txtPassword.Password,
    (loginEventArgs) =>
    {
      if (loginEventArgs.Result)
       {
         /*Login succeeded*/
         this.DialogResult = true;
       }
       else
      {
         /*Login failed*/
      }
    }
    );
}
The LoginAsync method’s signature is:
  /// <summary>
/// Logs in the User and calls the LoginComplete handler
/// </summary>
/// <param name="userName">UserName to login </param>
/// <param name="passWord">password for the user account</param>
/// <param name="pLoginComplete">Called when the login process is complete</param>
public void LoginAsync(string userName, string passWord, Action<LoginCompletedEventArgs> pLoginComplete)

Once the user types in his/her username and password and hits “Login” , the Login window hits the WCF authentication service
and extracts the FormsAuth cookie from the response.When the client library makes a request to the Data Service , the FormsAuthenticationAdapter
injects the FormsAuth cookie

Common errors:

1. You receive an ArgumentException when trying to set the cookie header in the SendingRequest event.

a. System.ArgumentException occurred
  Message="The 'Cookie' header cannot be modified directly.\r\nParameter name: name"
  StackTrace:
       at System.Net.WebHeaderCollection.ThrowOnRestrictedHeader(String name, String value)
  InnerException:

Resolution: The reason you get this is because the client library is using the classic networking stack (based on XmlHttpRequest)
to make the request. In this case, the Cookie header isn’t accessible and the above exception is valid.
This is probably the only case where we would ask you to set the HttpStack property on the Client library.
To fix this:
//Set the HttpStack on the client Context instance to force the client library

//to use the ClientHttpWebRequest stack for network access
publicationContext.HttpStack = HttpStack.ClientHttp;

Additional resources:

How do I authenticate my users against the Active Directory from my Silverlight application?
In ASP.NET Forms Authentication, the Membership provider is responsible for accessing the Credential store and validating the user name and password.
By setting the Membership provider to be the ActiveDirectoryMembershipProvider , you can authenticate the user name and password the user enters
with the credentials stored in Active Directory. For more details , please refer to this MSDN article :

Using the ActiveDirectoryMembershipProvider
References :

About ClientAccessPolicy.xml files
ASP.NET Application Services.
ASP.NET Forms Authentication
Membership Providers

DataServicesXDomainSLClient.WithAuth.zip