Introduction
Recently, I developed a small web application in order to be able to do some basic file-management in my intranet web server. Having finished my app, I thought I should add some sort of security in order to prevent the other network users from tampering with my files. Thus, ASP.NET security came to mind. The only problem was that I wanted to avoid the use of a database, so I couldn’t use the default SQL membership and role providers. There are hundreds of articles on how to create custom membership and role providers, but all of them require a database to read the data from. I wanted something simple and quite secure, so I thought I should save my users’ and roles’ data in the most secure file in ASP.NET, which is no other than the web.config file.
In the first part, we will be developing a custom configuration section in order to store the roles and users data in the web.config file. In the second part, we will be briefly implementing the role and membership providers, and then, in the final part, we will be creating a small demo web application to see the security model in action. By the end of this article, you will be able to use all the out-of-the-box ASP.NET login controls and even the ASP.NET Web Site Administration Tool in order to configure your access rules.
Part 1: Custom Configuration Section
I first came upon custom configuration sections when I started working with the provider model, and to be honest, I used to configure the whole thing mechanically rather than actually understanding what exactly was going on under the hood. This is where Jon Rista’s series of articles entitled “Unraveling the Mysteries of .NET 2.0 Configuration” comes into play. I suggest reading at least the first part if you get lost in here.
In order to record our usernames and passwords, we will be adding a section in the web.config that looks like this:
<configuration>
...
-->
<CustomUsersSection>
-->
<Roles>
-->
<add RoleName="User"/>
<add RoleName="Administrator"/>
</Roles>
-->
<Users>
-->
<add UserName="auser" Password="password"
Email="abot@home" Role="User"/>
<add UserName="admin" Password="password"
Email="abot@home" Role="Administrator"/>
</Users>
</CustomUsersSection>
...
</configuration>
Looking at the above mentioned XML and thinking a little bit in OOP terms, we could say that it describes a class named CustomUsersSection
which has two properties: Roles
and Users
. Roles
and Users
are collections of items. If we now pay a little bit more attention on the add
tag and observe the attributes, we could say that the following XML:
<add rolename="User" />
represents an instance of a class with the property RoleName
set to the value “User
”, while the following XML:
<add username="auser" password="password" email="abot@home" role="User" />
represents another instance of another class that has the following properties and values:
UserName
- auser
Password
- password
Email
- abot@home
Role
- User
Well, this is exactly what this is all about: a hierarchy of classes. There are five classes involved in the above mentioned XML: one that represents the section (UsersConfigurationSection
), another to represent the collection of roles (CustomRolesCollection
), a class to represent the roles (CustomRole
), a class to represent the collection of users (CustomUsersCollection
), and finally, the class that represents the user (CustomUser
).
We shall start with the self explainable classes, CustomRole
and CustomRolesCollection
, located in the App_Code>Configuration> CustomRole.vb file:
Namespace Configuration
Public Class CustomRole
Inherits ConfigurationElement
<ConfigurationProperty("RoleName", IsRequired:=True)> _
Public ReadOnly Property RoleName() As String
Get
Return MyBase.Item("RoleName")
End Get
End Property
End Class
Public Class CustomRolesCollection
Inherits ConfigurationElementCollection
Protected Overloads Overrides Function CreateNewElement() _
As System.Configuration.ConfigurationElement
Return New CustomRole
End Function
Protected Overrides Function GetElementKey(ByVal element _
As System.Configuration.ConfigurationElement) As Object
Return CType(element, CustomRole).RoleName
End Function
Public Shadows ReadOnly Property Item(ByVal index _
As Integer) As CustomRole
Get
Return MyBase.BaseGet(index)
End Get
End Property
Public Shadows ReadOnly Property Item(ByVal rolename _
As String) As CustomRole
Get
Return MyBase.BaseGet(rolename)
End Get
End Property
End Class
End Namespace
These two inherit from the ConfigurationElement
and the ConfigurationElementCollection
classes, respectively. The ConfigurationElement
is a single entity which has properties, while the ConfigurationElementCollection
is a collection of ConfigurationElement
s. In our case, the CustomRole
class has a ReadOnly
property named RoleName
, which actually recalls the base’s attribute RoleName
. Thus, the framework reads the XML attribute and is inherited into the property. When you create properties, be sure to add the following compiler declaration:
<ConfigurationProperty("propertyname", IsRequired:=True)> _
On the other hand, the CustomRolesCollection
, which inherits from ConfigurationElementCollection
, is only required to implement the CreateNewElement
and the GetElementKey
functions. The two item property implementations help to retrieve the items from the collection since, by default, the item property is private.
On the same idea, I cite the CustomUser
and the CustomUsersCollection
from the App_Code>Configuration>CustomUser.vb file:
Imports System.Web.Security
Namespace Configuration
Public Class CustomUser
Inherits ConfigurationElement
<ConfigurationProperty("UserName", IsRequired:=True)> _
Public ReadOnly Property UserName() As String
Get
Return MyBase.Item("UserName")
End Get
End Property
<ConfigurationProperty("Password", IsRequired:=True)> _
Public ReadOnly Property Password() As String
Get
Return MyBase.Item("Password")
End Get
End Property
<ConfigurationProperty("Role", IsRequired:=True)> _
Public ReadOnly Property Role() As String
Get
Return MyBase.Item("Role")
End Get
End Property
<ConfigurationProperty("Email", IsRequired:=True)> _
Public ReadOnly Property Email() As String
Get
Return MyBase.Item("Email")
End Get
End Property
Public ReadOnly Property AspNetMembership(ByVal ProviderName _
As String) As System.Web.Security.MembershipUser
Get
Return New MembershipUser(ProviderName, Me.UserName, _
Me.UserName, Me.Email, "", "", True, False, _
Date.Now, Date.Now, Date.Now, Date.Now, Nothing)
End Get
End Property
End Class
Public Class CustomUsersCollection
Inherits ConfigurationElementCollection
Protected Overloads Overrides Function CreateNewElement() _
As System.Configuration.ConfigurationElement
Return New CustomUser
End Function
Protected Overrides Function GetElementKey(ByVal element As _
System.Configuration.ConfigurationElement) As Object
Return CType(element, CustomUser).UserName
End Function
Public Shadows ReadOnly Property Item(ByVal _
index As Integer) As CustomUser
Get
Return MyBase.BaseGet(index)
End Get
End Property
Public Shadows ReadOnly Property Item(ByVal username _
As String) As CustomUser
Get
Return MyBase.BaseGet(username)
End Get
End Property
End Class
End Namespace
The only addition to the above mentioned philosophy is the AspNetMembership
readonly property that I added in the CustomUser
, which returns a dummy System.Web.Security.MembershipUser
needed by the Membership provider. In order to add another property to CustomUser
, you simply add the following code in the class:
<ConfigurationProperty("UserName", IsRequired:=True)> _
Public ReadOnly Property AnotherProperty () As String
Get
Return MyBase.Item("AnotherProperty ")
End Get
End Property
And, don’t forget to complete the attribute in the web.config file.
The final class is located in the App_Code>Configuration>UsersConfigurationSection.vb file, and is the one that handles the whole configuration section we created, and thus, inherits from ConfigurationSection
:
Imports System.Configuration
Namespace Configuration
Public Class UsersConfigurationSection
Inherits ConfigurationSection
<ConfigurationProperty("Roles")> _
Public ReadOnly Property Roles() As CustomRolesCollection
Get
Return MyBase.Item("Roles")
End Get
End Property
<ConfigurationProperty("Users")> _
Public ReadOnly Property Users() As CustomUsersCollection
Get
Return MyBase.Item("Users")
End Get
End Property
Public Shared ReadOnly Property Current() _
As UsersConfigurationSection
Get
Return ConfigurationManager.GetSection("CustomUsersSection")
End Get
End Property
End Class
End Namespace
As you may have noticed, the Roles
and the Users
properties are configuration properties of the section. I have also added a shared read only property called current
in order to get the instance named “CustomUsersSection
” that is saved in the web.config file, using the ConfigurationManager
class.
So, by now, we have the whole class hierarchy, and we need to instruct the .NET framework that there is a new section in the web.config file that is of type UsersConfigurationSection
and that it will be named “CustomUsersSection
”. If we look exactly below the configuration tag of the provided web.config file, we will see the following directives:
<ConfigSections >
<section name="CustomUsersSection"
type="Configuration.UsersConfigurationSection, App_Code" />
</ConfigSections>
This is exactly what we want. Don’t be alarmed by the Configuration
namespace, I simply wanted to pack the classes under a namespace.
The final web.config file would look like this:
<configuration>
<ConfigSections>
<section name="CustomUsersSection"
type="Configuration.UsersConfigurationSection, App_Code" />
</ConfigSections>
<CustomUsersSection>
<roles>
<add rolename="User" />
<add rolename="Administrator" />
</roles>
<users>
<add role="User" email="abot@home"
password="password" username="auser" />
<add role="Administrator" email="abot@home"
password="password" username="admin" />
</users>
</CustomUsersSection>
...
</configuration />
Part 2: Custom Membership and Role Providers
If you search the net with the following three keywords “Custom Membership Provider”, you will end up with millions of pages and blogs describing how to build your custom Membership provider. This is mainly done by inheriting from the abstract RoleProvider
and MembershipProvider
classes and then overriding the required functions. In our case, we will not provide the capability to modify the users and the roles, so we will be throwing a lot of NotSupportedException
exceptions :-). The main difference between any other custom membership provider and this implementation is that the data is derived from the read only properties UsersConfigurationSection.Current.Users
and UsersConfigurationSection.Current.Roles
. Thus, the WebConfigMembershipProvider
class (App_Code>AspNetProvider>WebConfigMembershipProvider.vb) should look something like this (a lot of optimization is welcomed here):
Imports Configuration
Namespace AspNetProvider
Public Class WebConfigMembershipProvider
Inherits MembershipProvider
Private _ApplicationName As String
Public Overrides Sub Initialize(ByVal name As String, _
ByVal config As System.Collections.Specialized.NameValueCollection)
_ApplicationName = config("ApplicationName")
MyBase.Initialize(name, config)
End Sub
Public Overrides Property ApplicationName() As String
Get
Return _ApplicationName
End Get
Set(ByVal value As String)
_ApplicationName = value
End Set
End Property
Public Overrides Function ChangePassword(ByVal username As String, _
ByVal oldPassword As String, ByVal newPassword As String) As Boolean
Throw New System.NotSupportedException("No saving ")
End Function
Public Overrides Function ChangePasswordQuestionAndAnswer(ByVal username As String, _
ByVal password As String, ByVal newPasswordQuestion As String, _
ByVal newPasswordAnswer As String) As Boolean
Throw New System.NotSupportedException("Not implemented")
End Function
Public Overrides Function CreateUser(ByVal username As String, _
ByVal password As String, ByVal email As String, _
ByVal passwordQuestion As String, ByVal passwordAnswer As String, _
ByVal isApproved As Boolean, ByVal providerUserKey As Object, _
ByRef status As System.Web.Security.MembershipCreateStatus) _
As System.Web.Security.MembershipUser
Throw New System.NotSupportedException("Not implemented")
End Function
Public Overrides Function DeleteUser(ByVal username As String, _
ByVal deleteAllRelatedData As Boolean) As Boolean
Throw New System.NotSupportedException("Not implemented")
End Function
Public Overrides ReadOnly Property EnablePasswordReset() As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property EnablePasswordRetrieval() As Boolean
Get
Return False
End Get
End Property
Public Overrides Function FindUsersByEmail(ByVal emailToMatch As String, _
ByVal pageIndex As Integer, ByVal pageSize As Integer, _
ByRef totalRecords As Integer) As System.Web.Security.MembershipUserCollection
Dim output As New System.Web.Security.MembershipUserCollection
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.Email.Equals(emailToMatch) Then
output.Add(user.AspNetMembership(Me.Name))
End If
Next
Return output
End Function
Public Overrides Function FindUsersByName(ByVal usernameToMatch As String, _
ByVal pageIndex As Integer, ByVal pageSize As Integer, _
ByRef totalRecords As Integer) As System.Web.Security.MembershipUserCollection
Dim output As New System.Web.Security.MembershipUserCollection
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.UserName.Equals(usernameToMatch) Then
output.Add(user.AspNetMembership(Me.Name))
End If
Next
Return output
End Function
Public Overrides Function GetAllUsers(ByVal pageIndex As Integer, _
ByVal pageSize As Integer, ByRef totalRecords As Integer) _
As System.Web.Security.MembershipUserCollection
Dim output As New System.Web.Security.MembershipUserCollection
For Each user As CustomUser In UsersConfigurationSection.Current.Users
output.Add(user.AspNetMembership(Me.Name))
Next
Return output
End Function
Public Overrides Function GetNumberOfUsersOnline() As Integer
Throw New System.NotSupportedException("No saving ")
End Function
Public Overrides Function GetPassword(ByVal username As String, _
ByVal answer As String) As String
Throw New System.NotSupportedException("The question/answer" & _
" model is not supported yet!")
End Function
Public Overloads Overrides Function GetUser(ByVal providerUserKey As Object, _
ByVal userIsOnline As Boolean) As System.Web.Security.MembershipUser
Dim output As System.Web.Security.MembershipUser = Nothing
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.UserName.Equals(providerUserKey) Then
output = user.AspNetMembership(Me.Name)
End If
Next
Return output
End Function
Public Overloads Overrides Function GetUser(ByVal username As String, _
ByVal userIsOnline As Boolean) As System.Web.Security.MembershipUser
Dim output As System.Web.Security.MembershipUser = Nothing
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.UserName.Equals(username) Then
output = user.AspNetMembership(Me.Name)
End If
Next
Return output
End Function
Public Overrides Function GetUserNameByEmail(ByVal email As String) As String
Dim output As String = Nothing
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.Email.Equals(email) Then
output = user.UserName
End If
Next
Return output
End Function
Public Overrides ReadOnly Property MaxInvalidPasswordAttempts() As Integer
Get
Return 5
End Get
End Property
Public Overrides ReadOnly Property _
MinRequiredNonAlphanumericCharacters() As Integer
Get
Return 0
End Get
End Property
Public Overrides ReadOnly Property MinRequiredPasswordLength() As Integer
Get
Return 8
End Get
End Property
Public Overrides ReadOnly Property PasswordAttemptWindow() As Integer
Get
Return 30
End Get
End Property
Public Overrides ReadOnly Property PasswordFormat() _
As System.Web.Security.MembershipPasswordFormat
Get
Return MembershipPasswordFormat.Clear
End Get
End Property
Public Overrides ReadOnly Property _
PasswordStrengthRegularExpression() As String
Get
Return ""
End Get
End Property
Public Overrides ReadOnly Property RequiresQuestionAndAnswer() As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property RequiresUniqueEmail() As Boolean
Get
Return False
End Get
End Property
Public Overrides Function ResetPassword(ByVal username As String, _
ByVal answer As String) As String
Throw New System.NotSupportedException("No saving")
End Function
Public Overrides Function UnlockUser(ByVal userName As String) As Boolean
Throw New System.NotSupportedException("No saving")
End Function
Public Overrides Sub UpdateUser(ByVal user As System.Web.Security.MembershipUser)
Throw New System.NotSupportedException("No saving")
End Sub
Public Overrides Function ValidateUser(ByVal username As String, _
ByVal password As String) As Boolean
Dim output As Boolean = False
If username.Length > 0 Then
Dim myUser As CustomUser = _
UsersConfigurationSection.Current.Users.Item(username)
If myUser IsNot Nothing Then
output = myUser.Password.Equals(password)
End If
End If
Return output
End Function
End Class
End Namespace
The WebConfigRoleProvider
class (App_Code>AspNetProvider>WebConfigRoleProvider.vb) is as follows:
Imports Configuration
Namespace AspNetProvider
Public Class WebConfigRoleProvider
Inherits RoleProvider
Private _ApplicationName As String
Public Overrides Sub Initialize(ByVal name As String, ByVal config _
As System.Collections.Specialized.NameValueCollection)
_ApplicationName = config("ApplicationName")
MyBase.Initialize(name, config)
End Sub
Public Overrides Sub AddUsersToRoles(ByVal usernames() As String, _
ByVal roleNames() As String)
Throw New System.NotSupportedException("No saving")
End Sub
Public Overrides Property ApplicationName() As String
Get
Return _ApplicationName
End Get
Set(ByVal value As String)
_ApplicationName = value
End Set
End Property
Public Overrides Sub CreateRole(ByVal roleName As String)
Throw New System.NotSupportedException("No saving")
End Sub
Public Overrides Function DeleteRole(ByVal roleName As String, _
ByVal throwOnPopulatedRole As Boolean) As Boolean
Throw New System.NotSupportedException("No saving")
End Function
Public Overrides Function FindUsersInRole(ByVal roleName As String, _
ByVal usernameToMatch As String) As String()
Dim output As New ArrayList
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.Role.Equals(roleName) AndAlso _
user.UserName.Equals(usernameToMatch) Then
output.Add(user.UserName)
End If
Next
Return output.ToArray(GetType(String))
End Function
Public Overrides Function GetAllRoles() As String()
Dim myRoles As New ArrayList
For Each role As CustomRole In UsersConfigurationSection.Current.Roles
myRoles.Add(role.RoleName)
Next
Return myRoles.ToArray(GetType(String))
End Function
Public Overrides Function GetRolesForUser(ByVal username As String) As String()
Dim user As CustomUser = _
UsersConfigurationSection.Current.Users.Item(username)
If user IsNot Nothing Then
Return New String() {user.Role}
Else
Return New String() {}
End If
End Function
Public Overrides Function GetUsersInRole(ByVal roleName As String) As String()
Dim output As New ArrayList
For Each user As CustomUser In UsersConfigurationSection.Current.Users
If user.Role.Equals(roleName) Then
output.Add(user.UserName)
End If
Next
Return output.ToArray(GetType(String))
End Function
Public Overrides Function IsUserInRole(ByVal username As String, _
ByVal roleName As String) As Boolean
Dim user As CustomUser = UsersConfigurationSection.Current.Users.Item(username)
If user IsNot Nothing Then
Return user.Role.Equals(roleName)
Else
Return False
End If
End Function
Public Overrides Sub RemoveUsersFromRoles(ByVal usernames() As String, _
ByVal roleNames() As String)
Throw New System.NotSupportedException("No saving")
End Sub
Public Overrides Function RoleExists(ByVal roleName As String) As Boolean
Return UsersConfigurationSection.Current.Roles.Item(roleName) IsNot Nothing
End Function
End Class
End Namespace
Hopefully, the functions are self-explainable since they do have ridiculously over-descriptive names. For completeness’ sake, I will cite the configuration required in the web.config in order to use the custom providers. The whole configuration is performed in the configuration>system.web
section, and looks like this:
<!---->
<authentication mode="Forms"/>
<!---->
<roleManager enabled="true" defaultProvider="WebConfigRoleProvider">
<providers>
<!---->
<clear/>
<!---->
<add name="WebConfigRoleProvider"
type="AspNetProvider.WebConfigRoleProvider" applicationName="WebSite" _
enabled="true" cacheRolesInCookie="true" cookieName=".ASPROLES"
cookieTimeout="30" cookiePath="/" cookieRequireSSL="false" _
cookieSlidingExpiration="true" cookieProtection="All" />
</providers>
</roleManager>
<!---->
<membership defaultProvider="WebConfigMembershipProvider">
<providers>
<clear/>
<add name="WebConfigMembershipProvider"
type="AspNetProvider.WebConfigMembershipProvider"
applicationName="Website"/>
</providers>
</membership>
Part 3: Demo Web Application
As a proof of concept, I have built a website which doesn’t allow anonymous users (see the authorization
section in the web.config file). Thus, when the user visits the website, he is redirected to the login form.
Both the login.aspx and the default.aspx pages utilize the controls from the Login tab in the toolbox, as seen in the following screen:
The most interesting lines of code are:
My.User.Name
: Gets the username
My.User.IsInRole("Administrator")
: Checks if the logged-in user is an Administrator
Configuration.UsersConfigurationSection.Current.Users.Item(my.User.Name).Email
: Gets the user’s email address
Finally, I should note that you can use the ASP.NET Web Site Administration Tool in order to configure the access rules of the website directories.
Conclusions
In this article, we have seen an innovative approach to implementing membership and role providers. The user credentials are stored in the web.config file in a custom configuration section, and we were able to use the out-of-the-box login controls in order to validate the user.
Possible Enhancements
One major possible enhancement for the given code is to allow the update of users and roles. The method for saving configuration changes is discussed in this article.
Moreover, a possible security update would be to encrypt or even hash the passwords. I would personally prefer hashing the password since it’s much safer. To authenticate a user, the password presented by the user is hashed and compared with the stored hash. In this case, we would have to add a new class called Hashing
:
Imports System.Security.Cryptography
Imports System.Text
Public Class Hashing
Public Shared Function HashInMD5(ByVal cleanString As String) As String
Dim clearBytes As [Byte]()
clearBytes = New UnicodeEncoding().GetBytes(cleanString)
Dim hashedBytes As [Byte]() = _
CType(CryptoConfig.CreateFromName("MD5"), _
HashAlgorithm).ComputeHash(clearBytes)
Dim hashedText As String = BitConverter.ToString(hashedBytes)
Return hashedText
End Function
End Class
And then, in the WebConfigMembershipProvider.vb file, we would have to change the ValidateUser
function to the following:
Public Overrides Function ValidateUser(ByVal username As String, _
ByVal password As String) As Boolean
Dim output As Boolean = False
If username.Length > 0 Then
Dim myUser As CustomUser = _
UsersConfigurationSection.Current.Users.Item(username)
If myUser IsNot Nothing Then
output = myUser.Password.Equals(Hashing.HashInMD5(password))
End If
End If
Return output
End Function
Thanks to a discussion with about:blank / colin in the forum, I was prompted to modify the code to support multiple roles per user. In order to locate the changes, find the
comments.
Feedback and Voting
If you have read this far, please remember to vote. If you do or don't like, agree, or disagree with what you have read in this article, then please say so in the forum below. Your feedback is imperative since this is my first CodeProject article!
History
- 21 July 2008: First version.
- 7 August 2008: Added multiple roles per user support under the possible enhancements section.
P.S.: If you do not want to implement a role provider, then you should revert to this article which uses the built-in .NET authentication model to store usernames and passwords, but it needs an extension in order to support roles.