Recipe: Implementing Role-Based Security with ASP.NET 2.0 using Windows Authentication and SQL Server
Problem
You are building an Intranet expense report application for your organization, and want to enable role-based authentication and authorization capabilities within it. Specifically, you want to create logical roles called “approvers”, “auditors”, and “administrators” for the application, and grant/deny end-users access to functionality within the application based on whether they are in these roles.
Because your application is an Intranet solution, you want to use Windows Authentication to login the users accessing the application (avoiding them having to manually login). However, because the roles you want to define are specific to your application, you do not want to define or store them within your network’s Windows Active Directory. Instead, you want to define and store these roles within a database. You then want to map Windows user accounts stored within Active Directory to these roles, and grant/deny access within the application based on them.
In addition to using roles to authorize access to individual pages within the application, you want to dynamically filter the links displayed within the site’s menu navigation based on whether users have permissions (or not) to those links. And lastly, you want to build-in a custom role-management administration UI directly within the expense report application for “expense app administrators” to manage these roles and control who has access to the capabilities of the app:
The below post walks through step-by-step how to implement all of this. You can download and run the completed sample we’ll build below here.
Discussion
ASP.NET provides a flexible authentication management system that allows you to easily take advantage of Windows authentication to identify “who” the authenticated user accessing your site is. You can learn how to enable this and how it works by reviewing my previous Recipe: Enabling Windows Authentication within an Intranet ASP.NET Web Application.
Most web applications typically have at least dozens (if not thousands or millions) of authenticated users accessing them. Authorizing access to pages or functionality within a site is difficult (if not impossible) when managing it on an individual user account level. Instead, it is much better for the application developer to define higher level “roles” to map users into, and then grant/deny access or permissions based on these roles.
For example: if we were building an internal expense reporting application, we might want to create three roles for the application: “approvers”, “auditors” and “administrators”. We would then only allow users in the “approvers” role to access the portion of the site that enables someone to approve employee expenses. We would only allow users in the “auditors” role to access the portion of the site that generates reports and analysis on employee spending. And we would only allow the select few users in the “administrators” role to have access to the admin pages of the application.
By coding against roles like “approvers”, “auditors” and “administrators”, as opposed to individual account names, we can have very clean code within our application, and make the application very flexible as we add/remove users to the system and change their permissions over time. For example, if “Joe” gets promoted to be a manager with expense approver permissions, all that needs to happen for him to get access to the approver portion of the expense app is for the administrator to go in and add him into the “approvers” role – no code changes or configuration changes to the app need to be made.
ASP.NET supports multiple places where user to role mappings can be stored and defined. When Windows Authentication is enabled, ASP.NET will by default use the Active Directory user/group mappings to support role access permission checks. This is useful when the permission checks you want to perform are global to your company environment.
For many applications, though, you might want to implement more local role policies – where the roles you define are specific to the application. In cases like these you either might not want to store these in a central Active Directory store, or your network administrator might not even allow it. Instead, you might want to store the role definitions and user mappings locally within a database – while still using Windows Authentication to identify and login the users stored within them.
The below walkthrough demonstrates how to-do this, and builds a complete end-to-end application to illustrate how all the pieces fit together.
Solution
Step1: Create a New Web Site
Use Visual Studio or Visual Web Developer (which is free) to create a new ASP.NET 2.0 web site/project.
We'll start by creating a new external CSS stylesheet called “Stylesheet.css”. We'll use this to style the application consistently. Add the below CSS rules to it:
body {
font-family:Arial;
}
.loginname {
font-size:small;
color:Red;
}
.menu {
float:left;
width:200px;
}
.roleList
{
margin-top:10px;
margin-bottom:10px;
}
We'll then add a new file called “Web.SiteMap” in the top level "root" directory of the project. SiteMap files enable us to specify the site hierarchy and link structure that we want to use to organize pages within our site, and can be used to databind Menus and navigation UI against (treeviews, breadcrumb controls, etc). Within the Web.SiteMap file add this XML to define the link structure for the site we are going to build:
<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode url="default.aspx" title="Home">
<siteMapNode url="approver.aspx" title="Approver Expenses" />
<siteMapNode url="audit/auditor.aspx" title="Audit Page" />
<siteMapNode url="admin/admin.aspx" title="Admin Manager" />
</siteMapNode>
</siteMap>
Then we'll create a Master Page template called “Site.Master” at the root of the project. This will enable us to define an overall consistent layout to use for all pages on the site. Within it we'll add a Treeview control that is databound to our site navigation hierarchy that we defined above, and which will provide a hierachical menu for navigating the pages in our site. Define the contents of the Site.Master page like so:
<%@ Master Language="VB" CodeFile="Site.master.vb" Inherits="Site" %>
<html>
<head runat="server">
<title>Expense Report</title>
<link href="StyleSheet.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="form1" runat="server">
<div class="header">
<h1>
Expense Report Sample
<asp:LoginName ID="LoginName1" FormatString="(Welcome {0})" CssClass="loginname" runat="server" />
</h1>
</div>
<div class="menu">
<asp:TreeView ID="T1" DataSourceID="SiteMapDataSource1" runat="server" ImageSet="Simple" EnableViewState="False" NodeIndent="10">
<ParentNodeStyle Font-Bold="False" />
<HoverNodeStyle Font-Underline="True" ForeColor="#DD5555" />
<SelectedNodeStyle Font-Underline="True" ForeColor="#DD5555" HorizontalPadding="0px" VerticalPadding="0px" />
<NodeStyle Font-Names="Verdana" Font-Size="8pt" ForeColor="Black" HorizontalPadding="0px" NodeSpacing="0px" VerticalPadding="0px" />
</asp:TreeView>
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" />
</div>
<div class="content">
<asp:contentplaceholder id="MainContent" runat="server">
</asp:contentplaceholder>
</div>
</form>
</body>
</html>
Note: for convenience sake I’m using the “Simple” auto-format selection option within the VS designer to style the TreeView control above. One downside is that this is embedding inline styles. To enable a pure CSS styling solution for the TreeView above I would want to download and use the ASP.NET 2.0 CSS Control Adapter Toolkit.
After we define the Site.Master template, we’ll create a new page called “Default.aspx” that is based on the master-page and that we will use as the home-page for the site. Create the page like so:
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" Runat="Server">
<h3>Some typical home-page content would go here...</h3>
</asp:Content>
And then run the page. You will see a page like this:
Note that the tree-view’s site navigation links on the left-hand side of the page are databound from the ASP.NET 2.0 Site Navigation System.
Next create two subdirectories in the project called “admin” and “audit” and add pages within these sub-directories called “admin.aspx” and “auditor.aspx” respectively. Also then create a top-level page called “approver.aspx” in the root directory of the site. All of these pages should be based on the Site.Master template we defined above.
When you are done with this your project’s solution explorer will look like this:
Now we are ready to enable Windows Authentication and add Role Support to the site.
Step 2: Enable Windows Authentication
To enable Windows Authentication for our expense report web-site above, and force users to always be authenticated when visiting the application, we’ll want to open our web.config file at the root of the project and add this XML to it:
<authentication mode="Windows" />
<authorization>
<deny users="?"/>
</authorization>
To understand better what this does, please take a few moments to review my previous Recipe: Enabling Windows Authentication within an Intranet ASP.NET Web Application.
Once we’ve configured the settings above, every user to the site will automatically be validated and authenticated via our Windows authentication system on the network.
Step 3: Enable SQL based Role Management
ASP.NET 2.0 ships with built-in role manager providers that work against SQL Express, SQL Server, and Active Directory (which can also be used against ADAM stores). If you’d prefer to use your own custom database (or the file-system or your own LDAP system), you can also build your own role manager provider and easily add it to the system. This web-site details how the ASP.NET 2.0 Provider Model works, how you can build your own providers to plug-in, and enables you to download the source code for the built-in providers that ASP.NET ships with.
For the purposes of this sample we are going to use either SQL Express or SQL Server to store our role mappings. To begin with we’ll want to enable the ASP.NET role-manager in our web.config file. We’ll do this by adding this section within the <system.web> group:
<roleManager enabled="true"/>
If you have SQL Express installed on your machine, then you are done. ASP.NET will automatically provision a new SQL Express database within your app_data folder at runtime that has the appropriate Roles tables configured to persist the role mappings we’ll do. You don’t need to take any additional steps to configure this.
If you don’t have SQL Express installed, and instead want to use a SQL Server to store the Roles data, you’ll need to create a database within SQL to store the ASP.NET Application Service tables, and update your web.config file to point at the database. The good news is that this is easy to-do. If you haven’t done this before, please read my previous Configuring ASP.NET 2.0 Application Services to use SQL Recipe that demonstrates how to do this. Below is a sample configuration entry that shows how to configure the web.config file to use a SQL database:
<roleManager enabled="true" defaultProvider="SqlRoleManager">
<providers>
<clear/>
<add name="SqlRoleManager"
type="System.Web.Security.SqlRoleProvider"
connectionStringName="SqlRoleManagerConnection"
applicationName="MyApplication" />
</providers>
</roleManager>
Where the “SqlRoleManagerConnection” connection-string we referenced above is defined within the <connectionStrings> section of our web.config file like so:
<connectionStrings>
<add name="SqlRoleManagerConnection"
connectionString="Data Source=localhost;Initial Catalog=BasePageSample;Integrated Security=SSPI;">
</add>
</connectionStrings>
Important: If you explicitly declare a new provider reference like I did within the <providers> section above, you need to make sure that you specify the “applicationName” attribute. Otherwise you’ll run into this problem that I discuss in this blog post here.
And now, when we run our application again, ASP.NET will automatically authenticate the incoming users to the site using Windows Authentication, and use the SQL Database we defined above to retrieve all role mappings.
Step 4: Create Our Roles and Initial User Mappings to Them
Now that we have configured our role provider, we can use the “Roles” API (the System.Web.Security.Roles class) within ASP.NET to create roles, and manage users within them.
One tip/trick I like to use is to take advantage of the “Application_Start” event handler within Global.asax to setup my Roles if they don’t already exist, and map any initial users into them if necessary. To-do this, choose File->Add New Item and select the “Global.asax” file item. Then add this code to your Application_Start event handler:
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
If (Roles.RoleExists("Auditors") = False) Then
Roles.CreateRole("Auditors")
End If
If (Roles.RoleExists("Approvers") = False) Then
Roles.CreateRole("Approvers")
End If
If (Roles.RoleExists("Admins") = False) Then
Roles.CreateRole("Admins")
Roles.AddUserToRole("REDMOND\scottgu", "Admins")
End If
End Sub
This event handler gets called once every-time the ASP.NET application starts up. As you can see above, within it I am checking to see whether our three roles exist in the configured Roles database – and if not I create them. I also then added my Windows account into the “Admins” role that I created last. This is a good approach you can use to setup the initial admin users for your application.
Now, when we run the application again, it will automatically provision the three new roles into the database and add me into the admin one (note: in the code above I’ve only added myself to the admin role – I’m not yet in the Auditors or Approvers role).
Step 5: Authorizing Access Based On Roles
To verify that our roles were setup correctly, let’s add some authorization rules to grant/deny access to portions of the site based on them. I can do this using the “authorization” section of our web.config files.
First create new “web.config” files within both the “admin” and “audit” directories:
Then add the below XML content within the web.config file within the “admin” directory:
<?xml version="1.0"?>
<configuration>
<system.web>
<authorization>
<allow roles="Admins"/>
<deny users="*"/>
</authorization>
</system.web>
</configuration>
This tells ASP.NET to allow users within the “Admins” role to access the pages within the directory, but to block everyone else who isn’t in this role. We’ll then want to add similar content to the web.config file within the “audit” directory (which does the same logical thing as above – except in this case only allowing “Auditors” access).
For the “Approver.aspx” page that is in the root directory of the application we’ll want to-do something a little extra. Because it isn’t in a sub-directory of its own, we can’t use a global directory rule like we did above. Instead, we’ll want to restrict the rule to only apply to the “Approver.aspx” page within that directory. We can do this by specifying a <location path=”url”> directive around it within our root web.config file on the site:
<location path="Approver.aspx">
<system.web>
<authorization>
<allow roles="Approvers"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
And now when we run the application again, and try to access the “Audit Page” or “Approver Page” within the site we’ll get this error message:
This happens because when we created the roles in our Application_Start event handler we didn’t add ourselves into the “Approvers” or “Auditors” role – and so ASP.NET is denying us access to those resources (like it should). When we click the “Admin” link we are able to access it, though, because we belong to the “Admins” role.
Step 6: Implementing Security Trimming for our Menu
One issue you will have noticed when we ran the sample above is that the menu on the left-hand side of the screen is still displaying all of the links when we visit the site – including links to the “Audit” and “Approver” pages that we don’t currently have access to (because we aren’t in those roles).
Ideally we want to hide those links and only display them to users within the appropriate roles to access them. This avoids users inadvertently seeing our “Access Denied” error message above. The good news is that this is easy to implement using a cool ASP.NET 2.0 feature called “Security Trimming”.
To implement security trimming, add this XML section to your web.config file:
<siteMap defaultProvider="XmlSiteMapProvider" enabled="true">
<providers>
<add name="XmlSiteMapProvider"
description="Default SiteMap provider."
type="System.Web.XmlSiteMapProvider "
siteMapFile="Web.sitemap"
securityTrimmingEnabled="true" />
</providers>
</siteMap>
This tells ASP.NET to enable security-trimming at the Site Navigation provider. Once we do this, we can then open up our web.sitemap configuration file again and update the individual nodes within it with “roles” attributes indicating what nodes are visible to incoming users:
<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode url="default.aspx" title="Home">
<siteMapNode url="approver.aspx" title="Approver Expenses" roles="Approvers" />
<siteMapNode url="audit/auditor.aspx" title="Audit Page" roles="Auditors" />
<siteMapNode url="admin/admin.aspx" title="Admin Manager" roles="Admins" />
</siteMapNode>
</siteMap>
And now when I run the application again, you’ll notice that the “Audit” and “Approver” links are hidden from the TreeView (since I’m still not in those roles):
The “administrator” link is still available, however, because I am a member of the “Admins” role.
Step 6: Building our Admin Page to Manage Role Permissions
The final step we’ll want to take is to build a simple role-manager page that Administrators within our expense reporting application can use to Add/Remove users from roles. We’ll do this within the “admin.aspx” page underneath our “Admin” sub-directory (which only users within the “Admins” role can access). Within this page we’ll add a TextBox that administrators can use to lookup a username, and then use a checkbox list to enable administrators to easily add/remove the selected user from roles. When the admin clicks the “Update” button these changes will be persisted in the role database:
Here is what the .aspx markup for the admin.aspx page looks like to implement this:
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" Runat="Server">
<h3>Role Manager</h3>
<div>
Enter UserName:
<asp:TextBox ID="TxtUserName" runat="server"></asp:TextBox>
<asp:Button ID="LookupBtn" runat="server" Text="Search" />
</div>
<div class="roleList">
<asp:CheckBoxList ID="RoleList" runat="server">
</asp:CheckBoxList>
</div>
<div>
<asp:button ID="UpdateBtn" text="Update" Visible="false" runat="server" />
</div>
</asp:Content>
Notice that I’m using an <asp:checkboxlist> control to provided a list of checkboxes for the roles. Here is then the complete code-behind file for the page:
Partial Class Admin_Admin
Inherits System.Web.UI.Page
Sub PopulateRoleList(ByVal userName As String)
RoleList.Items.Clear()
Dim roleNames() As String
Dim roleName As String
roleNames = Roles.GetAllRoles()
For Each roleName In roleNames
Dim roleListItem As New ListItem
roleListItem.Text = roleName
roleListItem.Selected = Roles.IsUserInRole(userName, roleName)
RoleList.Items.Add(roleListItem)
Next
End Sub
Sub UpdateRolesFromList()
Dim roleListItem As ListItem
For Each roleListItem In RoleList.Items
Dim roleName As String = roleListItem.Value
Dim userName As String = TxtUserName.Text
Dim enableRole As Boolean = roleListItem.Selected
If (enableRole = True) And (Roles.IsUserInRole(userName, roleName) = False) Then
Roles.AddUserToRole(userName, roleName)
ElseIf (enableRole = False) And (Roles.IsUserInRole(userName, roleName) = True) Then
Roles.RemoveUserFromRole(userName, roleName)
End If
Next
End Sub
Sub LookupBtn_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles LookupBtn.Click
PopulateRoleList(TxtUserName.Text)
UpdateBtn.Visible = True
End Sub
Sub UpdateBtn_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles UpdateBtn.Click
UpdateRolesFromList()
PopulateRoleList(TxtUserName.Text)
End Sub
End Class
Note that the above code does not hardcode the list of roles used within the application. Instead it dynamically populates the <asp:checklistbox> with the list of all roles currently in the Role Manager. This means that if we add a new role to our application it will automatically show up in the list (it also means you should be able to copy/paste this page into any application with Roles enabled and take advantage of it).
Note also that if you lookup your own account, and enable either the “auditors” or “approvers” role for it, the Treeview menu for the site will now show those nodes that were previously unavailable to us:
Programmatically Looking Up Roles
In the example above, I’ve been using the <authorization> section within web.config files to control access to page resources based on the role of the user. This works well for page-level authorization, however sometimes you want to grant everyone access to a page and then perform role-level checks within the page to control capabilities (for example: showing/hiding the edit and delete buttons within GridViews, etc).
Developers can use the User.IsInRole(rolename) method to easily check to see whether an authenticated user is within a specific role within a page. For example:
If User.IsInRole("Administrators") Then
' Do something only admins are allowed to-do
End If
This works great from within ASP.NET pages. Non-page derived classes don’t have a built-in “User” object. However, you can always access an ASP.NET Request’s User identity context using code like this:
Dim User As System.Security.Principal.IPrincipal
User = System.Web.HttpContext.Current.User
If User.IsInRole("Administrators") Then
' Do something only admins are allowed to-do
End If
The above code will work in any class within your application (or in a referenced class library assembly).
Summary
The above walkthrough demonstrates how you can build an application from scratch using ASP.NET 2.0’s built-in Windows Authentication, Role Management, Master Pages, Site Navigation, Url Authorization, and Security Trimming features. The combination of these features makes building secure Intranet applications easy. You can download a completed version of the above sample here.
For additional reading I’d recommend checking out these links:
My ASP.NET 2.0 Security Resource Link Page: This is a good page to bookmark for ASP.NET 2.0 Security information, and contains ton of tutorials and links on ASP.NET security topics.
ASP.NET 2.0 “How Do I” Videos: This is a great series of short 10-15 minute videos that you can watch online to learn ASP.NET concepts. Included are several videos that show off Security, Roles, Master Pages and Site Navigation.
How To: Use Role Provider in ASP.NET 2.0: This is a great article on MSDN that walks-through how to use the ASP.NET 2.0 Role Features.
Role Providers in ASP.NET: This page from K. Scott Allen contains some additional information on the ASP.NET Role Providers.
ASP.NET Site Navigation Tutorials: This is a great set of tutorials by Scott Mitchell that discuss the ASP.NET Site Navigation features.
Hope this helps,
Scott