Partager via


Business Apps Example for Silverlight 3 RTM and .NET RIA Services July Update: Part 26: Authentication and Personalization

The data we work with in business application is valuable.  We need to protect the data.. sometimes by keeping up with exactly who accesses and updates what data when and other times we need to actively prevent data from being accessed expected by trust parties.   

The web is increasingly becoming a personal place – applications often “know about you” enabling users to having customized settings that work everywhere the apps works. 

In this example, I will take our ever popular SuperEmployees application and augment it to show more details on the authentication and personalization. 

You can see the full series here.

The demo requires (all 100% free and always free):

  1. VS2008 SP1
  2. Silverlight 3 RTM
  3. .NET RIA Services July '09 Preview

 download the full demo files

 

Basic Authentication

Let’s start by looking at how we ensure that only authenticated users can access access the data and keep a very simple log of who access the data.  

Starting with the original example,  let’s look at adding authentication to the GetSuperEmployees() method on the DomainService in the server project.

  1. [RequiresAuthentication]
  2. public IQueryable<SuperEmployee> GetSuperEmployees()
  3. {

Once we use the RequiresAuthentication attribute, the system will ensure that only calls from authenticated users make it through.  That means we can do some very simple such as logging who is accessing the data and when:

  1. [RequiresAuthentication]
  2. public IQueryable<SuperEmployee> GetSuperEmployees()
  3. {
  4.     File.AppendAllText(@"C:\users\brada\desktop\userslog.txt",
  5.         String.Format("{0}: {1} {2}", DateTime.Now,
  6.         ServiceContext.User.Identity.Name, Environment.NewLine));

(check out a package such as Log4net for a complete solution for logging).

Now when we run the application, no results are returned. 

image

 

We need to log in to see some results… Luckily the Business Application Template that ships with .NET RIA Services includes great support for this. 

Click on login

image

Notice here, we could change to use window auth to get integrated NTLM security, either way works fine.

Then register now

image

and we can create a new user directly from the Silverlight client.

image

Notice if you want to customize the look and feel of any of these dialogs, it is easy to do by looking in the Views\LoginControl.xaml, Views\LoginWindow.xaml.

And if you want to control the backend on how these are implemented, you can by looking in server project under Services\AuthenticationService.cs and UserRegistrationService.cs.  By default these go against the ASP.NET Membership and roles system, but you can customize them to do whatever you’d like by simply overriding the methods there. 

Now, we just need to react to the logged in event.  In this case, I am going to simply reload the data when the user logs in.  Lines 10-13 signs up for the logged in event and sets reloads the data, this time, as an authenticated user. 

  1. public Home()
  2. {
  3.     InitializeComponent();
  4.  
  5.     var context = dds.DomainContext as SuperEmployeeDomainContext;
  6.     originFilterBox.ItemsSource = context.Origins;
  7.  
  8.     context.Load(context.GetOriginsQuery());
  9.  
  10.     RiaContext.Current.Authentication.LoggedIn += (s, e) =>
  11.     {
  12.         if (dds != null) dds.Load();
  13.     };
  14.  
  15. }

 

image

 

And notice, the client knows who I am:

image

And the server knows as well..  If you go look at the log file we create in the DomainService we will see:

image

So, that is cool, but I think we can do a bit better on the client user experience.   After all, I get no error whatsoever to tell me I need to log in to see the data.  

First, let’s follow best practice and handle the DDS.LoadedData event and simply show any errors that are returned. 

  1. <riaControls:DomainDataSource x:Name="dds"
  2.         AutoLoad="True"
  3.         QueryName="GetSuperEmployeesQuery"
  4.         LoadedData="dds_LoadedData"
  5.         LoadSize="20">

Then the implementation is very simple:

  1. private void dds_LoadedData(object sender, LoadedDataEventArgs e)
  2. {  
  3.     if (e.Error != null)
  4.     {
  5.         var win = new ErrorWindow(e.Error);
  6.         win.Show();
  7.     }
  8. }

Now, when we run this app, we get this error:

image

That is helpful, maybe for a developer, but for an end user, maybe we want something more explicit. 

The first step is to not even make the request if the user is not authenticated.  We know that on the client, so this is very easy to do.

First, let’s sign up for the DDS.DataLoading event to capture the load before it happens. 

  1. <riaControls:DomainDataSource x:Name="dds"
  2.         AutoLoad="True"
  3.         QueryName="GetSuperEmployeesQuery"
  4.         LoadedData="dds_LoadedData"
  5.         LoadingData="dds_LoadingData"
  6.         LoadSize="20">

then we will simple cancel the load if the user is not authenticated. 

  1. private void dds_LoadingData(object sender, LoadingDataEventArgs e)
  2. {
  3.    e.Cancel = !RiaContext.Current.User.IsAuthenticated;
  4. }

 

Now, let’s provide an alternate way to tell the user they need to log on.     We simply add some text and make it visible only when the user is not authenticated. 

  1. <TextBlock Text="Data is only available to authenticated users" Foreground="Red"
  2.            DataContext="{StaticResource RiaContext}"
  3.            Visibility="{Binding Path=User.IsAuthenticated, Converter={StaticResource VisibilityConverter}}">
  4. </TextBlock>

The implementation of the value convert is pretty simple. 

  1. public class VisibilityConverter : IValueConverter
  2. {
  3.     public object Convert(
  4.         object value,
  5.         Type targetType,
  6.         object parameter,
  7.         CultureInfo culture)
  8.     {
  9.         bool visibility = (bool)value;
  10.         return visibility ? Visibility.Collapsed : Visibility.Visible;
  11.     }
  12.  
  13.     public object ConvertBack(
  14.         object value,
  15.         Type targetType,
  16.         object parameter,
  17.         CultureInfo culture)
  18.     {
  19.         Visibility visibility = (Visibility)value;
  20.         return (visibility != Visibility.Visible);
  21.     }
  22. }

 

Now, when we run this, we get a nice UX:

image

Then when we log in, it looks nice.

image

We can even make it a bit better by giving users an easy to to log in from here:

  1. <TextBlock Text="Data is only available to authenticated users. Please click here to log in." Foreground="Red"
  2.            DataContext="{StaticResource RiaContext}"
  3.            Visibility="{Binding Path=User.IsAuthenticated, Converter={StaticResource VisibilityConverter}}"
  4.            MouseLeftButtonUp="TextBlock_MouseLeftButtonUp">
  5. </TextBlock>

 

  1. private void TextBlock_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  2. {
  3.     new LoginWindow().Show();
  4. }

image

 

What we showed in this section is how easy it is to require authentication for data and how to create a great user experience for this on the client. 

 

Personalization

Now that we have the basics of authentication down, let’s see how we can provide a bit more of a personalized experience.  For many applications, uses spend a huge amount of time in the application, we want them to feel comfortable and in control of their experience.    For the first part of this, let’s create a user setting for the background color of the application.  Each user can have a different value and it should follow them no mater what machine they run the application on.   

Let’s start be defining a profile property in the web.config file. 

  1. <profile enabled="true" >
  2.   <properties>
  3.     <add name="PageBackgroundColor"  defaultValue="White"/>
  4.   </properties>
  5. </profile>

Then we can make this strongly typed by adding it to the AuthenticationService.cs file on the server.  

  1. public class User : UserBase
  2. {
  3.     public string PageBackgroundColor { get; set; }
  4. }

 

Now we can simply access this on the client.  First let’s define a page to set this value.  in MyFirstPage.xaml… let’s add some UI:

  1. <StackPanel Orientation="Horizontal" >
  2.     <TextBlock Text="Enter background color: "/>
  3.     <TextBox x:Name="colorTextBox" KeyDown="colorTextBox_KeyDown" Width="100" />
  4.     <Button Content="Save" Click="Button_Click" />
  5. </StackPanel>
  6. <TextBlock x:Name="saveStatus"/>

 

We can handle the save button click as follows..

  1. private void Button_Click(object sender, RoutedEventArgs e)
  2. {
  3.     string colorString = this.colorTextBox.Text.Trim().ToLower();
  4.     colorString = colorString.Substring(0, 1).ToUpper() + colorString.Substring(1, colorString.Length - 1);
  5.     RiaContext.Current.User.PageBackgroundColor = colorString;
  6.     this.saveStatus.Text = "setting saving..";
  7.     RiaContext.Current.Authentication.SaveUser((o) =>
  8.                 { this.saveStatus.Text = "setting saved"; },
  9.       null);
  10. }
  11.  
  12. private void colorTextBox_KeyDown(object sender, KeyEventArgs e)
  13. {
  14.     this.saveStatus.Text = "";
  15. }

Notice in lines 3-4 we are normalizing the string name of the color so that it is “xaml compliant”..
Then in line 5 we are setting the strongly typed User.PageBackgroundColor property. 
Then in lines 6-9 we are simply giving some UI hints as we save this value back to the server. 

Of course this will only work if the user is logged in first, so this time, let’s be proactice and encourage the user to log in when they hit the page for the first time.

  1. protected override void OnNavigatedTo(NavigationEventArgs e)
  2. {
  3.     if (!RiaContext.Current.User.IsAuthenticated)
  4.     {
  5.         new LoginWindow().Show();
  6.     }
  7. }

 

The last step here is the honor this value when it is set.  That turns out to be pretty easy in this case.  Just go to MainPage.Xaml and databind the LayoutRoot’s backgroun color to this value.  

  1. <Grid x:Name="LayoutRoot" Style="{StaticResource LayoutRootGridStyle}"
  2.       DataContext="{StaticResource RiaContext}"
  3.       Background="{Binding Path=User.PageBackgroundColor}">

 

image

then when we log in…

image

And if we change the color to blue…

image

And notice the color change effects the whole app. 

image

And if I hit the app from a different machine, on a different browser, my setting still carries forward… We start off not logged in we get the default:

image

but when we log in… our settings show up.

image

Now this is a user specific setting, so if I create a new user “Glenn” and set his background color to pink

image

that doesn’t effect the background color for Darb…

image image

 

OK, background color is fun and all, but what might be even more useful is to store some state on how I last left the application.   This ensures that as I access the application from over time, the context of my work is preserved.  

So, let’s add a few more fields to our profile.. 

  1. <profile enabled="true" >
  2.   <properties>
  3.     <add name="PageBackgroundColor"  defaultValue="White"/>
  4.     <add name="SortOrder"  type="Int32" defaultValue="0"/>
  5.     <add name="SortProperty"  defaultValue="Name"/>
  6.     <add name="OriginFilter"  defaultValue=""/>
  7.   </properties>
  8. </profile>

then update the User class to make this strongly typed.

  1. public class User : UserBase
  2. {
  3.     public string PageBackgroundColor { get; set; }
  4.     public int SortOrder { get; set; }
  5.     public string SortProperty { get; set; }
  6.     public string OriginFilter { get; set; }
  7. }

 

We need to set the UI based on the user’s settings.

  1. void LoadUserState()
  2. {
  3.     var user = RiaContext.Current.User;
  4.     if (user.OriginFilter != null)
  5.         originFilterBox.Text = user.OriginFilter;
  6.     else
  7.         originFilterBox.Text = string.Empty;
  8.  
  9.     if (user.SortProperty != null)
  10.     {
  11.         dds.SortDescriptors.Add(new SortDescriptor(user.SortProperty,
  12.                                 (SortDirection)user.SortOrder));
  13.     }
  14. }

And we need to call that when the page is navigated to…

  1. protected override void OnNavigatedTo(NavigationEventArgs e)
  2. {
  3.     LoadUserState();

and when the user logs on.

  1. RiaContext.Current.Authentication.LoggedIn += (s, e) =>
  2. {
  3.     User user = RiaContext.Current.User;
  4.     if (dds != null)
  5.     {
  6.         dds.Load();
  7.         LoadUserState();
  8.     }
  9. };

 

Next we need to store the values back to the server at the right time.  This SaveUserState method plucks the right values out of the UI, and saves them the server if the values have changed. 

  1. string lastSave;
  2. void SaveUserState()
  3. {
  4.     User user = RiaContext.Current.User;
  5.     if (!user.IsAuthenticated) return;
  6.  
  7.     var order = dds.SortDescriptors.LastOrDefault();
  8.     if (order != null)
  9.     {
  10.         user.SortProperty = order.PropertyPath.Value.ToString();
  11.         user.SortOrder = (int)order.Direction;
  12.     }
  13.     user.OriginFilter = this.originFilterBox.Text;
  14.  
  15.     if (lastSave != user.SortProperty + user.SortOrder + user.OriginFilter)
  16.     {
  17.          RiaContext.Current.Authentication.SaveUser();
  18.          lastSave = user.SortProperty + user.SortOrder + user.OriginFilter;
  19.     }
  20.  
  21. }

 

We need to call this method when the user navigates away.

  1. protected override void OnNavigatedFrom(NavigationEventArgs e)
  2. {
  3.     SaveUserState();
  4. }

and, periodically we check to see if we need to save the changes back to the server.  So we set this up in the forms constructor..

  1. Timer = new DispatcherTimer();
  2. Timer.Interval = TimeSpan.FromSeconds(10);
  3. Timer.Tick += (o, e) => SaveUserState();
  4. Timer.Start();

 

Now, when we run it..   setup some sort order and a filter

image

then log out

image

 

log back in (from a different machine) and we see it is back just where we left off.

image

 

What we saw in this section what how to personalize the user experience based on the user preferences. 

 

Admin UI

In this last section, let’s look at how to build out an admin UI.. What we want to do is provide a page that allows Admins to see all the users and edit their profile settings.

First, let’s go into the AuthenticationService and add some custom methods to return all the users.  We should be sure that only users in the Admin role can access this service. 

  1.     [EnableClientAccess]

  2.     public class AuthenticationService : AuthenticationBase<User>

  3.     {

  4.         [RequiresRoles("Admin")]

  5.         public IEnumerable<User> GetAllUsers()

  6.         {

  7.             return Membership.GetAllUsers().Cast<MembershipUser>().Select(mu => this.GetUserForMembershipUser(mu));

  8.         }

  9.  

  10.         private User GetUserForMembershipUser(MembershipUser membershipUser)

  11.         {

  12.             return this.GetAuthenticatedUser(

  13.                 new GenericPrincipal(new GenericIdentity(membershipUser.UserName), new string[0]));

  14.         }

 

Now, lets add some Silverlight UI to consume this.  We will create a new page called “Admin”.   The first thing we want to do is to prompt the user to log in if they are not already logged in as a user in the admin role.

  1. protected override void OnNavigatedTo(NavigationEventArgs e)
  2. {
  3.     if (!RiaContext.Current.User.Roles.Contains("Admin"))
  4.     {
  5.         new LoginWindow().Show();
  6.         RiaContext.Current.Authentication.LoggedIn += (s, ev) =>
  7.         {
  8.             if (dds != null) dds.Load();
  9.         };
  10.     }
  11. }

 

Next, we define a DomainDataSource for accessing  the AuthenticationService

  1. <riaControls:DomainDataSource x:Name="dds"
  2.         AutoLoad="True"
  3.         QueryName="GetAllUsersQuery"
  4.         LoadSize="20">
  5.  
  6.     <riaControls:DomainDataSource.DomainContext>
  7.         <App:AuthenticationContext/>
  8.     </riaControls:DomainDataSource.DomainContext>
  9.  
  10. </riaControls:DomainDataSource>

then we define some simple UI for working with the data..

  1. <activity:Activity IsActive="{Binding IsBusy, ElementName=dds}">
  2.     <StackPanel>
  3.         <dataControls:DataForm x:Name="dataForm1"
  4.                                Height="393" Width="331"
  5.            VerticalAlignment="Top"       
  6.            Header="User Data"
  7.            ItemsSource="{Binding Data, ElementName=dds}"                     
  8.            HorizontalAlignment="Left" >
  9.         </dataControls:DataForm>
  10.  
  11.         <StackPanel Orientation="Horizontal" Margin="0,5,0,0">
  12.             <Button Content="Submit" Width="105" Height="28"
  13.               Click="SubmitButton_Click" />
  14.  
  15.         </StackPanel>
  16.  
  17.     </StackPanel>
  18. </activity:Activity>

Now, we run it..  log in but it doesn’t give us any data… why?

image

Well, the user we created is not an Admin.  To make them a Admin, go to the Web Admin tool and add them to the “Admin” role.

image

Select “Security”

image

Then under Roles, Add a new role for “Admin”

image

and under Users, “Manager User”… here you can easily add your user to the role. 

image

Now when I log on and go to the Admin page, I can access the all the user’s settings.

image

What we saw in this section was how to build an admin UI for your applications. 

 

I hope you found this to be a helpful walkthrough of the authentication and personalization support in RIA Services.   Again, you can download the full demo files or check out the full series here.

Enjoy.

Comments

  • Anonymous
    October 06, 2009
    Again a very helpful tutorial! Just like all the others.. keep on the good work!

  • Anonymous
    October 06, 2009
    Hi Brad, I am excited to go through the tutorial series to start off with SL. While viewing the demo of the running app, I found a err and I thought of mentioning it. Pager is displaying wrong, actually total pages are '8' not '7'. Please correct me if am wrong. Thanks, Sundeep

  • Anonymous
    October 06, 2009
    Thanks Brad - wonderful as usual.  I was just trying to figure out how to do this kind of thing.  I'm thrilled to see RIA Services is offering me a solution. Could you elaborate a bit on what this is doing for me under the covers?  This appears to be using Forms Auth hosted in ASP.NET so apparently there's a client side cookie being managed here - yes? I'm looking for a solution for a simple SL client that I intend to host on a public domain with a shared ASP.NET hosting provider.  This Forms based auth seems like it's exactly what I want.  However, if this were an intranet application, I'm guessing I can flip this over to use Windows Auth and Active Directory ala a standard ASP.NET Web App? Thanks Brad -kelly

  • Anonymous
    October 08, 2009
    Hi Brad Thanks fort his great Tut. *****  Need your kind Advice ********* I have made like the tutorial shows an silverlight Aplication. I connect it with a local MSSQL Server at my Lan Network. So far it runs fine; I can see the Skins, can load the Data and update them. But now I Transferred my web Project to a Web server, which has also a MSSQL Database, the problem is, I can see the Skins and empty Grid, but it don’t load any Data’s. I have checked the Connection String several times, but my updated Connection string is correct. Please could you tell me, whats wrong? Is this a Ria bug? Please be so nice and answer me soon as possible. J.Akhtar

  • Anonymous
    October 15, 2009
    Hi Brad I solved the Problem. Ria do run on a webserver fine, everytging works, the Problem I had was just i needed to give my Application Folder full trust. I Love RIA Services!!!!

  • Anonymous
    October 16, 2009
    great post about RIA Services, thanks!

  • Anonymous
    October 16, 2009
    The comment has been removed

  • Anonymous
    October 16, 2009
    The problem is have a newer version of Sql Express.. .the best way I have found to upgrade is to use WebPI and get a new version of Sql Express, http://www.microsoft.com/web/downloads/platform.aspx

  • Anonymous
    October 20, 2009
    In your page about authentication the following link "video of the full session" does not work can you upadate the link

  • Anonymous
    October 20, 2009
    In your page about authentication the following link "video of the full session" does not work can you upadate the link

  • Anonymous
    October 21, 2009
    I need SQL on other serwer so I changed AspNetSqlProvider to  SqlProvider in web.config    <membership defaultProvider="SqlProvider" userIsOnlineTimeWindow="20">      <providers>        <remove name="AspNetSqlProvider" />        <add name="SqlProvider"          type="System.Web.Security.SqlMembershipProvider"          connectionStringName="SqlServices"          enablePasswordRetrieval="false"          enablePasswordReset="true"          requiresQuestionAndAnswer="true"          passwordFormat="Hashed"          applicationName="/" />      </providers>    </membership>  <connectionStrings>    <add name="NORTHWNDEntities"         connectionString="metadata=res:///Northwind.csdl|res:///Northwind.ssdl|res://*/Northwind.msl;provider=System.Data.SqlClient;provider connection string=&quot;Data Source=vfksql2008;Initial Catalog=NORTHWND;Integrated Security=True&quot;" providerName="System.Data.EntityClient" />    <add name="SqlServices" connectionString="Data Source=vfksql2008;Initial Catalog=ASPNETDB;Integrated Security=True" />  </connectionStrings> On SQL2008 Developer Edition during login I've got an error 'Unknown error: SQL error 26 Cannot locate server' but there was no problem with adding new user. To pass it I overrided in AuthenticationService: protected override User GetAuthenticatedUser(IPrincipal principal) {            User autUsr = new User()                {                    Name = principal.Identity.Name,                    AuthenticationType = principal.Identity.AuthenticationType                };            return autUsr; } and I can login now, but only with proper login and password from ASPNETDB. But when I try change the body of GetAuthenticatedUser to {return base.GetAuthenticatedUser(principal);} in SQLProfiler i've got query: exec dbo.aspnet_Membership_GetPasswordWithFormat @ApplicationName=N'/',@UserName=N'pb1' ,@UpdateLastLoginActivityDate=1, @CurrentTimeUtc='2009-10-22 10:19:02.4970000' and executing this query in MSSQL SMS I've got error: 'Procedure aspnet_Membership_GetPasswordWithFormat, Line 0 Error converting data type varchar to datetime.' When I change parameter to @CurrentTimeUtc='2009-10-22 10:19:02.497' (no trailing zeros)  it executes without errors. What is wrong with my SQL or SQLProvider?

  • Anonymous
    October 21, 2009
    I've read that is a difference in handling datetime in SQL2008 (datetime2) between 'SQL Native Client' and 'SQL Server Native Client 10.0' (for SQL2008). How to change .NET to using old SQL2005 client?

  • Anonymous
    October 25, 2009
    Hi Brad, I am attempting to add an extra field to the Registration details in the LoginWindow.xaml. Whenever I add an extra item to the Edit Template stackpanel, I lose the OK and Cancel buttons from the bottom of the register form when I click "Register".  I have trawled through the code and I can't see why this would happen, all heights are set to Auto. You mention that "if you want to customize the look and feel of any of these dialogs, it is easy to do by looking in the ViewsLoginControl.xaml, ViewsLoginWindow.xaml." but I am not finding it easy!  Have you managed to do this? Please could you help. I realise there's probably something obvious I'm missing! Many thanks

  • Anonymous
    November 04, 2009
    Thanks Brad.  This is an awesome tutorial and, like the reader above was saying, it's exactly what I've been trying to do... well, almost.  I'm trying to figure out how to go between navigateable child windows while fully saving the current window's context prior to navigating away from a window.   The LoadUser/SaveUser is great for persisting data to a database (via the user profile) such that upon subsequent logins the state can be restored. But what if I just want to save state as the user changes windows?  What must I do such that as the user clicks on the links to go from window A to window B and then back again to window A that the window control is right back where I was prior to clicking on the link to go to window B (for example, let's say on window A there's a master/detail with a pager control and the user has paged to page 10 of 100 and has selected the third data item of 5 that are displayed)?  Also, is it possible to do this such that there's no database requerying (the network roundtrip to the database has already been made... why do so again?... let's keep the DBAs happy by minimizing network roundtrips). LOB applications consisting of many links would potentially benefit from a "state capture" such as the one described that could potentially grow to be quite large (if the data being retrieved by each window is substantial).  I would image using the Silverlight operational store as an option.

  • Anonymous
    November 05, 2009
    Hi Brad, i am trying to hide some controls after user logged in. i made converter class like in this article. but it is not working.. i checked value of "{Binding Path=User.IsAuthenticated, Converter={StaticResource VisibilityConverter}}" on textblock, it's returning True  or False only value is not converting to 'Visible' or 'Collapsed'. what could be the problem?

  • Anonymous
    November 09, 2009
    The comment has been removed

  • Anonymous
    November 15, 2009
    Do you have a copy of the project in vb.net?