I have an app I built that helps me with some tasks such as adding users to groups, removing, creating users, deleting users, and unlocking users in Active Directory. Which all require some admin privileges.
Currently, the way it authenticates the admin account connection to the domain is password-based authentication.
The user gets prompted to provide a username and password and using PrincipalContext
I authenticate the session. Throughout the app I reference the context
variable to perform these tasks.
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, _myDomainName, adminUsername, adminPassword))
I want to check the login to the app to use Smart Card. I was able to use that, but it seems like PrincipalContext
only support password-based authentication there is not certification-based authentication as far as I can tell. This lead to context
variable to not be able to reference my admin account in the session, causing "Access Denied" errors.
Currently I authenticate the user like this:
static X509Certificate2 GetAdminCertificate()
{
Console.Write("Enter admin username: ");
adminUsername = Console.ReadLine().Trim();
/*
Console.Write("Enter your smart card PIN: ");
string smartCardPin = PasswordManager.GetPassword().Trim();
*/
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection yubiKeyCerts = new X509Certificate2Collection();
foreach (var cert in store.Certificates)
{
// First, check if the subject name matches the admin username
string subjectName = cert.GetNameInfo(X509NameType.SimpleName, false);
if (!subjectName.Equals(adminUsername, StringComparison.OrdinalIgnoreCase))
{
// Skip this certificate if the subject does not match
continue;
}
try
{
if (cert.HasPrivateKey)
{
// Access the private key
var privateKey = cert.PrivateKey;
if (privateKey is RSACng rsaCng)
{
if (rsaCng.KeyExchangeAlgorithm.Equals("RSA", StringComparison.OrdinalIgnoreCase))
{
// Match the certificate with admin username
if (subjectName.Equals(adminUsername, StringComparison.OrdinalIgnoreCase))
{
// Attempt to sign data with the RSA key to validate access
byte[] dataToSign = new byte[] { 0x01 }; // Dummy data
byte[] signedData = rsaCng.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
yubiKeyCerts.Add(cert);
} // end of if-statement
} // end of if-statement
} // end of if-statement
} // end of if-statement
} // end of try
catch (Exception ex)
{
Console.WriteLine($"Error obtaining certification: {ex.Message}");
continue;
} // end of catch
} // end of foreach
if (yubiKeyCerts.Count == 0)
{
Console.WriteLine("No smart card detected or no valid certificates found on the connected smart card.");
return null;
}
// X509Certificate2 selectedCert = null;
if (yubiKeyCerts.Count > 0)
{
selectecdCert = X509Certificate2UI.SelectFromCollection(
yubiKeyCerts,
"Select a YubiKey certificate",
"Please select your admin certificate from the YubiKey",
X509SelectionFlag.SingleSelection
)[0];
}
adminUsername = selectecdCert.GetNameInfo(X509NameType.SimpleName, false);
// adminPassword = selectecdCert.Verify();
return selectecdCert;
} // end of using x509Store
} // end of GetAdminCertificate
(too many if statements - I know)
The app connects to the domain with follow:
static void Main(string[] args)
{
ActiveDirectoryManager ADManager = new ActiveDirectoryManager();
AccountCreationManager ACManager;
AccountDeactivationManager ACCDeactivationManager = new AccountDeactivationManager();
PasswordManager PWDManager = null;
ADGroupActionManager ADGroupManager = null;
AuditLogManager auditLogManager = null;
configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Appsettings.json", optional: false, reloadOnChange: true)
.Build();
EmailNotifcationManager emailManager = new EmailNotifcationManager(configuration);
string _myDomainName = configuration["AccountCreationSettings:myDomainName"];
do
{
X509Certificate2 certificate = GetAdminCertificate();
if (certificate == null)
{
Console.WriteLine("No valid smart card certificate found.");
return;
}
try
{
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, _myDomainName))
// Check if the the password/user are correct
{
// Throw error if the password/username is incorrect
if (context.ConnectedServer != null)
{
isAuthenticated = true;
Console.WriteLine($"Connected to Active Directory as: {adminUsername}.".Pastel(Color.GreenYellow));
auditLogManager = new AuditLogManager(adminUsername, configuration);
ADGroupManager = new ADGroupActionManager(auditLogManager);
PWDManager = new PasswordManager(auditLogManager);
ACManager = new AccountCreationManager(auditLogManager, configuration);
bool exit = false;
// Loop the menu
while (!exit)
{
DisplayMainMenu();
string choice = Console.ReadLine();
exit = HandleMainMenuChoice(choice, context, ADManager, ADGroupManager, PWDManager, ACManager, ACCDeactivationManager);
} // end of while-loop
} // end of if statement
context.Dispose();
} // end of using
} // end of try
catch (DirectoryServicesCOMException)
{
// Error out if password/username are incorrect
Console.WriteLine("Error: Unable to connect to the Active Directory server. Please check your credentials and try again.".Pastel(Color.IndianRed));
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}".Pastel(Color.IndianRed));
} // end of Catch
} while (!isAuthenticated || string.IsNullOrEmpty(adminUsername));
// Repeat until a valid password is entered
} // end of Main Method
Example of a snippet of code I get Access denied. As you see I reference the context
variable here to use the admin session to perform these tasks.
UserPrincipal user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username);
// Check for user in AD
if (user != null)
{
// Check for group in AD
GroupPrincipal group = GroupPrincipal.FindByIdentity(context, groupName);
if (group != null)
{
// If the user is not in the group add him
if (!group.Members.Contains(user))
{
// Add the user to the group
group.Members.Add(user);
// Apply changes
group.Save();
group.Dispose();
Console.WriteLine($"User '{username}' added to group '{groupName}' successfully.".Pastel(Color.LimeGreen));
string logEntry = ($"\"{user.DisplayName}\" has been added to \"{groupName}\" group in Active Directory\n");
emailActionLog.Add(logEntry);
auditLogManager.Log(logEntry);
} // end of inner-2 if-statement
else
{
Console.WriteLine($"User '{username}' is already a member of group '{groupName}'.".Pastel(Color.DarkGoldenrod));
} // end of inner-2 else-statement
} // end of inner if-statement
else
{
Console.WriteLine($"Group '{groupName}' not found in Active Directory.".Pastel(Color.IndianRed));
} // end of outer else-statement
}
My main question, is there a way I could Authenticate my admin account with a certification-based logon?
I have an app I built that helps me with some tasks such as adding users to groups, removing, creating users, deleting users, and unlocking users in Active Directory. Which all require some admin privileges.
Currently, the way it authenticates the admin account connection to the domain is password-based authentication.
The user gets prompted to provide a username and password and using PrincipalContext
I authenticate the session. Throughout the app I reference the context
variable to perform these tasks.
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, _myDomainName, adminUsername, adminPassword))
I want to check the login to the app to use Smart Card. I was able to use that, but it seems like PrincipalContext
only support password-based authentication there is not certification-based authentication as far as I can tell. This lead to context
variable to not be able to reference my admin account in the session, causing "Access Denied" errors.
Currently I authenticate the user like this:
static X509Certificate2 GetAdminCertificate()
{
Console.Write("Enter admin username: ");
adminUsername = Console.ReadLine().Trim();
/*
Console.Write("Enter your smart card PIN: ");
string smartCardPin = PasswordManager.GetPassword().Trim();
*/
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection yubiKeyCerts = new X509Certificate2Collection();
foreach (var cert in store.Certificates)
{
// First, check if the subject name matches the admin username
string subjectName = cert.GetNameInfo(X509NameType.SimpleName, false);
if (!subjectName.Equals(adminUsername, StringComparison.OrdinalIgnoreCase))
{
// Skip this certificate if the subject does not match
continue;
}
try
{
if (cert.HasPrivateKey)
{
// Access the private key
var privateKey = cert.PrivateKey;
if (privateKey is RSACng rsaCng)
{
if (rsaCng.KeyExchangeAlgorithm.Equals("RSA", StringComparison.OrdinalIgnoreCase))
{
// Match the certificate with admin username
if (subjectName.Equals(adminUsername, StringComparison.OrdinalIgnoreCase))
{
// Attempt to sign data with the RSA key to validate access
byte[] dataToSign = new byte[] { 0x01 }; // Dummy data
byte[] signedData = rsaCng.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
yubiKeyCerts.Add(cert);
} // end of if-statement
} // end of if-statement
} // end of if-statement
} // end of if-statement
} // end of try
catch (Exception ex)
{
Console.WriteLine($"Error obtaining certification: {ex.Message}");
continue;
} // end of catch
} // end of foreach
if (yubiKeyCerts.Count == 0)
{
Console.WriteLine("No smart card detected or no valid certificates found on the connected smart card.");
return null;
}
// X509Certificate2 selectedCert = null;
if (yubiKeyCerts.Count > 0)
{
selectecdCert = X509Certificate2UI.SelectFromCollection(
yubiKeyCerts,
"Select a YubiKey certificate",
"Please select your admin certificate from the YubiKey",
X509SelectionFlag.SingleSelection
)[0];
}
adminUsername = selectecdCert.GetNameInfo(X509NameType.SimpleName, false);
// adminPassword = selectecdCert.Verify();
return selectecdCert;
} // end of using x509Store
} // end of GetAdminCertificate
(too many if statements - I know)
The app connects to the domain with follow:
static void Main(string[] args)
{
ActiveDirectoryManager ADManager = new ActiveDirectoryManager();
AccountCreationManager ACManager;
AccountDeactivationManager ACCDeactivationManager = new AccountDeactivationManager();
PasswordManager PWDManager = null;
ADGroupActionManager ADGroupManager = null;
AuditLogManager auditLogManager = null;
configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Appsettings.json", optional: false, reloadOnChange: true)
.Build();
EmailNotifcationManager emailManager = new EmailNotifcationManager(configuration);
string _myDomainName = configuration["AccountCreationSettings:myDomainName"];
do
{
X509Certificate2 certificate = GetAdminCertificate();
if (certificate == null)
{
Console.WriteLine("No valid smart card certificate found.");
return;
}
try
{
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, _myDomainName))
// Check if the the password/user are correct
{
// Throw error if the password/username is incorrect
if (context.ConnectedServer != null)
{
isAuthenticated = true;
Console.WriteLine($"Connected to Active Directory as: {adminUsername}.".Pastel(Color.GreenYellow));
auditLogManager = new AuditLogManager(adminUsername, configuration);
ADGroupManager = new ADGroupActionManager(auditLogManager);
PWDManager = new PasswordManager(auditLogManager);
ACManager = new AccountCreationManager(auditLogManager, configuration);
bool exit = false;
// Loop the menu
while (!exit)
{
DisplayMainMenu();
string choice = Console.ReadLine();
exit = HandleMainMenuChoice(choice, context, ADManager, ADGroupManager, PWDManager, ACManager, ACCDeactivationManager);
} // end of while-loop
} // end of if statement
context.Dispose();
} // end of using
} // end of try
catch (DirectoryServicesCOMException)
{
// Error out if password/username are incorrect
Console.WriteLine("Error: Unable to connect to the Active Directory server. Please check your credentials and try again.".Pastel(Color.IndianRed));
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}".Pastel(Color.IndianRed));
} // end of Catch
} while (!isAuthenticated || string.IsNullOrEmpty(adminUsername));
// Repeat until a valid password is entered
} // end of Main Method
Example of a snippet of code I get Access denied. As you see I reference the context
variable here to use the admin session to perform these tasks.
UserPrincipal user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username);
// Check for user in AD
if (user != null)
{
// Check for group in AD
GroupPrincipal group = GroupPrincipal.FindByIdentity(context, groupName);
if (group != null)
{
// If the user is not in the group add him
if (!group.Members.Contains(user))
{
// Add the user to the group
group.Members.Add(user);
// Apply changes
group.Save();
group.Dispose();
Console.WriteLine($"User '{username}' added to group '{groupName}' successfully.".Pastel(Color.LimeGreen));
string logEntry = ($"\"{user.DisplayName}\" has been added to \"{groupName}\" group in Active Directory\n");
emailActionLog.Add(logEntry);
auditLogManager.Log(logEntry);
} // end of inner-2 if-statement
else
{
Console.WriteLine($"User '{username}' is already a member of group '{groupName}'.".Pastel(Color.DarkGoldenrod));
} // end of inner-2 else-statement
} // end of inner if-statement
else
{
Console.WriteLine($"Group '{groupName}' not found in Active Directory.".Pastel(Color.IndianRed));
} // end of outer else-statement
}
My main question, is there a way I could Authenticate my admin account with a certification-based logon?
Share Improve this question edited Nov 20, 2024 at 5:17 marc_s 756k184 gold badges1.4k silver badges1.5k bronze badges asked Nov 19, 2024 at 21:52 MaizMaiz 211 silver badge6 bronze badges1 Answer
Reset to default 1You may either;
Setup a Certification Authority (CA) and issue smart card certificates OR
Get smart card certificates from a 3rd party certificate provider
In first option, if you setup a Microsoft CA, it can issue smart card certificates that you can directly use for logging on to AD (Active Directory) domain, it will see the CA as trusted root authority and automatically map UPN (user principal name) part of the certificate (which should be on CN field) to the AD user.
In the second option you should distribute to root CA of the provider to the client PCs and follow the procedure at https://learn.microsoft/en-us/troubleshoot/windows-server/certificates-and-public-key-infrastructure-pki/import-third-party-ca-to-enterprise-ntauth-store. This procedure is required for smart card logons.
In both options the client PCs that your application will run should be AD domain members.
In your application, I think you should check UPN information at CN (common name) field and check that root certificate of the smart card certificate chain is trusted by the Operating System and this root CA certificate exists in NTAUTH store as described in the above link. Then UPN may should be mapped to the AD user.