Reporting on Smart Card Logon in Active Directory

Yesterday, the higher-ups needed a report of all users in Active Directory whose accounts required Smart Cards to log into the Domain (and those that did not). Most things I have needed to retrieve out of Active Directory have been quick to find and usually obvious, but this particular information was a bit more elusive. Any AD guru would certainly know exactly where to look, but alas, I did not. As I looked around Google and on all the properties on the User object in AD, nothing obvious came up – then I stumbled across this KB article: kb305144.

The Smart Card Required for Interactive Logon check box located in the property pane for a User in Active Directory Users and Computers mmc snap-in is stored as a bit field in the UserAccountControl property of the User object in Active Directory. Once I saw it was a bit field and I had all the possible values, the rest is simple.

Here the quick and dirty code I wrote to run the report:

using System.DirectoryServices;

namespace AdSmartCardReport
{
public class AdUtility
{
// This retrieves the root entry point for AD for
// current DOMAIN
internal static string ADsDomainName
{
get
{
DirectoryEntry rootEntry
=
new DirectoryEntry("LDAP://RootDSE");
return "LDAP://" +
(
string)rootEntry.Properties
[
"defaultNamingContext"][0];
}
}

public void RunReport()
{
DirectoryEntry _entry
=
new DirectoryEntry(AdUtility.ADsDomainName);

_entry.AuthenticationType
=
AuthenticationTypes.FastBind
|
AuthenticationTypes.ReadonlyServer;

DirectorySearcher searcher
=
new DirectorySearcher(_entry);

searcher.SearchScope
= SearchScope.Subtree;
searcher.Filter
= "(&(objectCategory=person)" +
"(objectClass=user)(!" +
"useraccountcontrol:1.2.840.113556.1.4.803:=2))";
searcher.PropertiesToLoad.Add(
"UserAccountControl");
searcher.PropertiesToLoad.Add(
"samAccountName");
searcher.PropertiesToLoad.Add(
"DisplayName");
searcher.PropertiesToLoad.Add(
"mail");

try
{
SearchResultCollection results
=
searcher.FindAll();
int counter = 0;

foreach (SearchResult result in results)
{
ResultPropertyCollection collection
=
result.Properties;

string displayName =
(
string)collection["DisplayName"][0];
string email = (string)collection["mail"][0];
string samAccountName =
(
string)collection["samAccountName"][0];

UserAccountControl userAccountControl
=
(UserAccountControl)collection
[
"UserAccountControl"][0];

if ((userAccountControl &
UserAccountControl.SMARTCARD_REQUIRED)
== UserAccountControl.SMARTCARD_REQUIRED)
{
counter
++;
}

// Write out to console in csv format
System.Console.Write("\"");
System.Console.Write(displayName);
System.Console.Write(
"\",\"");
System.Console.Write(email);
System.Console.Write(
"\",\"");
System.Console.Write(samAccountName);
System.Console.Write(
"\",\"");
System.Console.Write(userAccountControl);
System.Console.WriteLine(
"\"");
}

System.Console.WriteLine(
"There are " + counter +
"users required to use a Smart Card to logon " +
"in the domain.")
);
}
finally
{
if (_entry != null)
{
_entry.Close();
_entry.Dispose();
}
}
}
}

// See: http://support.microsoft.com/kb/305144/ for more
// info on UserAccountControl flag in Active Directory
[Flags()]
public enum UserAccountControl
{
SCRIPT
= 0x1,
ACCOUNTDISABLE
= 0x2,
HOMEDIR_REQUIRED
= 0x8,
LOCKOUT
= 0x10,
PASSWD_NOTREQD
= 0x20,
PASSWD_CANT_CHANGE
= 0x40,
ENCRYPTED_TEXT_PWD_ALLOWED
= 0x80,
TEMP_DUPLICATE_ACCOUNT
= 0x100,
NORMAL_ACCOUNT
= 0x200,
INTERDOMAIN_TRUST_ACCOUNT
= 0x800,
WORKSTATION_TRUST_ACCOUNT
= 0x1000,
SERVER_TRUST_ACCOUNT
= 0x2000,
DONT_EXPIRE_PASSWORD
= 0x10000,
MNS_LOGON_ACCOUNT
= 0x20000,
SMARTCARD_REQUIRED
= 0x40000,
TRUSTED_FOR_DELEGATION
= 0x80000,
NOT_DELEGATED
= 0x100000,
USE_DES_KEY_ONLY
= 0x200000
DONT_REQ_PREAUTH
= 0x400000,
PASSWORD_EXPIRED
= 0x800000,
TRUSTED_TO_AUTH_FOR_DELEGATION
= 0x1000000
}
}


