Working with User Names and Roles in Silverlight Applications
Security is a key component of applications and something that developers often struggle with to get right. How do you authenticate a user? How do you integrate roles and use them to show or hide different parts of a screen? These and other questions commonly come up as I talk with developers working on ASP.NET and Silverlight applications.
I was recently presenting a workshop on Silverlight at the DevConnections conference in Orlando and had a question from the audience on how I handle security roles in Silverlight applications. Since I had just implemented a security mechanism for a customer I gave a brief response but didn’t have a sample application available to share to point people in the right direction. After the workshop was over I put together a sample application to demonstrate one potential approach for accessing user names and roles. I’ll walk through the sample application in this post and highlight the key components.
The goal of the post isn’t to dictate how to authenticate users since every application has unique requirements. However, I will discuss general techniques for accessing user names and working with roles to block access to views and show or hide controls.
Security Techniques
Silverlight applications can take advantage of Windows and Forms authentication techniques and can integrate user roles into the mix as well. However, unless you use WCF RIA Services on the backend you’ll need to write the plumbing code to authenticate a user if you need to do it directly within the application. WCF RIA Services projects provide login and registration screens out of the box that leverage Forms authentication by default. You can view a walk-through of the WCF RIA Services authentication process here http://msdn.microsoft.com/en-us/library/ee942449(VS.91).aspx.
WCF RIA Services also provides a means for accessing an authenticated user’s user name and roles by using a WebContext object (see http://msdn.microsoft.com/en-us/library/ee707361(VS.91).aspx). This isn’t possible out-of-the-box in a standard Silverlight application unless you write custom code to handle it. If WCF RIA Services is appropriate for your project then it’s a great way to go for data exchange and security tasks. If you won’t be using WCF RIA Services then this post will provide insight into other techniques that can be used.
Most of the Silverlight Line of Business (LOB) applications I’ve worked on authenticate the user at the page level using Windows authentication. If the user can’t authenticate into the page then the Silverlight application is never displayed. With out-of-browser applications the Windows user account can be passed through and accessed as calls to a service are made. The sample application available with this post assumes that authentication occurs at the page level as opposed to within the Silverlight application itself.
Accessing a User’s Identity
To access an authenticated user’s user name within a Silverlight application you can either pass the user name into the object tag’s initialization parameter (called “initParams”) or call a service that returns the user name. An example of passing in the user name using the initParams option within an ASP.NET page that is hosting the object tag is shown next:
<param name="initParams" value="UserName=<%=User.Identity.Name%>" />
Within App.xaml.cs you can access the initParams parameters and store them. The code below shows how to do this and add initParams values into the application resources so that they can be accessed throughout the application.
private void Application_Startup(object sender, StartupEventArgs e) { ProcessInitParams(e.InitParams); this.RootVisual = new MainPage(); } private void ProcessInitParams(IDictionary<string, string> initParams) { if (initParams != null) { foreach (var item in initParams) { this.Resources.Add(item.Key, item.Value); } } }
I don’t personally like to embed the user name into the object tag unless it’s simply going to be displayed in the application. If you’ll be doing look-ups against different databases or other resources based upon the user name then it’s better to let a service resolve the user name dynamically so that it can’t be spoofed. Otherwise, a user that authenticated into the application could potentially change the user name defined in initParams and bypass security.
To access the user name using a service you can create a WCF security service as shown next and add an operation that is responsible for returning the user name. The easiest way to create the service is to add a Silverlight-enabled WCF Service into the Web project.
[ServiceContract(Namespace = "YourNamespace")] [SilverlightFaultBehavior] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class SecurityService { [OperationContract] public string GetLoggedInUserName() { return new SecurityRepository().GetUserName(OperationContext.Current); } [OperationContract] public List<Role> GetRoles() { return new SecurityRepository().GetRoles(); } [OperationContract] public UserSecurity GetUserSecurity() { return new SecurityRepository().GetUserSecurity(OperationContext.Current); } }
The previous code contains a GetLoggedInUserName() operation that makes a call into a SecurityRepository class’s GetUserName() method to access the user name. GetUserName() accesses the user name through the OperationContext object’s ServiceSecurityContext property which provides access to the user’s identity object (note that specific configuration changes must be made for this object to be useful – see the sample project’s web.config file for more details).
The following code shows the SecurityRepository class. The class simulates roles by adding them directly into the code but could easily be enhanced to retrieve roles from a database or other store.
public class SecurityRepository { List<Role> _Roles = new List<Role>(); public SecurityRepository() {
//Simulate roles
_Roles.Add(new Role { Name = "Admin" }); _Roles.Add(new Role { Name = "Editor" }); _Roles.Add(new Role { Name = "User" }); } public string GetUserName(OperationContext opContext) { return GetOpContextUserName(opContext); } public List<Role> GetRoles() { return _Roles; } public UserSecurity GetUserSecurity(OperationContext opContext) { var userName = GetOpContextUserName(opContext); if (userName != null) { return new UserSecurity { UserName = userName, Roles = _Roles }; } return null; } private string GetOpContextUserName(OperationContext opContext) { return (opContext.ServiceSecurityContext != null && opContext.ServiceSecurityContext.WindowsIdentity != null) ? opContext.ServiceSecurityContext.WindowsIdentity.Name : null; } }
The GetUserSecurity() method provides a way for the Silverlight application to make a single call and get the user name and roles for the authenticated user. This method is called by the Silverlight client and used to show and hide controls within the application. Let’s take a look at how that process works.
Building a SecurityManager for Silverlight
The WCF service shown earlier provides a way for a Silverlight application to retrieve a user name and roles. How do you go about calling the service and storing the resulting information? For a recent customer application I created a SecurityManager class that was responsible for storing user name and role information and exposing properties such as IsAdmin and IsEditor to handle determining what role a user was in. It went through a service agent class that was responsible for calling the WCF service and returning the data to the SecurityManager. By going this route a single class is responsible for security which avoids scattering security logic throughout an application. The following code shows the SecurityManager class available with the sample application.
public class SecurityManager : ISecurityManager { public ISecurityServiceAgent SecurityServiceAgent { get; set; } public event EventHandler UserSecurityLoaded; public SecurityManager() { SecurityServiceAgent = new SecurityServiceAgent(); GetUserSecurityDetails(); } public void OnUserRolesLoaded(object sender, EventArgs e) { if (UserSecurityLoaded != null) { UserSecurityLoaded(sender, e); } } #region Properties public ObservableCollection<Role> UserRoles { get; set; } public string UserName { get; set; } public bool IsAdmin { get { if (UserRoles == null) return false; return UserIsInAnyRole("Admin"); } } public bool IsInUserRole { get { if (UserRoles == null) return false; return (UserRoles.Count == 1 && UserRoles.Any(r => r.Name == "User")); } } public bool IsValidUser { get { return UserName != null && (UserRoles != null && UserRoles.Count > 0); } } public bool IsEditor { get { if (UserRoles == null) return false; return IsAdmin || UserIsInAnyRole("Editor", "HRAdmin"); } } public bool IsUserSecurityLoadComplete { get; set; } #endregion private void GetUserSecurityDetails() { SecurityServiceAgent.CallService<GetUserSecurityCompletedEventArgs>( (s, args) => { IsUserSecurityLoadComplete = true; UserRoles = args.Result.Roles; UserName = args.Result.UserName; OnUserRolesLoaded(this, EventArgs.Empty); }); } //Determine if a user has rights to see an application view or not public bool CheckUserAccessToUri(Uri uri) { if (UserRoles != null) { string screenUri = uri.ToString().ToLower(); if (screenUri.StartsWith("/home")) return IsValidUser; if (screenUri.StartsWith("/customers")) return IsAdmin; if (screenUri.StartsWith("/about")) return IsValidUser; } return false; } public bool UserIsInRole(string role) { if (UserRoles == null) return false; return UserRoles.Any(r => r.Name == role); } public bool UserIsInAnyRole(params string[] roles) { if (UserRoles == null) return false; return (from ur in UserRoles from r in roles where ur.Name.Contains(r) select r).Any(); } }
The key part of the SecurityManager class is found in the constructor where a call is made to another class named SecurityServiceAgent (a “service agent” that specializes in data retrieval) to retrieve the user name and roles from the WCF security service shown earlier. Since the service call is asynchronous an event is defined in SecurityManager named UserSecurityLoaded that is raised once the data is loaded in the Silverlight client.[
In addition to getting and storing user data, the SecurityManager class also has methods such as UserIsInAnyRole() to check if the user is a member of an array of roles, UserIsInRole() to check if they’re in a specific role and CheckUserAccessToUri() to verify whether or not they have access to a specific view. Properties such as IsAdmin and IsEditor provide a simple way for consumers of the SecurityManager class to check if a user is in a role specific to the application.
Using the SecurityManager to Handle Roles and User Names
The SecurityManager class can be used directly in views or within ViewModel classes. When using the MVVM pattern a property can be added into a ViewModel base class (a class that all ViewModel classes derive from) as shown next:
public ISecurityManager SecurityManager { get; set; }
This property allows security functionality to be available across all ViewModel classes. The sample application contains two ViewModel classes that use SecurityManager named MainPageViewModel and HomeViewModel. MainPageViewModel uses the SecurityManager class to render the user name in the MainPage.xaml view and hide any HyperlinkButton controls that a user shouldn’t be able to see. The following code shows the MainPageViewModel class.
public class MainPageViewModel : ViewModelBase { private bool _IsAdmin; private string _UserName; public MainPageViewModel() { if (!IsDesignTime) SecurityManager.UserSecurityLoaded += SecurityManagerUserSecurityLoaded; } public bool IsAdmin { get { return _IsAdmin; } private set { if (_IsAdmin != value) { _IsAdmin = value; OnNotifyPropertyChanged("IsAdmin"); } } } public string UserName { get { return _UserName; } private set { if (_UserName != value) { _UserName = value; OnNotifyPropertyChanged("UserName"); } } } void SecurityManagerUserSecurityLoaded(object sender, EventArgs e) { IsAdmin = SecurityManager.IsAdmin; UserName = SecurityManager.UserName; } }
MainPageViewModel starts by attaching to the SecurityManager’s UserSecurityLoaded event. Once the event fires, SecurityManagerUserSecurityLoaded is called and the IsAdmin and UserName properties are assigned to the ViewModel’s properties. These properties are then bound to controls in the view using standard Silverlight data binding techniques. The IsAdmin property is bound to HyperlinkButton controls and used to show or hide the controls based on if the user is in the “Admin” role or not. A value converter is used in the view to handle converting the Boolean value to a Visibility enumeration value. The UserName property is bound to a TextBlock control that displays the user name in the interface.
HomeViewModel uses the SecurityManager class to determine if edit controls that allow customer information to be saved and edited should be present or not. If the user is in the Admin or Editor role then the controls are shown. If not, the controls are hidden.
Securing Views
Although accessing user name and role functionality is important in order to customize the user interface based upon the user’s security rights, you’ll also need to secure individual views in many cases. For example, the MainPageViewModel defines an IsAdmin property (shown previously) that is used to show or hide a HyperlinkButton to prevent a user from going to a specific view. However, if the user knows the path to the view they can type it directly into the browser’s URL and load the view directly which bypasses the intended security. To prevent this, the CheckUserAccessToUri() method in the SecurityManager class can be used in conjunction with the Navigating event of the Frame within MainPage.xaml (the Frame control is included since the sample project uses the Silverlight navigation application project template).
The following snippet shows the code that handles checking if a user has access to a specific view as the Frame in MainPage.xaml loads content. The code shown in MainPage_Loaded handles attaching to the Frame’s Navigating event. When the event is raised the code in the ContentFrame_Navigating event handler cancels the Navigating event if the user isn’t determined to be a valid user. It also makes the call to the CheckUserAccessToUri() method to determine if the user is allowed to get to the view that the content Frame is attempting to load. If the user doesn’t access, a view named AccessDenied.xaml is loaded which displays the appropriate “Access Denied” error message.
void MainPage_Loaded(object sender, RoutedEventArgs e) { PeopleEventBus.OperationCompleted += PeopleEventBus_OperationCompleted; ViewModel = (MainPageViewModel)this.Resources["ViewModel"]; ViewModel.SecurityManager.UserSecurityLoaded += SecurityManagerUserSecurityLoaded; ContentFrame.Navigating += ContentFrame_Navigating; ContentFrame.Navigated += ContentFrame_Navigated; } void SecurityManagerUserSecurityLoaded(object sender, EventArgs e) { //Cause frame to navigate to view user originally wanted to see ViewModel.SecurityManager.UserSecurityLoaded -= SecurityManagerUserSecurityLoaded; ContentFrame.Navigate(ContentFrame.Source); } private void ContentFrame_Navigating(object sender, NavigatingCancelEventArgs e) { //No user name or roles found if (!ViewModel.SecurityManager.IsValidUser) { e.Cancel = true; return; } //Check if user has access to page that they're trying to nagivate to var hasAccess = ViewModel.SecurityManager.CheckUserAccessToUri(e.Uri); if (!hasAccess) { ContentFrame.Content = new AccessDenied(); e.Cancel = true; } }
Security is an important part of Line of Business (LOB) applications and something that definitely must be thought through and planned carefully. In this post you’ve seen different ways to access a user name and associated roles in a Silverlight application. You’ve also seen how a SecurityManager class can be created to perform security checks that are used by ViewModel classes to show or hide controls. To the person in the DevConnections workshop mentioned at the beginning, thanks for asking the question and I hope the sample application (available below) helps get you started in the right direction integrating security features into your Silverlight applications.
Download the sample application code here.