Как пройти аутентификацию в Active Directory через LDAP через TLS?

У меня есть работающее проверочное приложение, которое может успешно пройти аутентификацию в Active Directory через LDAP на тестовом сервере, но производственное приложение должно будет делать это через TLS - контроллер домена закрывает любое соединение, которое не инициируется через TLS.

Я установил браузер LDAP в Eclipse, и я действительно могу выполнить привязку от себя, используя TLS в it, но я не могу, хоть убей, понять, как заставить мое приложение использовать 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 - это класс переопределения, который расширяет копию класса Spring ActiveDirectoryLdapAuthenticationProvider, который по какой-то причине обозначен как final. Мои причины переопределения связаны с настройкой способа заполнения разрешений / полномочий для объекта пользователя (мы либо будем использовать членство в соответствующих группах для создания разрешений пользователя, либо будем читать данные из поля в объекте пользователя AD). В нем я только переопределяю метод loadUserAuthorities(), но подозреваю, что мне также может потребоваться переопределить метод bindAsUser() или, возможно, метод doAuthentication().

XML и один класс переопределения - единственные два места, где мое приложение управляет аутентификацией, а не позволяет Spring делать эту работу. Я читал в нескольких местах, что для включения TLS мне нужно расширить класс DefaultTlsDirContextAuthenticationStrategy, но где его подключить? Есть ли решение для пространства имен? Нужно ли мне делать что-то еще (т.е. отказаться от использования Spring ActiveDirectoryLdapAuthenticationProvider и вместо этого использовать LdapAuthenticationProvider)?

Любая помощь приветствуется.


person cabbagery    schedule 15.05.2013    source источник


Ответы (2)


Хорошо, примерно через полтора дня работы над этим я понял это.

Мой первоначальный подход заключался в том, чтобы расширить класс Spring ActiveDirectoryLdapAuthenticationProvider и переопределить его метод loadUserAuthorities(), чтобы настроить способ создания разрешений аутентифицированного пользователя. По неочевидным причинам класс ActiveDirectoryLdapAuthenticationProvider обозначен как final, поэтому, конечно, я не могу его расширить.

К счастью, открытый исходный код обеспечивает возможность взлома (а суперклассы этого класса не final), поэтому я просто скопировал все его содержимое, удалил обозначение final и соответствующим образом скорректировал ссылки на пакеты и классы. Я не редактировал никакой код в этом классе, за исключением добавления заметного комментария, в котором говорится, что его нельзя редактировать. Затем я расширил этот класс в OverrideActiveDirectoryLdapAuthenticationProvider, на который я также ссылался в своем файле ldap.xml, и добавил в него метод переопределения для loadUserAuthorities. Все это отлично работало с простой привязкой LDAP через незашифрованный сеанс (на изолированном виртуальном сервере).

Однако реальная сетевая среда требует, чтобы все запросы LDAP начинались с подтверждения TLS, а запрашиваемый сервер не является PDC - его имя - 'sub.domain.tld', но пользователь правильно аутентифицирован по 'domain.tld . ' Кроме того, для привязки к имени пользователя должно быть добавлено "NT_DOMAIN \". Все это требовало работы по настройке, и, к сожалению, я нигде почти не нашел помощи.

Итак, вот абсурдно простые изменения, все из которых включают дополнительные переопределения в 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
}

То есть все, что я сделал с этим методом, - это изменил способ форматирования строки bindPrincipal и добавил ключ / значение в хеш-таблицу.

Мне не нужно было удалять субдомен из параметра domain, переданного моему классу, потому что он передавался ldap.xml; Я просто изменил параметр там на <constructor-arg value="domain.tld"/>

Затем я изменил метод searchForUser() в 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});

Последнее изменение касалось метода createBindPrincipal() для правильного построения String (для моих целей):

@Override
String createBindPrincipal(String username) {
    if (domain == null || username.toLowerCase().endsWith(domain)) {
        return username;
    }
    return "NT_DOMAIN\\" + username;
}

И с вышеупомянутыми изменениями, которые все еще нуждаются в очистке от всего моего тестирования и работы с головным офисом, я смог привязать и аутентифицироваться от имени себя в Active Directory в самой сети, захватывать любые поля пользовательских объектов, которые я хотел, идентифицировать членство в группе , и т.д.

Да, и очевидно, что TLS не требует 'ldaps: //', поэтому мой ldap.xml просто имеет ldap://192.168.0.3:389.


tl; dr:

Чтобы включить TLS, скопируйте класс Spring ActiveDirectoryLdapAuthenticationProvider, удалите обозначение final, расширите его до настраиваемого класса и переопределите bindAsUser(), добавив env.put(Context.SECURITY_PROTOCOL, "tls"); в хэш-таблицу среды. Вот и все.

Чтобы более точно контролировать имя пользователя привязки, домен и строку запроса LDAP, при необходимости переопределите применимые методы. В моем случае я не мог точно определить значение {0}, поэтому я полностью удалил его и вместо этого вставил переданную строку username.

Надеюсь, кто-то найдет это полезным.

person cabbagery    schedule 16.05.2013

В качестве альтернативы, если вы не против использования spring-ldap и создания фабричного класса в org.springframework.security.ldap.authentication.ad, также можно взломать ActiveDirectoryLdapAuthenticationProvider, переопределив contextFactory, который разрешен для защищенного доступа к пакету в целях тестирования, используя следующее:

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);
    }
}

Бонусный контент: если вы не хотите иметь дело с проблемами сертификата / именования, что обычно имеет место в AD, вы можете вместо этого использовать следующее:

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: Для читабельности кода используется Lombok. Естественно, это необязательно и легко снимается.

person Soner Koksal    schedule 18.02.2021