I hope others may find this helpful.

UPDATE: You can also filter the search results using the same UserAccountControl enum.

Let's inspect the following expression:

(&(objectCategory=person)(objectClass=user)(useraccountcontrol:1.2.840.113556.1.4.803:=262144))

decodes to:

Filter on AD Records where (objectCategory = Person) && (objectClass=user) && (UserAccountControl & 0x40000)

The number sequence 1.2.840.113556.1.4.803 tells AD to to a bitwise AND operation on the value in useraccountcontrol and 262144 (0x40000 hex). See kb269181 on how to query Active Directory by using a bitwise filter.

Using a bitwise AND (&) on UserAccountControl and 0x40000 will return true (bit 1) if that bit field is set.


If you found this article helpful: kick it on DotNetKicks.com

10 comments:

  1. Great post, but what if you want to find the users who are not required to use their smart card for login?

    ReplyDelete
  2. Ah, simple enough! Prefix the filter for useraccountcontrol with an exclimation point ('!' which means NOT) like this:

    (&
      (objectCategory=person)
      (objectClass=user)
      (!useraccountcontrol:
      1.2.840.113556.1.4.803:=262144)
    )

    ReplyDelete
  3. Do you happen to know what the query might be in order to find out who has OWA enabled under the exchange features for a users profile? We cant seem to figure out what the value is for it.

    ReplyDelete
  4. Yep,

    You'll want to check msExchUserAccountControl. A value of 0 means they have an enabled account (see: http://support.microsoft.com/kb/296479 ):

    Example query to find ENABLED accounts:

    (&
      (objectCategory=person)
      (objectClass=user)
      (msExchUserAccountControl=0)
    )

    If you want to find accounts that are DISABLED, you can do it this way:

    (&
      (objectCategory=person)
      (objectClass=user)
      (!msExchUserAccountControl=0)
    )

    ReplyDelete
  5. I've been trying to use this query (&(objectClassuser)(homeMDB*)(!protocolSettingsHTTP§0§1§§§§§§)), but when i enter the custom query in AD it says its not a valid query string. Any ideas on what the problem might be??

    ReplyDelete
  6. Clarification on my above comment:

    Values for msExchUserAccountControl:
    • 0: This is an enabled user
    • 2: This is a disabled user

    It is better to check for (msExchUserAccountControl=2) to find DISABLED accounts.

    ReplyDelete
  7. I tried that query in AD, and it still says its not a valid query string? Any ideas on what I might be doing wrong? This is how im entering into the query, i've tried it exactly the way you have it on this page as well and that wont work either.

    (&(objectCategory=person)(objectClass=user)(msExchUserAccountControl=0))

    ReplyDelete
  8. Interesting....I add the spaces and CR, LF for readability only. You won't want to include the formatting into your queries.

    Are you using ActiveDirectory Users and Computers Saved Queries to test your query first? Or are you doing these directly in code?

    I'm not sure your query posted correctly. Is this the query you were attempting?

    (&(objectCategory=person)(homeMDB=*)(protocolSettings=*http§0*))

    Also, there are also a bunch of pre-defined queries they try and help you build, but I like to use the "Custom Query" and write them myself. If you do use Custom Queries to build your LDAP query, it likes to automatically add the AND condition (& ... ) around your query (so I leave it off my query and it adds it back in).

    ReplyDelete
  9. Thanks for all our help..I was able to figure it out by combining your query with mine. It shows all users that have owa enabled under exchange features in the users profile.

    (&(objectCategory=person)(homeMDB=*)(!protocolSettings=*HTTP§0§1§§§§§§))

    ReplyDelete
  10. No problem. Glad you got it working.

    ReplyDelete