A common question I get from ASP .NET developers is how to quickly and easily set up forms authentication where the passwords in the database are not stored in plain text. It actually shocks me how many systems I've gone into on the database end and seen passwords for every account in the application sitting there in clear text. Here's how Easy Assets .NET handles hashing passwords with a random salt value.
First let's take a look at our LoginUser object:
1 Imports System.Web.Security
2 Imports System.Security.Cryptography
3
4 Public Class LoginUsers
5 Inherits DomainObjects
6
7 Public Overloads Sub Add(ByVal newItem As AcquisitionMethod)
8 MyBase.List.Add(newItem)
9 End Sub
10
11 Default Public Shadows Property Item(ByVal index As Integer) As AcquisitionMethod
12 Get
13 Return CType(list(index), AcquisitionMethod)
14 End Get
15 Set(ByVal Value As AcquisitionMethod)
16 list(index) = Value
17 End Set
18 End Property
19 End Class
20
21 Public Class LoginUser
22 Inherits DomainObject
23
24 #Region "Constructors"
25 Public Sub New()
26 Me.UserName = ""
27 End Sub
28
29 Public Sub New(ByVal LoginName As String)
30 Me.UserName = LoginName
31 End Sub
32 #End Region
33
34 #Region "Private Members"
35 Private _username As String
36 Private _emailaddress As String
37 Private _firstname As String
38 Private _issuesolver As Boolean
39 Private _lastname As String
40 Private _password As String
41 Private _roles As System.Text.StringBuilder
42 Private _salt As String
43 Private _isActive As Boolean
44 #End Region
45
46 #Region "Properties"
47 Public Property EmailAddress() As String
48 Get
49 Return Me._emailaddress
50 End Get
51 Set(ByVal value As String)
52 Me._emailaddress = value
53 End Set
54 End Property
55
56 Public ReadOnly Property FirstLast() As String
57 Get
58 Return (Me._firstname & " " & Me._lastname)
59 End Get
60 End Property
61
62 Public Property FirstName() As String
63 Get
64 Return Me._firstname
65 End Get
66 Set(ByVal value As String)
67 Me._firstname = value
68 End Set
69 End Property
70
71 Public Property IssueSolver() As Boolean
72 Get
73 Return Me._issuesolver
74 End Get
75 Set(ByVal value As Boolean)
76 Me._issuesolver = value
77 End Set
78 End Property
79
80 Public ReadOnly Property LastFirst() As String
81 Get
82 If ((Not Me._lastname Is String.Empty) AndAlso (Not Me._firstname Is String.Empty)) Then
83 Return (Me._lastname & ", " & Me._firstname)
84 End If
85 Return ""
86 End Get
87 End Property
88
89 Public Property LastName() As String
90 Get
91 Return Me._lastname
92 End Get
93 Set(ByVal value As String)
94 Me._lastname = value
95 End Set
96 End Property
97
98 Public Property Password() As String
99 Get
100 Return Me._password
101 End Get
102 Set(ByVal Value As String)
103 Me._password = Value
104 End Set
105 End Property
106
107 Public Property salt() As String
108 Get
109 Return Me._salt
110 End Get
111 Set(ByVal Value As String)
112 Me._salt = Value
113 End Set
114 End Property
115
116 Public ReadOnly Property Roles() As String
117 Get
118 If IsNothing(_roles) Then
119 Me._roles = New System.Text.StringBuilder
120
121 Dim query As New EasyAssets.DAC.UserRoleQuery("RoleDescription") 122 query.UserRole = New EasyAssets.DAC.UserRole
123 query.UserRole.UserName = Me._username
124
125 Dim dt As DataTable = ObjectDomainMGR.ListSummary(query)
126 For Each dr As DataRow In dt.Rows
127 Me._roles.Append(dr("RoleName") & "|") 128 Next
129 End If
130
131 Return Me._roles.ToString()
132 End Get
133 End Property
134
135 Public Property UserName() As String
136 Get
137 Return Me._username
138 End Get
139 Set(ByVal value As String)
140 Me._username = value
141 Me.Key = value
142 End Set
143 End Property
144
145 Public Property isActive() As Boolean
146 Get
147 Return _isActive
148 End Get
149 Set(ByVal Value As Boolean)
150 _isActive = Value
151 End Set
152 End Property
153 #End Region
154
155 #Region "Methods"
156 Private Sub CreatePasswordHash()
157 Dim passwordSalt As String = (_password & _salt)
158 _password = FormsAuthentication.HashPasswordForStoringInConfigFile(passwordSalt, "SHA1")
159 End Sub
160
161 Private Sub CreateSalt(ByVal size As Integer)
162 Dim provider1 As New RNGCryptoServiceProvider
163 Dim buffer1 As Byte() = New Byte(size - 1) {} 164
165 provider1.GetBytes(buffer1)
166 _salt = Convert.ToBase64String(buffer1)
167 End Sub
168
169 Public Sub SetPassword()
170 CreateSalt(5)
171 CreatePasswordHash()
172 End Sub
173 #End Region
Creating/Changing a Password
When you load the loginuser object from the database the password hash and the salt value are stored in the appropriate properties. If you are creating a new user or wish to change the password of an existing user you simply set the Password property to the plain text value of the password from your form. Then you call the SetPassword() method which generates a random salt value and uses it in conjunction with the plain text password to create the hash.
Logging In
So how do we authenticate the user then? The code is fairly simple. The Login.aspx page has a username and password textboxes and a button to click. The button code grabs the LoginUser from the database specified by the username and if the user is active uses the salt value to generate a hash for comparison. The code looks like this:
Private Sub btnLogin_Click(ByVal sender As System.Object, ByVal e As System.Web.UI.ImageClickEventArgs) Handles btnLogin.Click
Try
Dim thisUser As New EasyAssets.DAC.LoginUser(txtUserName.Text)
thisUser = DomainManager.Load(thisUser)
If thisUser.isActive Then
Dim passwordAndSalt As String = String.Concat(txtPassword.Text, thisUser.salt)
Dim hashedPasswordAndSalt As String = FormsAuthentication.HashPasswordForStoringInConfigFile(passwordAndSalt, "SHA1")
If Not thisUser.Password.Equals(hashedPasswordAndSalt) Then
Throw New System.Exception("Invalid Username/Password combination!") End If
Dim authTicket As New FormsAuthenticationTicket(1, thisUser.UserName, DateTime.Now, DateTime.Now.AddMinutes(30), False, thisUser.Roles)
Dim encryptedTicket As String = FormsAuthentication.Encrypt(authTicket)
Dim authCookie = New HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket)
Response.Cookies.Add(authCookie)
Else
Throw New Exception("Account Expired! Contact an administrator.") End If
Response.Redirect("Welcome.aspx", False) Catch ex As Exception
WriteMessage(ex.Message, True)
End Try
End Sub
Role Based Security
You'll notice that the authentication ticket being created passes in the LoginUser's Roles. In EasyAssets .NET there are a list of roles that any user can be assigned to and they grant or deny access to various parts of the application. You'll notice that the Roles() property of the LoginUser queries the database and then builds a StringBuilder with the role names delimited by the pipe |. The end result is that if you have a role called "ADMIN" you can simply do context.user.identity.isInRole("ADMIN") to validate whether that user is the admin.
How does ASP .NET handle building the ticket and setting the roles on the context.user? Simply add the following code to your global.asax:
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
Dim CookieName As String = FormsAuthentication.FormsCookieName
Dim authCookie As HttpCookie = Context.Request.Cookies(CookieName)
'Check for cookie
If IsNothing(authCookie) Then Return
Dim authTicket As FormsAuthenticationTicket
Try
authTicket = FormsAuthentication.Decrypt(authCookie.Value)
Catch
Return
End Try
If IsNothing(authTicket) Then
'Cookie failed to decrypt
Return
End If
Dim roles() As String = authTicket.UserData.Split("|") Dim id As New FormsIdentity(authTicket)
Dim principal As New System.Security.Principal.GenericPrincipal(id, roles)
Context.User = principal
End Sub
Finally, set your web.config to be forms authentication mode as follows:
<authentication mode="Forms">
<forms name="EasyAssetFormsAuth" loginUrl="WebModules/Security/LogIn.aspx" protection="Encryption" timeout="30" path="/">
forms>
authentication>
<authorization>
<deny users="?" />
<allow users="*" />
authorization>