質問

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.

役に立ちましたか?

解決

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.

他のヒント

Alternatively if you don't mind using spring-ldap and creating a factory class under org.springframework.security.ldap.authentication.ad it is also possible to hack ActiveDirectoryLdapAuthenticationProvider by overriding contextFactory which is allowed for package protected access for testing purposes using following:

package org.springframework.security.ldap.authentication.ad;

import lombok.experimental.UtilityClass;

@UtilityClass
public class ActiveDirectoryLdapAuthenticationProviderFactory {
    private final TlsContextFactory TLS_CONTEXT_FACTORY = new TlsContextFactory();

    public ActiveDirectoryLdapAuthenticationProvider create(String domain, String url, boolean startTls) {
        final var authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(domain, url);
        if (startTls) {
            authenticationProvider.contextFactory = TLS_CONTEXT_FACTORY;
        }
        return authenticationProvider;
    }
}
package org.springframework.security.ldap.authentication.ad;

import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import java.util.Hashtable;

class TlsContextFactory extends ActiveDirectoryLdapAuthenticationProvider.ContextFactory {
    private static final DefaultTlsDirContextAuthenticationStrategy TLS_DIR_CONTEXT_AUTHENTICATION_STRATEGY = new DefaultTlsDirContextAuthenticationStrategy();

    @Override
    DirContext createContext(Hashtable<?, ?> env) throws NamingException {
        final var username = (String) env.remove(Context.SECURITY_PRINCIPAL);
        final var password = (String) env.remove(Context.SECURITY_CREDENTIALS);
        final var context = super.createContext(env);
        return TLS_DIR_CONTEXT_AUTHENTICATION_STRATEGY.processContextAfterCreation(context, username, password);
    }
}

Bonus content: If you don't want to deal with cert/naming issues which is usually the case for AD you can go with following instead:

package org.springframework.security.ldap.authentication.ad;

import com.acme.IgnoreAllTlsDirContextAuthenticationStrategy;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import java.util.Hashtable;

class TlsContextFactory extends ActiveDirectoryLdapAuthenticationProvider.ContextFactory {
    private static final IgnoreAllTlsDirContextAuthenticationStrategy TLS_DIR_CONTEXT_AUTHENTICATION_STRATEGY = new IgnoreAllTlsDirContextAuthenticationStrategy();

    @Override
    DirContext createContext(Hashtable<?, ?> env) throws NamingException {
        final var username = (String) env.remove(Context.SECURITY_PRINCIPAL);
        final var password = (String) env.remove(Context.SECURITY_CREDENTIALS);
        final var context = super.createContext(env);
        return TLS_DIR_CONTEXT_AUTHENTICATION_STRATEGY.processContextAfterCreation(context, username, password);
    }
}
package com.acme;

import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;

public class IgnoreAllTlsDirContextAuthenticationStrategy extends DefaultTlsDirContextAuthenticationStrategy {
    public IgnoreAllTlsDirContextAuthenticationStrategy() {
        setHostnameVerifier((hostname, session) -> true);
        setSslSocketFactory(new NonValidatingSSLSocketFactory());
    }
}
package com.acme;

import lombok.SneakyThrows;
import lombok.experimental.Delegate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;

public class NonValidatingSSLSocketFactory extends SSLSocketFactory {
    @Delegate
    private final SSLSocketFactory delegateSocketFactory;

    @SneakyThrows
    public NonValidatingSSLSocketFactory() {
        SSLContext ctx = SSLContext.getInstance("TLS");

        ctx.init(null, new TrustManager[]{new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) {
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        }}, null);

        delegateSocketFactory = ctx.getSocketFactory();
    }
}

PS: For the sake of code readability Lombok is used. Naturally it is optional and can be easily removed.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top