Walkthrough: Implementing Custom Authentication and Authorization
This walkthrough demonstrates how to implement custom authentication and authorization by using classes that derive from IIdentity and IPrincipal. This walkthrough also demonstrates how to override the application thread's default identity, the Windows identity, by setting My.User.CurrentPrincipal to an instance of the class that derives from IPrincipal. The new user information is available immediately through the My.User object, which returns information about the thread's current user identity.
Business applications often provide access to data or resources based on credentials supplied by the user. Typically, such applications check the role of a user and provide access to resources based on that role. The common language runtime provides support for role-based authorization based on a Windows account or a custom identity. For more information, see Role-Based Security.
Getting Started
First, set up a project with a main form and a login form, and configure it to use custom authentication.
To create the sample application
Create a new Visual Basic Windows Application project. For more information, see How to: Create a Windows Application Project.
The default name of the main form is Form1.
On the Project menu, click Add New Item.
Select the Login Form template and click Add.
The default name of the login form is LoginForm1.
On the Project menu, click Add New Item.
Select the Class template, change the name to SampleIIdentity, and then click Add.
On the Project menu, click Add New Item.
Select the Class template, change the name to SampleIPrincipal, and then click Add.
On the Project menu, click <ApplicationName> Properties.
In the Project Designer, click the Application tab.
Change the Authentication mode drop-down to Application-defined.
To configure the main form
Switch to Form1 in the Forms Designer.
Add a Button to Form1 from the Toolbox.
The default name of the button is Button1.
Change the button text to Authenticate.
From the Toolbox, add a Label to Form1.
The default name of the label is Label1.
Change the label text to an empty string.
From the Toolbox, add a Label to Form1.
The default name of the label is Label2.
Change the label text to an empty string.
Double-click Button1 to create the event handler for the Click event, and then open the Code Editor.
Add the following code to the Button1_Click method.
My.Forms.LoginForm1.ShowDialog() ' Check if the user was authenticated. If My.User.IsAuthenticated Then Me.Label1.Text = "Authenticated " & My.User.Name Else Me.Label1.Text = "User not authenticated" End If If My.User.IsInRole(ApplicationServices.BuiltInRole.Administrator) Then Me.Label2.Text = "User is an Administrator" Else Me.Label2.Text = "User is not an Administrator" End If
You can run the application, but because there is no authentication code, it will not authenticate any user. Adding authentication code is discussed in the following section.
Creating an Identity
The .NET Framework uses the IIdentity and IPrincipal interfaces as the basis for authentication and authorization. Your application can use custom user authentication by implementing these interfaces, as these procedures demonstrate.
To create a class that implements IIdentity
Select the SampleIIdentity.vb file in Solution Explorer.
This class encapsulates a user's identity.
In the line following Public Class SampleIIdentity, add the following code to inherit from IIdentity.
Implements System.Security.Principal.IIdentity
After you add that code and press ENTER, the Code Editor creates stub properties that you must implement.
Add private fields to store the user name and a value that indicates if the user is authenticated.
Private nameValue As String Private authenticatedValue As Boolean Private roleValue As ApplicationServices.BuiltInRole
Enter the following code in the AuthenticationType property.
The AuthenticationType property needs to return a string that indicates the current authentication mechanism.
This example uses explicitly specified authentication, so the string is "Custom Authentication". If the user authentication data were stored in a SQL Server database, the value might be "SqlDatabase".
Return "Custom Authentication"
Enter the following code in the IsAuthenticated property.
Return authenticatedValue
The IsAuthenticated property needs to return a value that indicates whether the user has been authenticated.
The Name property needs to return the name of the user associated with this identity.
Enter the following code in the Name property.
Return nameValue
Create a property that returns the user's role.
Public ReadOnly Property Role() As ApplicationServices.BuiltInRole Get Return roleValue End Get End Property
Create a Sub New method that initializes the class by authenticating the user and then setting the user name and role, based on a name and a password.
This method calls a method named IsValidNameAndPassword to determine if a user name and password combination is valid.
Public Sub New(ByVal name As String, ByVal password As String) ' The name is not case sensitive, but the password is. If IsValidNameAndPassword(name, password) Then nameValue = name authenticatedValue = True roleValue = ApplicationServices.BuiltInRole.Administrator Else nameValue = "" authenticatedValue = False roleValue = ApplicationServices.BuiltInRole.Guest End If End Sub
Create a method named IsValidNameAndPassword that determines if a user name and password combination is valid.
Security Note: The authentication algorithm must treat passwords securely. For example, the password should not be stored in a class field.
You should not store user passwords in your system, because if that information is leaked, there is no more security. You could store the hash of each user's password. (A hash function scrambles data so the input cannot be deduced from the output.) A password cannot be directly determined from the hash of a password.
However, a malicious user could take the time to generate a dictionary of the hashes of all possible passwords, and then look up the password for a given hash. To protect against this type of attack, you should add salt to the password before hashing it, to generate a salted hash. Salt is extra data that is unique to each password, which make it impossible to precompute a hash dictionary.
To protect the passwords from malicious users, you should store only salted hashes of the passwords, preferably on a secure computer. It is very difficult for a malicious user to recover a password from a salted hash. This example uses the GetHashedPassword and GetSalt methods to load a user's hashed password and salt.
Private Function IsValidNameAndPassword( _ ByVal username As String, _ ByVal password As String) _ As Boolean ' Look up the stored hashed password and salt for the username. Dim storedHashedPW As String = GetHashedPassword(username) Dim salt As String = GetSalt(username) 'Create the salted hash. Dim rawSalted As String = salt & Trim(password) Dim saltedPwBytes() As Byte = _ System.Text.Encoding.Unicode.GetBytes(rawSalted) Dim sha1 As New _ System.Security.Cryptography.SHA1CryptoServiceProvider Dim hashedPwBytes() As Byte = sha1.ComputeHash(saltedPwBytes) Dim hashedPw As String = Convert.ToBase64String(hashedPwBytes) ' Compare the hashed password with the stored password. Return hashedPw = storedHashedPW End Function
Create functions named GetHashedPassword and GetSalt that return the hashed password and salt for the specified user.
Security Note: You should avoid hard coding the hashed passwords and salts into your client applications for two reasons. First, malicious users may be able to access them and find a hash collision. Second, you cannot change or revoke a user's password. The application should obtain the hashed password and salt for a given user from a secure source that an administrator maintains.
Although, for simplicity, this example has a hard-coded, hashed password and salt, you should use a more secure approach in production code. For example, you could store the user information in a SQL Server database and access it with stored procedures. For more information, see How to: Connect to Data in a Database.
Note
The password corresponding to this hard-coded hashed password is given in the "Testing the Application" section.
Private Function GetHashedPassword(ByVal username As String) As String ' Code that gets the user's hashed password goes here. ' This example uses a hard-coded hashed passcode. ' In general, the hashed passcode should be stored ' outside of the application. If Trim(username).ToLower = "testuser" Then Return "ZFFzgfsGjgtmExzWBRmZI5S4w6o=" Else Return "" End If End Function Private Function GetSalt(ByVal username As String) As String ' Code that gets the user's salt goes here. ' This example uses a hard-coded salt. ' In general, the salt should be stored ' outside of the application. If Trim(username).ToLower = "testuser" Then Return "Should be a different random value for each user" Else Return "" End If End Function
The SampleIIdentity.vb file should now contain the following code:
Public Class SampleIIdentity
Implements System.Security.Principal.IIdentity
Private nameValue As String
Private authenticatedValue As Boolean
Private roleValue As ApplicationServices.BuiltInRole
Public ReadOnly Property AuthenticationType() As String Implements System.Security.Principal.IIdentity.AuthenticationType
Get
Return "Custom Authentication"
End Get
End Property
Public ReadOnly Property IsAuthenticated() As Boolean Implements System.Security.Principal.IIdentity.IsAuthenticated
Get
Return authenticatedValue
End Get
End Property
Public ReadOnly Property Name() As String Implements System.Security.Principal.IIdentity.Name
Get
Return nameValue
End Get
End Property
Public ReadOnly Property Role() As ApplicationServices.BuiltInRole
Get
Return roleValue
End Get
End Property
Public Sub New(ByVal name As String, ByVal password As String)
' The name is not case sensitive, but the password is.
If IsValidNameAndPassword(name, password) Then
nameValue = name
authenticatedValue = True
roleValue = ApplicationServices.BuiltInRole.Administrator
Else
nameValue = ""
authenticatedValue = False
roleValue = ApplicationServices.BuiltInRole.Guest
End If
End Sub
Private Function IsValidNameAndPassword( _
ByVal username As String, _
ByVal password As String) _
As Boolean
' Look up the stored hashed password and salt for the username.
Dim storedHashedPW As String = GetHashedPassword(username)
Dim salt As String = GetSalt(username)
'Create the salted hash.
Dim rawSalted As String = salt & Trim(password)
Dim saltedPwBytes() As Byte = _
System.Text.Encoding.Unicode.GetBytes(rawSalted)
Dim sha1 As New _
System.Security.Cryptography.SHA1CryptoServiceProvider
Dim hashedPwBytes() As Byte = sha1.ComputeHash(saltedPwBytes)
Dim hashedPw As String = Convert.ToBase64String(hashedPwBytes)
' Compare the hashed password with the stored password.
Return hashedPw = storedHashedPW
End Function
Private Function GetHashedPassword(ByVal username As String) As String
' Code that gets the user's hashed password goes here.
' This example uses a hard-coded hashed passcode.
' In general, the hashed passcode should be stored
' outside of the application.
If Trim(username).ToLower = "testuser" Then
Return "ZFFzgfsGjgtmExzWBRmZI5S4w6o="
Else
Return ""
End If
End Function
Private Function GetSalt(ByVal username As String) As String
' Code that gets the user's salt goes here.
' This example uses a hard-coded salt.
' In general, the salt should be stored
' outside of the application.
If Trim(username).ToLower = "testuser" Then
Return "Should be a different random value for each user"
Else
Return ""
End If
End Function
End Class
Creating a Principal
Next, you need to implement a class that derives from IPrincipal, and have it return instances of the SampleIIdentity class.
To create a class that implements IPrincipal
Select the SampleIPrincipal.vb file in Solution Explorer.
This class encapsulates a user's identity. You can use the My.User object to attach this principal to the current thread and access the user's identity.
In the line following Public Class SampleIPrincipal, add the following code to inherit from IPrincipal.
Implements System.Security.Principal.IPrincipal
After you add that code and press ENTER, the Code Editor creates a stub property and method that you must implement.
Add a private field to store the identity associated with this principal.
Private identityValue As SampleIIdentity
Enter the following code in the Identity property.
Return identityValue
The Identity property needs to return the user identity of the current principal.
Enter the following code in the IsInRole method.
The IsInRole method determines whether the current principal belongs to the specified role.
Return role = identityValue.Role.ToString
Create a Sub New method that initializes the class with a new instance of SampleIIdentity given a user name and password.
Public Sub New(ByVal name As String, ByVal password As String) identityValue = New SampleIIdentity(name, password) End Sub
This code sets the user identity for the SampleIPrincipal class.
The SampleIPrincipal.vb file should now contain the following code:
Public Class SampleIPrincipal
Implements System.Security.Principal.IPrincipal
Private identityValue As SampleIIdentity
Public ReadOnly Property Identity() As System.Security.Principal.IIdentity Implements System.Security.Principal.IPrincipal.Identity
Get
Return identityValue
End Get
End Property
Public Function IsInRole(ByVal role As String) As Boolean Implements System.Security.Principal.IPrincipal.IsInRole
Return role = identityValue.Role.ToString
End Function
Public Sub New(ByVal name As String, ByVal password As String)
identityValue = New SampleIIdentity(name, password)
End Sub
End Class
Connecting the Login Form
The application can use the login form to gather a user name and password. It can use this information to initialize an instance of the SampleIPrincipal class and use the My.User object to set the current thread's identity to that instance.
To configure the login form
Select LoginForm1 in the designer.
Double-click the OK button to open the Code Editor for the Click event.
Replace the code in the OK_Click method with the following code.
Dim samplePrincipal As New SampleIPrincipal( _ Me.UsernameTextBox.Text, Me.PasswordTextBox.Text) Me.PasswordTextBox.Text = "" If (Not samplePrincipal.Identity.IsAuthenticated) Then ' The user is still not validated. MsgBox("The username and password pair is incorrect") Else ' Update the current principal. My.User.CurrentPrincipal = samplePrincipal Me.Close() End If
Testing the Application
Now that the application has authentication code, you can run the application and attempt to authenticate a user.
To test the application
Start the application.
Click Authenticate.
The login form opens.
Enter TestUser in the User name box and BadPassword in the Password box, and then click OK.
A message box opens stating that user name and password pair is incorrect.
Click OK to dismiss the message box.
Click Cancel to dismiss the login form.
The labels in the main form now state User not authenticated and User is not an Administrator.
Click Authenticate.
The login form opens.
Enter TestUser in the User Name text box and Password in the Password text box, and then click OK. Make sure that the password is entered with the correct capitalization.
The labels in the main form now say Authenticated TestUser and User is an Administrator.
See Also
Tasks
How to: Connect to Data in a Database
Concepts
Reference
Other Resources
Authentication and Authorization in the .NET Framework with Visual Basic