One of the main reasons I started this blog was so that I would have a place to “give back” to the software development community. For those that read my blog for more personal updates, you’ll want to stop here. My daily work life depends heavily on open source projects, discussion forums and blogs posts like this one. In this post I’ll be sharing what I’ve learned regarding programmatic authentication to Active Directory for a custom website authentication scheme.
I recently redeveloped a centralized web-based authentication service for a large corporate intranet that leveraged RSA Access Manager as a web single sign-on (SSO) provider. The authentication service validates credentials against Active Directory and handles cases of expired password resets and locked accounts. Once a credential has been validated, the user Id is asserted to RSA Access Manager, creating a new session and then passing control over to RSA and redirecting onto the requested resource.
A few key points:
As I am a Java developer with no previous .Net experience, the first attempt to externalize authentication from RSA Access Manager was in Java via JAAS/GSS-API and then JNDI. Without going into detail, this turned out to be difficult given the diversity of the AD infrastructure.
Next, since we had other needs to interface with Active Directory from our applications, I decided on a new approach. I developed a .Net Web Services API in C# to interface with our Active Directory infrastructure. The Web Services API encapsulated the complexity of the environment and allowed the caller to search, retrieve, and manipulate AD users and group information. This is now how we integrate our Java applications with AD. As part of this implementation in the same ASP.Net web application, I implemented the centralized authentication HTML service. Next I’ll dive into three iterations of the authentication functionality. I’ll save anything outside of the authentication portion for other posts if there’s interest.
The first iteration leveraged Integrated Windows Authentication (IWA) via IIS. I won’t go into detail on this but will say we abandoned that approach because of the regular use of shared workstations and auto-login accounts.
The second iteration used LDAP via ADSI:
Given a domain, samAccountName and password, attempt to bind to the root of the domain of the user. If successful, continue. If the bind failed, one of these cases is true: the credential was invalid, the account password is expired (including “must change password on next login” situations, the account is locked (too many bad attempts) or the account is disabled. Therefore, bind with a known system credential to check the account status. If the account password is expired, notify the user and prompt the user to reset their password. Otherwise, if the account is locked, disabled, or looks OK, return that the authentication was unsuccessful.
Below are several snippets of code:
public static AuthenticationResult authAndStatus(string domain, string samAccountName,
string password, String adminUsername, String adminPassword)
{
String domainAndUsername = domain + "\\\\" + samAccountName;
DirectoryEntry de = getDirectoryEntry(domain, domainAndUsername, password);
AuthenticationResult result = new AuthenticationResult();
DirectoryEntry userDE = null;
try
{
// Bind to the native AdsObject to force authentication.
Object obj = de.NativeObject;
result.authResult = AuthenticationResult.SUCCESSFUL;
}
catch (Exception ex)
{
// the bind failed
result.authResult = AuthenticationResult.FAILED;
}
try
{
// get the account status
de = getDirectoryEntry(domain, adminUsername, adminPassword);
userDE = ADUtilities.getUserDE(de, "samAccountName", samAccountName,
adminUsername, adminPassword);
if (userDE != null)
{
AccountStatus accountStatus = new AccountStatus(userDE);
result.accountStatus = accountStatus;
}
else
{
result.authResult = AuthenticationResult.INVALID;
}
}
catch (Exception ex)
{
// need to trap, basically don't let the status check break
//the authentication
}
return result;
}
public static DirectoryEntry getUserDE(DirectoryEntry de, String propertyName, String propertyValue, String adminUsername, String adminPassword)
{
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = "(" + propertyName + "=" + propertyValue + ")";
SearchResult result = search.FindOne();
if (result != null) {
return new DirectoryEntry(result.Path, adminUsername, adminPassword);
} else {
return null;
}
}
public static DirectoryEntry getDirectoryEntry(String domain, String adminUsername, String adminPassword)
{
String dePath = null;
String adForestName = (String)System.AppDomain.CurrentDomain.GetData("adForestName");
String forestDomainName = adForestName.Substring(0, adForestName.IndexOf('.'));
String gcDNS = (String)System.AppDomain.CurrentDomain.GetData("adForestPreferredGlobalCatalog");
if (domain != null && domain.ToUpper().Contains(".") && domain.ToUpper().Contains(adForestName.ToUpper()))
{
domain = (domain.Split(new char[] { '.' }))[0];
}
// if the domain is not null and not All
if (domain != null && !domain.Trim().Equals("") && !domain.Equals("All"))
{
String fqdn = domain + "." + adForestName;
// check if this is the root domain and if so rest the fqdn to the forest name
if (domain.ToUpper().Equals(forestDomainName.ToUpper()))
fqdn = adForestName;
String ldapDCs = getLDAPDCs(fqdn);
dePath = "LDAP://" + fqdn + "/" + ldapDCs;
}
else
{
String ldapDCs = getLDAPDCs(adForestName);
// if the domain is null assume the global catalog
dePath = "LDAP://" + gcDNS + ":3268/" + ldapDCs;
}
DirectoryEntry de = new DirectoryEntry(dePath, adminUsername, adminPassword, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
return de;
}
public static DirectoryEntry getGCDirectoryEntryForDomain(String domain, String adminUsername, String adminPassword)
{
String dePath = null;
String adForestName = (String)System.AppDomain.CurrentDomain.GetData("adForestName");
String forestDomainName = adForestName.Substring(0, adForestName.IndexOf('.'));
String gcDNS = (String)System.AppDomain.CurrentDomain.GetData("adForestPreferredGlobalCatalog");
dePath = "LDAP://" + gcDNS + ":3268/" + getLDAPDCs(domain + "." + adForestName);
DirectoryEntry de = new DirectoryEntry(dePath, adminUsername, adminPassword, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
return de;
}
public static String getLDAPDCs(String fqdn)
{
String ldapDCs = "";
if (fqdn != null) {
char[] separator = new char[1];
separator[0] = '.';
String[] parts = fqdn.Split(separator);
for (int i = 0; i < parts.Length; i++) {
ldapDCs = ldapDCs + "dc=" + parts[i];
if (i + 1 != parts.Length)
ldapDCs = ldapDCs + ",";
}
}
return ldapDCs;
}
In my next post I describe how well this approach actually worked and the issues that prompted the search for another solution. Then I’ll go into using SSPI and then the use of the LogonUser native function call…
I FINALLY posted a follow-up to this here





6 responses so far ↓
1 Jibbers42 // Jan 29, 2008 at 2:41 am
I really enjoyed this article. Thank you.
Do you think you’ll get around to discussing the “issues that prompted the search for another solution” and how to use SSPI and LogonUser?
2 Mike // Jan 29, 2008 at 2:22 pm
I had all the best intentions of posting a follow up to this post, but you know how that goes. I’ll try and get the follow-up written by the end of the week.
Thanks!
Mike
3 Steve Smith // Jul 14, 2008 at 3:19 am
The code blocks are too thin. Users have to scroll to the bottom of the code block to get to the scroll bar to scroll to the right to see the code on the right hand side.
4 Jay // Jul 22, 2008 at 1:08 am
If the user must change password at the next log on, I need to validate the password which the user entered initially and should prompt the user to provide new password if the password was correct.
I am getting an exception at the line:
Object obj = de.NativeObject;
“Logon failure: unknown user name or bad password”
Could you please help how handle this situation?
Thanks,
Jay
5 dubs // Aug 24, 2008 at 10:49 pm
Question about the AuthenticationResult? Where is the code for this class, or is this built into .net?
6 Active Directory Authentication Part II // Sep 27, 2008 at 12:19 am
[...] is a long overdue follow-up to my Active Directory Authentication post. I had all the best intentions of quickly following this post up with a second post so this is [...]
Leave a Comment