Practical .NET
Create a Flexible Security System for the ASP.NET Platform in the .NET Framework 4.5
Sometimes you need more than roles to effectively manage authorizing user requests. You can do that without moving to claims-based security in the Microsoft .NET Framework 4.5 by creating your own user object.
First, you noticed you were assigning users to roles that gave them more access than they needed. So you started tailoring roles more specifically and ended up with roles with one user in them. (And at that point, why bother with roles at all?) Finally, you started creating very targeted roles and assigning multiple roles to each user.
That final strategy is moving you to claims-based security: each role really represents a kind of claim. One role says the user is a manager, another that the user is in the Western division, a third that the user can authorize expenditures up to $10,000. The Microsoft .NET Framework 4.5 supports claims-based security directly, but you can create an equivalent system in ASP.NET 4 (either in MVC, Web Forms or Web API) without much trouble.
The first step is to create an object that implements the IPrincipal interface and has properties for the data (claims) that you'll use to authorize requests. A class that implements IPrincipal has to have a property called Identity that returns a GenericIdentity object (which provides the user's name) and a method called IsInRole method that returns True if a user is assigned to a specific role. The user object in Listing 1 does all of that and adds three additional properties (Division, Job and ApprovalLevel).
Listing 1. A custom user object.
Public Class PHVUser
Implements IPrincipal
Dim _gi As GenericIdentity
Dim _roles As New List(Of String)
Public Sub New(Name As String, Optional Roles As String = "")
_gi = New GenericIdentity(Name)
If Roles <> "" And Roles <> String.Empty Then
For Each rol In Roles.Split(",").
Select(Function(r) r.Trim()).ToArray
_roles.Add(rol)
Next
End If
End Sub
Public Property Division As String
Public Property Job As String
Public Property ApprovalLevel As Integer
Public ReadOnly Property Identity _
As IIdentity Implements IPrincipal.Identity
Get
Return _gi
End Get
End Property
Public Function IsInRole(role As String) _
As Boolean Implements IPrincipal.IsInRole
If _roles.Contains(role) Then
Return True
Else
Return False
End If
End Function
End Class
To create a user object for a user named "Peter" who's assigned to the roles manager and clerk, you'd use this code:
usr = New PHVUser("Peter", "manager, clerk")
Replacing the User
Now you have to get ASP.NET to use your security object. ASP.NET looks for its IPrincipal object in the CurrentPrincipal object of the Thread class and the User property of HttpContext.Current. If you put your object in there, ASP.NET will use it for its default user name and roles authorization.
To get your IPrincipal object into those two spots, you need to create an HTTP module (a class that implements the IHttpModule and IDisposable events) and add it to your site's processing pipeline. ASP.NET will fire the PostAuthenticateRequest event on your module whenever a request comes in to your site. You need to wire up a method to that event in your module's Init event to create and insert your object.
The class begins like this (I've omitted some code that implements the IDisposable interface that Visual Studio will generate for you):
Public Class PHVAuthHttp
Implements IHttpModule, IDisposable
Public Sub Init(context As HttpApplication) _
Implements IHttpModule.Init
AddHandler context.PostAuthenticateRequest,
New EventHandler(AddressOf ReplaceUser)
End Sub
In your method, you need to find out who the current user is so you can create your IPrincipal object for that user. When a user successfully gets through Forms Authentication, they're given a cookie with their name (and some other information) encrypted inside of it. You can grab that cookie and decrypt it like this:
Private Shared Sub ReplaceUser(sender As Object,
e As EventArgs)
Dim authCookie As HttpCookie
authCookie = HttpContext.Current.Request.Cookies.
Get(FormsAuthentication.FormsCookieName)
If authCookie IsNot Nothing Then
Dim tkt As FormsAuthenticationTicket
tkt = FormsAuthentication.Decrypt(authCookie.Value)
After you check that everything has worked, you can use the user's name to retrieve information about the user, and create your IPrincipal object and set your properties on it before putting it where ASP.NET will look for it:
If tkt.Name IsNot Nothing AndAlso
tkt.Name <> String.Empty AndAlso
tkt.Name <> "" Then
// ... Retrieve user information into a variable called emp ...
Threading.Thread.CurrentPrincipal = New PHVUser(tkt.Name) With
{.Region = emp.Region,
.Job = emp.Job,
.ApprovalLevel = 10000}
HttpContext.Current.User = Threading.Thread.CurrentPrincipal
End If
Because you're doing this on every request, it would be a good idea to put the user's information (my emp variable in the previous example) in ASP.NET Cache and look for it there before going to the database to retrieve it.
Finally, you need to get ASP.NET to use your HTTP module by adding a modules element to your web.config file inside the system.webServer element. Inside the modules element put an add tag. You can set the name attribute to anything you want, but the type attribute must be set to the full name for your class (namespace and class name) followed by the name of the DLL it's in:
<modules>
<add name="PHVAuth" type="PHVProj.PHVAuthHttp, PHVProj" />
</modules>
Testing Authorization
You can now test for your user's claims in your own code:
Dim usr As PHVUser = TryCast(HttpContext.Current.User, PHVUser)
If usr IsNot Nothing AndAlso
usr.Region = "West" AndAlso
usr.ApprovalLevel = 10000 Then
If you're working in ASP.NET MVC you can also create a custom Authorize filter that checks your user. Listing 2 shows an Authorize filter that checks a user's Region and works well in an asynchronous world. You could add it to a method like this:
<DivisionAuthAttribute(Region="West")>
Listing 2. An Authorize filter that checks a user's Region.
<AttributeUsage(AttributeTargets.Class Or AttributeTargets.Method)>
Public Class DivisionAuthAttribute
Inherits AuthorizeAttribute
Private Const IS_AUTHORIZED As String = "isAuthorized"
Private RedirectUrl As String = "~/Error/Unauthorized"
Public Property Division As String
Protected Overrides Function AuthorizeCore(
httpContext As Web.HttpContextBase) As Boolean
Dim IsAuthorized As Boolean = False
Dim us As UserSecurity = TryCast(httpContext.User, PHVUser)
If us IsNot Nothing AndAlso
Me.Division <> String.Empty AndAlso
us.Division = Me.Division Then
httpContext.Items.Add(IS_AUTHORIZED, IsAuthorized)
Return True
End If
httpContext.Items.Add(IS_AUTHORIZED, IsAuthorized)
Return False
End Function
Public Overrides Sub OnAuthorization(
filterContext As Web.Mvc.AuthorizationContext)
MyBase.OnAuthorization(filterContext)
Dim IsAuthorized As Boolean
If filterContext.HttpContext.Items(IS_AUTHORIZED) _
IsNot Nothing Then
IsAuthorized = Convert.ToBoolean(
filterContext.HttpContext.Items(IS_AUTHORIZED))
Else
IsAuthorized = False
End If
If IsAuthorized = False AndAlso
filterContext.RequestContext.HttpContext.
User.Identity.IsAuthenticated Then
filterContext.Result = New RedirectResult(RedirectUrl)
End If
End Sub
End Class
You can now extend your user object to encompass any information about your user that you need to authorize requests.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.