(Print this page)

Authenticate and Authorize users against the AD from a .Net application
Published date: Monday, December 28, 2020
On: Moer and Éric Moreau's web site

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 If
End Sub


Public 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 blnValid
End 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:

  • GetGroups returns only the groups of which the principal is directly a member; no recursive searches are performed.
  • GetAuthorizationGroups returns nested groups (e.g: User1 > Group1 > Group2: User1 is indirectly member of Group2 because Group1 is member of Group2.

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
Next
End Sub

Private 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 Sub

Public 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 Try
End 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!


(Print this page)