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

  1. 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.

  2. On the Project menu, click Add New Item.

  3. Select the Login Form template and click Add.

    The default name of the login form is LoginForm1.

  4. On the Project menu, click Add New Item.

  5. Select the Class template, change the name to SampleIIdentity, and then click Add.

  6. On the Project menu, click Add New Item.

  7. Select the Class template, change the name to SampleIPrincipal, and then click Add.

  8. On the Project menu, click <ApplicationName> Properties.

  9. In the Project Designer, click the Application tab.

  10. Change the Authentication mode drop-down to Application-defined.

To configure the main form

  1. Switch to Form1 in the Forms Designer.

  2. Add a Button to Form1 from the Toolbox.

    The default name of the button is Button1.

  3. Change the button text to Authenticate.

  4. From the Toolbox, add a Label to Form1.

    The default name of the label is Label1.

  5. Change the label text to an empty string.

  6. From the Toolbox, add a Label to Form1.

    The default name of the label is Label2.

  7. Change the label text to an empty string.

  8. Double-click Button1 to create the event handler for the Click event, and then open the Code Editor.

  9. 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

  1. Select the SampleIIdentity.vb file in Solution Explorer.

    This class encapsulates a user's identity.

  2. 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.

  3. 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
    
  4. 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"
    
  5. 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.

  6. 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
    
  7. Create a property that returns the user's role.

    Public ReadOnly Property Role() As ApplicationServices.BuiltInRole
        Get 
            Return roleValue
        End Get 
    End Property
    
  8. 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
    
  9. Create a method named IsValidNameAndPassword that determines if a user name and password combination is valid.

    Security noteSecurity 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
    
  10. Create functions named GetHashedPassword and GetSalt that return the hashed password and salt for the specified user.

    Security noteSecurity 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

  1. 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.

  2. 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.

  3. Add a private field to store the identity associated with this principal.

    Private identityValue As SampleIIdentity
    
  4. Enter the following code in the Identity property.

    Return identityValue
    

    The Identity property needs to return the user identity of the current principal.

  5. 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
    
  6. 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

  1. Select LoginForm1 in the designer.

  2. Double-click the OK button to open the Code Editor for the Click event.

  3. 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

  1. Start the application.

  2. Click Authenticate.

    The login form opens.

  3. 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.

  4. Click OK to dismiss the message box.

  5. Click Cancel to dismiss the login form.

    The labels in the main form now state User not authenticated and User is not an Administrator.

  6. Click Authenticate.

    The login form opens.

  7. 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

Accessing User Data

Reference

My.User Object

IIdentity

IPrincipal

Other Resources

Authentication and Authorization in the .NET Framework with Visual Basic