Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to authenticate against Active Directory via LDAP over TLS?

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 have to do so over TLS -- the domain controller closes any connection which does not initiate via TLS.

I have installed the LDAP browser in Eclipse, and I can indeed bind as myself using TLS in it, but I cannot for the life of me figure out how to get my application to use TLS.

ldap.xml:

<bean id="ldapAuthenticationProvider"
        class="my.project.package.OverrideActiveDirectoryLdapAuthenticationProvider">

    <!-- this works to authenticate by binding as the user in question -->
    <constructor-arg value="test.server"/>
    <constructor-arg value="ldap://192.168.0.2:389"/>

    <!-- this doesn't work, because the server requires a TLS connection -->
    <!-- <constructor-arg value="production.server"/> -->
    <!-- <constructor-arg value="ldaps://192.168.0.3:389"/> -->

    <property name="convertSubErrorCodesToExceptions" value="true"/>
</bean>

OverrideActiveDirectoryLdapAuthenticationProvider is an override class which extends a copy of Spring's ActiveDirectoryLdapAuthenticationProvider class, which is for some reason designated final. My reasons for overriding have to do with customizing the way permissions/authorities are populated on the user object (we will either use group membership of relevant groups to build the user's permissions, or we will read from a field on the AD user object). In it, I'm only overriding the loadUserAuthorities() method, but I suspect I may also need to override the bindAsUser() method or possibly the doAuthentication() method.

The XML and one override class are the only two places where authentication is being managed by my application as opposed to letting Spring do the work. I've read several places that to enable TLS I need to extend the DefaultTlsDirContextAuthenticationStrategy class, but where do I wire it in? Is there a namespace solution? Do I need to do something else entirely (i.e. abandon the use of Spring's ActiveDirectoryLdapAuthenticationProvider and instead use LdapAuthenticationProvider)?

Any help is appreciated.

like image 835
cabbagery Avatar asked Jan 13 '23 09:01

cabbagery


1 Answers

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.

like image 83
cabbagery Avatar answered Jan 17 '23 16:01

cabbagery