如何通過 LDAP over TLS 對 Active Directory 進行身份驗證? (How to authenticate against Active Directory via LDAP over TLS?)


問題描述

如何通過 LDAP over TLS 對 Active Directory 進行身份驗證? (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.


參考解法

方法 1:

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.

方法 2:

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:

<pre class="lang‑java prettyprint‑override">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;
    }
}
</code></pre>

<pre class="lang‑java prettyprint‑override">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);
    }
}
</code></pre>


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:

<pre class="lang‑java prettyprint‑override">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);
    }
}
</code></pre>

<pre class="lang‑java prettyprint‑override">package com.acme;

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

public class IgnoreAllTlsDirContextAuthenticationStrategy extends DefaultTlsDirContextAuthenticationStrategy {
    public IgnoreAllTlsDirContextAuthenticationStrategy() {
        setHostnameVerifier((hostname, session) ‑> true);
        setSslSocketFactory(new NonValidatingSSLSocketFactory());
    }
}
</code></pre>

<pre class="lang‑java prettyprint‑override">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();
    }
}
</code></pre>

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

(by cabbagerycabbagerySoner Koksal)

參考文件

  1. How to authenticate against Active Directory via LDAP over TLS? (CC BY‑SA 3.0/4.0)

#java #ldap #active-directory #spring-security-ldap #spring-security






相關問題

電子郵件地址中帶有 + 字符的 Java 郵件 (Java mail with + character in email address)

如何快速原型化 Java 代碼? (How to quickly prototype Java code?)

如何使用 Maven 在目標(SVN-)服務器上創建 Javadoc? (How to create Javadoc on the target (SVN-) server using Maven?)

為什麼檢查二叉樹有效性的解決方案不起作用? (Why the solution for checking the validity of binary tree is not working?)

Selenium webdriver通過第一個數字找到texy (Selenium webdriver find texy by first digits)

setOnClickListener 沒有在圖像視圖上被調用 (setOnClickListener is not getting called on image view)

繪製多邊形:找不到錯誤 (Drawing Polygon : unable to find error)

半透明 JButton:對像出現在背景中 (Semi-Transparent JButton: Objects appear in Background)

比較同一數組的元素 (Compare elements of the same array)

Java 屏幕截圖小程序 (Java screen capture applet)

Minecraft 1.8.9 Forge Modding 的Java 開發工具包,需要什麼JDK/JRE,代碼是否正確? (Java Development Kit with Minecraft 1.8.9 Forge Modding, What JDK/JRE Is Needed, Is Code Correct?)

java while (resultset.next()) 不返回同一列中的所有數據 (java while (resultset.next()) does not return all data in the same column)







留言討論