I have a working proof-of-concept application which can successfully authenticate against Active Directory via LDAP on a test server, but the production application will hav
Okay, so after about a day and a half of working on it, I figured it out.
My original approach was to extend Spring's ActiveDirectoryLdapAuthenticationProvider
class, and override its loadUserAuthorities()
method, so as to customize the way the authenticated user's permissions were built. For unobvious reasons, the ActiveDirectoryLdapAuthenticationProvider
class is designated as final
, so of course I cannot extend it.
Thankfully, open source provides for hacking (and that class' superclasses are not final
), so I simply copied the entire contents of it, removed the final
designation, and adjusted the package and class references accordingly. I did not edit any code in this class, except to add a highly visible comment which says not to edit it. I then extended this class in OverrideActiveDirectoryLdapAuthenticationProvider
, which I also referenced in my ldap.xml
file, and in it added an override method for loadUserAuthorities
. All of that worked great with a simple LDAP bind over an unencrypted session (on an isolated virtual server).
The real network environment requires that all LDAP queries begin with a TLS handshake, however, and the server being queried is not the PDC -- its name is 'sub.domain.tld`, but the user is properly authenticated against 'domain.tld.' Also, the username must be prepended with 'NT_DOMAIN\' in order to bind. All of this required customization work, and unfortunately, I found little or no help anywhere.
So here are the preposterously simple changes, all of which involve further overrides in OverrideActiveDirectoryLdapAuthenticationProvider
:
@Override
protected DirContext bindAsUser(String username, String password) {
final String bindUrl = url; //super reference
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.SECURITY_AUTHENTICATION, "simple");
//String bindPrincipal = createBindPrincipal(username);
String bindPrincipal = "NT_DOMAIN\\" + username; //the bindPrincipal() method builds the principal name incorrectly
env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
env.put(Context.PROVIDER_URL, bindUrl);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxtFactory");
//and finally, this simple addition
env.put(Context.SECURITY_PROTOCOL, "tls");
//. . . try/catch portion left alone
}
That is, all I did to this method was change the way the bindPrincipal
string was formatted, and I added a key/value to the hashtable.
I did not have to remove the subdomain from the domain
parameter passed to my class, because that was being passed by ldap.xml
; I simply changed the parameter there to <constructor-arg value="domain.tld"/>
Then I changed the searchForUser()
method in OverrideActiveDirectoryLdapAuthenticationProvider
:
@Override
protected DirContextOperations searchForUser(DirContext ctx, String username) throws NamingException {
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
//this doesn't work, and I'm not sure exactly what the value of the parameter {0} is
//String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
String searchFilter = "(&(objectClass=user)(userPrincipalName=" + username + "@domain.tld))";
final String bindPrincipal = createBindPrincipal(username);
String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
return SpringSecurityLdapTemplate.searchForSingleEntryInternal(ctx, searchCtls, searchRoot, searchFilter, new Object[]{bindPrincipal});
The last change was to the createBindPrincipal()
method, to build the String properly (for my purposes):
@Override
String createBindPrincipal(String username) {
if (domain == null || username.toLowerCase().endsWith(domain)) {
return username;
}
return "NT_DOMAIN\\" + username;
}
And with the above changes -- which still need cleaned up from all of my testing and headdesking -- I was able to bind and authenticate as myself against Active Directory on the network-proper, capture whatever user object fields I wished, identify group membership, etc.
Oh, and apparently TLS does not require 'ldaps://', so my ldap.xml
simply has ldap://192.168.0.3:389
.
tl;dr:
To enable TLS, copy Spring's ActiveDirectoryLdapAuthenticationProvider
class, remove the final
designation, extend it in a custom class, and override bindAsUser()
by adding env.put(Context.SECURITY_PROTOCOL, "tls");
to the environment hashtable. That's it.
To control more closely the bind username, the domain, and the LDAP querystring, override the applicable methods as appropriate. In my case, I could not identify just what the value of {0}
was, so I removed it entirely and inserted the passed username
string instead.
Hopefully, someone out there finds this helpful.