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.
While I was presenting a workshop on Silverlight at the DevConnections conference in Orlando last March, an audience member asked me a question about 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 usernames and roles. I'll walk through the sample application in this article and highlight the key components.
The goal of the article isn't to dictate how to authenticate users, since every application has unique requirements. However, I will discuss general techniques for accessing usernames and working with roles to block access to views and show or hide controls.
Working with Security
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 Windows Communication Foundation (WCF) RIA Services on the back end, you'll need to write the plumbing code to authenticate a user if you need to do so 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.
WCF RIA Services also provides a means for accessing an authenticated user's username and roles by using a WebContext object. 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 handling data exchange and security tasks. If you won't be using WCF RIA Services, then this article 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 article assumes that authentication occurs at the page level as opposed to within the Silverlight application itself.
Accessing the Username
To access an authenticated user's username within a Silverlight application, you can either pass the username into the object tag's initialization parameter (called "initParams") or call a service that returns the username. Following is an example of passing in the username using the initParams option within an ASP.NET page that is hosting the object tag.
Within App.xaml.cs you can access the initParams parameters and store them. The code in Figure 1 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(IDictionaryinitParams) { if (initParams != null) { foreach (var item in initParams) { this.Resources.Add(item.Key, item.Value); } } }
My own preference is not to embed the username into the object tag unless it's merely going to be displayed in the application. If you'll be doing lookups against different databases or other resources based on the username, it's better to let a service resolve the username dynamically so that it can't be spoofed. Otherwise, a user who authenticated into the application could potentially change the username defined in initParams and bypass security.
To access the username using a service, you can create a WCF security service as shown in Figure 2 and add an operation that is responsible for returning the username. The easiest way to create the service is to add a Silverlight-enabled WCF service into the web project.
[ServiceContract(Namespace = "")] [SilverlightFaultBehavior] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class SecurityService { [OperationContract] public string GetLoggedInUserName() { return new SecurityRepository().GetUserName(OperationContext.Current); } [OperationContract] public ListGetRoles() { return new SecurityRepository().GetRoles(); } [OperationContract] public UserSecurity GetUserSecurity() { return new SecurityRepository().GetUserSecurity(OperationContext.Current); } }
The codein Figure 2 contains a GetLoggedInUserName() operation that makes a call into a SecurityRepository class's GetUserName() method to access the username. GetUserName() accesses the username 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.)
Figure 3 shows the code in 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 another store.
public class SecurityRepository { //Simulate roles List_Roles = new List (); public SecurityRepository() { _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 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 username 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.
Creating a SecurityManager for Silverlight
The WCF service shown in Figure 2 provides a way for a Silverlight application to retrieve a username 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 username 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, you make a single class responsible for security, which avoids scattering security logic throughout an application. Figure 4 contains the code for the SecurityManager class available with the sample application.
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 username 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 whether the user is a member of an array of roles, UserIsInRole() to check whether 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 whether a user is in a role specific to the application.
Using the SecurityManager Class
The SecurityManager class can be used directly in views or within ViewModel classes. When using the MVVM pattern, you can add a property 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—named MainPageViewModel and HomeViewModel—that use SecurityManager. MainPageViewModel uses the SecurityManager class to render the username in the MainPage.xaml view and hide any HyperlinkButton controls that a user shouldn't be able to see. Figure 5 shows the complete code for MainPageViewModel.
public class MainPageViewModel : ViewModelBase { private bool _IsAdmin; private string _UserName; public MainPageViewModel() { if (!IsDesignTime) SecurityManager.UserSecurityLoaded += SecurityManagerUserSecurityLoaded; } public bool IsAdmin { get { return _IsAdmin; } set { if (_IsAdmin != value) { _IsAdmin = value; OnNotifyPropertyChanged("IsAdmin"); } } } public string UserName { get { return _UserName; } 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 whether or not the user is in the Admin role. 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 username in the interface.
HomeViewModel uses the SecurityManager class to determine whether or not edit controls that allow customer information to be saved and edited should be present. If the user is in the Admin or Editor role, then the controls are shown. If not, the controls are hidden.
Securing Individual Views
Although accessing username and role functionality is important in order to customize the user interface based on the user's security rights, in many cases, you'll also need to secure individual views. For example, the MainPageViewModel defines an IsAdmin property (shown inFigure 5) 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, bypassing the intended security. To prevent this, the CheckUserAccessToUri() method in the SecurityManager class (see Figure 4) 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).
Figure 6 shows the code that handles checking when 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 whether the user is allowed to get to the view that the content Frame is attempting to load. If the user doesn't gain access, a view named AccessDenied.xaml is loaded, which displays the appropriate Access Denied error message.
void MainPage_Loaded(object sender, RoutedEventArgs e) { ViewModel = (MainPageViewModel)this.Resources["ViewModel"]; ViewModel.SecurityManager.UserSecurityLoaded += SecurityManagerUserSecurityLoaded; ContentFrame.Navigating += ContentFrame_Navigating; } 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 username or roles found if (!ViewModel.SecurityManager.IsValidUser) { e.Cancel = true; return; } //Check if user has access to page that they're trying to navigate to var hasAccess = ViewModel.SecurityManager.CheckUserAccessToUri(e.Uri); if (!hasAccess) { ContentFrame.Content = new AccessDenied(); e.Cancel = true; } }
Security Is Crucial
Security is an important part of LOB applications and something that definitely must be thought through and planned carefully. In this article you've seen different ways to access a username 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 of this article, thanks for asking the question, and I hope the sample application helps get you started in the right direction integrating security features into your Silverlight applications.