It is a good practice to ensure that a user gets authenticated before accessing critical feature of your applications.
I have always maintained a table with users and their passwords in a database but with users getting too many passwords to remember, you can now allow them to use the very same password they use to login their corporate computer if you are using an Active Directory account.
It is amazingly easy for a .Net application running on a computer that can access the AD (useless on a laptop not connected to anything!) to validate the credentials of a user.
You can also check the groups to which this user belongs if you want to build your security based on these groups (which I am not a big fan because it is not very flexible).
The downloadable code
This month’s downloadable demo solution contains both VB and C# projects. The solution was created using Visual Studio 2019 but can be used in most older versions as well.
Figure 1: The demo application in action
Reference required
Before being able to use any of the code provided here, you will need to add a reference to your project to a library provided by Microsoft. Nothing to install, just need to reference it. This library is named System.DirectoryServices.AccountManagement.
Figure 2: New reference required
Authentication versus Authorization
Many people consider these two words to be synonyms, but this is not the case.
I have found a simple description to differentiate both terms on this page.
It simply says: Authentication confirms that users are who they say they are. Authorization gives those users permission to access a resource.
Authenticating users
As per the description above, authentication validate a username and a password (aka credentials) provided by a user against a source to ensure the user is really who he says he is (as far as the username and password can check that assumption – not going to explore MFA here).
Using the library cited here above, it is as simple as calling a method named ValidateCredentials as shown in this code snippet:
Private Sub btnValidateCredentials_Click(sender As Object, e As EventArgs) Handles btnValidateCredentials.Click If ValidateCredentials(txtUserName.Text, txtPassword.Text, txtDomain.Text) Then lblResultCredentials.Text = "Credentials are valid" Else lblResultCredentials.Text = "Credentials are NOT valid" End IfEnd SubPublic Function ValidateCredentials(ByVal pUsername As String, ByVal pPassword As String, ByVal pDomain As String) As Boolean Dim blnValid As Boolean = False Try Using context As New PrincipalContext(ContextType.Domain, pDomain) blnValid = context.ValidateCredentials(pUsername, pPassword, ContextOptions.Negotiate) End Using Catch ex As Exception MessageBox.Show($"An exception occured: {ex.Message}") End Try Return blnValidEnd Function
private void btnValidateCredentials_Click(object sender, EventArgs e){ if (ValidateCredentials(txtUserName.Text, txtPassword.Text, txtDomain.Text)) lblResultCredentials.Text = "Credentials are valid"; else lblResultCredentials.Text = "Credentials are NOT valid";}public bool ValidateCredentials(string pUsername, string pPassword, string pDomain){ bool blnValid = false; try { using (PrincipalContext context = new PrincipalContext(ContextType.Domain, pDomain)) { blnValid = context.ValidateCredentials(pUsername, pPassword, ContextOptions.Negotiate); } } catch (Exception ex) { MessageBox.Show($"An exception occured: {ex.Message}"); } return blnValid;}
Getting the list of groups
When a user is created in the active directory, the user is also usually added to groups (that will allow or deny access to resources).
The library in use here offers 2 methods related to those groups. The first method is GetGroups and the second one is GetAuthorizationGroups. I much prefer the second one.
The main difference between these two methods is:
That means that the method GetGroups will not return Group2. Try this on your own account and you might find out the list to be quite different.
Depending on what you are really looking for, you will choose one method or the other.
Private Sub btnGetGroups_Click(sender As Object, e As EventArgs) Handles btnGetGroups.Click lstGroups.Items.Clear() Dim groups As PrincipalSearchResult(Of Principal) = GetUserGroups(txtUserName.Text, txtPassword.Text, txtDomain.Text) For Each result As Principal In groups If TypeOf result Is GroupPrincipal Then Dim gp As GroupPrincipal = DirectCast(result, GroupPrincipal) If gp IsNot Nothing AndAlso gp.IsSecurityGroup Then lstGroups.Items.Add($"Security Group: {gp.Name}") Else lstGroups.Items.Add($"Other Group : {result.DistinguishedName}") End If Else lstGroups.Items.Add($"Other Group : {result.DistinguishedName}") End If NextEnd SubPrivate Function GetUserGroups(ByVal pUsername As String, ByVal pPassword As String, ByVal pDomain As String) As PrincipalSearchResult(Of Principal) Dim objContext As New PrincipalContext(ContextType.Domain, pDomain, pUsername, pPassword) Dim objUser As UserPrincipal = UserPrincipal.FindByIdentity(objContext, IdentityType.SamAccountName, pUsername) Return objUser.GetAuthorizationGroups() 'Return objUser.GetGroups()End Function
private void btnGetGroups_Click(object sender, EventArgs e){ lstGroups.Items.Clear(); PrincipalSearchResult<Principal> groups = GetUserGroups(txtUserName.Text, txtPassword.Text, txtDomain.Text); foreach (Principal result in groups) { if (result is GroupPrincipal) { GroupPrincipal gp = (GroupPrincipal)result; if (gp != null && gp.IsSecurityGroup != null && gp.IsSecurityGroup.Value) lstGroups.Items.Add($"Security Group: {gp.Name}"); else lstGroups.Items.Add($"Other Group : {result.DistinguishedName}"); } else lstGroups.Items.Add($"Other Group : {result.DistinguishedName}"); }}private PrincipalSearchResult<Principal> GetUserGroups(string pUsername, string pPassword, string pDomain){ PrincipalContext objContext = new PrincipalContext(ContextType.Domain, pDomain, pUsername, pPassword); UserPrincipal objUser = UserPrincipal.FindByIdentity(objContext, IdentityType.SamAccountName, pUsername); return objUser?.GetAuthorizationGroups(); //return objUser?.GetGroups();}
IsMemberOf
As said before, you can check the groups from the AD to authorize your users for some resources (or features in your application).
When I looked at the library, I found the method named IsMemberOf to be promising but my tests were not as satisfying as they could be. It seems that the validation is done against the list returned by GetGroups (instead of the full list returned by GetAuthorizationGroups).
So instead of using the built-in IsMemberOf method, I have created my own method as shown in here below to compare against the list returned by GetAuthorizationGroups:
Private Sub btnValidateGroup_Click(sender As Object, e As EventArgs) Handles btnValidateGroup.Click lblResultGroup.Text = ValidateGroup(txtUserName.Text, txtPassword.Text, txtDomain.Text, txtGroup.Text)End SubPublic Function ValidateGroup(ByVal pUsername As String, ByVal pPassword As String, ByVal pDomain As String, ByVal pGroupName As String) As String If String.IsNullOrWhiteSpace(pUsername) OrElse String.IsNullOrWhiteSpace(pPassword) OrElse String.IsNullOrWhiteSpace(pDomain) OrElse String.IsNullOrWhiteSpace(pGroupName) Then Return "Fill the required fields first." End If Try Dim objContext As New PrincipalContext(ContextType.Domain, pDomain, pUsername, pPassword) Dim objUser As UserPrincipal = UserPrincipal.FindByIdentity(objContext, IdentityType.SamAccountName, pUsername) If objUser Is Nothing Then Return "User cannot be found" Else Dim objGroup As GroupPrincipal = GroupPrincipal.FindByIdentity(objContext, pGroupName) If objGroup Is Nothing Then Return "Group does not exist" Else Dim objGroups As PrincipalSearchResult(Of Principal) = objUser.GetAuthorizationGroups() Dim groupResult As Principal = objGroups.Where(Function(x) x.Name.Trim().ToUpper() = pGroupName.Trim().ToUpper()).FirstOrDefault() If groupResult Is Nothing Then Return "User is NOT member of the group" Else Return "User is member of the group" End If End If End If Catch ex As Exception Return $"Exception occured: {ex.Message}" End TryEnd Function
private void btnValidateGroup_Click(object sender, EventArgs e){ lblResultGroup.Text = ValidateGroup(txtUserName.Text, txtPassword.Text, txtDomain.Text, txtGroup.Text);}public string ValidateGroup(string pUsername, string pPassword, string pDomain, string pGroupName){ if (string.IsNullOrWhiteSpace(pUsername) || string.IsNullOrWhiteSpace(pPassword) || string.IsNullOrWhiteSpace(pDomain) || string.IsNullOrWhiteSpace(pGroupName)) return "Fill the required fields first."; try { PrincipalContext objContext = new PrincipalContext(ContextType.Domain, pDomain, pUsername, pPassword); UserPrincipal objUser = UserPrincipal.FindByIdentity(objContext, IdentityType.SamAccountName, pUsername); if (objUser == null) return "User cannot be found"; else { GroupPrincipal objGroup = GroupPrincipal.FindByIdentity(objContext, pGroupName); if (objGroup == null) return "Group does not exist"; else { PrincipalSearchResult<Principal> objGroups = objUser.GetAuthorizationGroups(); Principal groupResult = objGroups.Where(x => x.Name.Trim().ToUpper() == pGroupName.Trim().ToUpper()).FirstOrDefault(); if (groupResult == null) return "User is NOT member of the group"; else return "User is member of the group"; } } } catch (Exception ex) { return $"Exception occured: {ex.Message}"; }}
Conclusion
If you just want a simple mean of ensuring a user provided valid credentials and do not want to maintain a database of password (and most importantly – your user exists in the AD), why not give a try to this simple mechanism?
Particularly important, do not try to authenticate your account in a loop with an incorrect password as you might lock out your account in the Active Directory. Better yet, create a test account if you are planning to test for many incorrect sets of credentials!