14
votes

I have read almost everything about Spring/Security/Ldap and ActiveDirectory on stackoverflow. Even if I found useful tips and hints, I wasn't able to solve my problem.

Here it is: I did configure Spring Security using a user-service and a custom login page and everything is working just fine. Then, I tried to switch to the final authentication provider which happens to be ActiveDirectory.

Here is my security-applicationContext.xml (I remind you this setup is working fine with the user-service as an authentication provider, so, the file is actually imported, etc):

<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/security"
    xmlns:oauth="http://www.springframework.org/schema/security/oauth"
    xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd http://www.springframework.org/schema/security/oauth http://www.springframework.org/schema/security/spring-security-oauth.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">

<!-- Définitions globales de sécurité -->
<global-method-security pre-post-annotations="enabled" />

<!-- Configuration de l'accès et du formulaire -->
<!-- Permettre l'accès libre aux feuilles de style, polices et images -->
<http pattern='/resources/css/**' security="none" />
<http pattern='/resources/fonts/**' security="none" />
<http pattern='/resources/images/**' security="none" />

<http use-expressions="true" access-denied-page="/403" disable-url-rewriting="true">

    <!-- Limitation à une seule session utilisateur concurrente -->
    <session-management invalid-session-url="/identite?time=1">
        <concurrency-control max-sessions="1" expired-url="/identite?time=1" />
    </session-management>

    <!-- Définitions pour le formulaire de la page JSP d'identification -->
    <form-login login-page="/identite" login-processing-url="/identite.proc" default-target-url="/" always-use-default-target="true" authentication-failure-url="/identite?err=1" username-parameter="username" password-parameter="password" />

    <logout logout-url="/logout" logout-success-url="/identite?out=1" delete-cookies="JSESSIONID" invalidate-session="true" />

    <!-- Utiliser un canal chiffré pour les échanges -->
    <intercept-url requires-channel="https" pattern="/identite*" access="permitAll()" />
    <intercept-url requires-channel="https" pattern="/**" access="isAuthenticated()" />
</http>

<!-- Fournisseurs d'identité pour le formulaire (au final, LDAP) -->
<authentication-manager erase-credentials="true">
    <ldap-authentication-provider ref="myADProvider" />
    <!-- This is the user-service authentication provider that is working fine
        <authentication-provider>
        <user-service>
        <user name="Toto" authorities="ROLE_USER, ROLE_ADMIN" password="totototo" />
        </user-service>
        </authentication-provider>
    -->
</authentication-manager>

<b:bean id="myADProvider"
    class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <b:constructor-arg value="fsappsuni" />
    <b:constructor-arg value="ldap://fsapps.company.uni:389/" />
    <b:property name="convertSubErrorCodesToExceptions" value="true" />
    <b:property name="useAuthenticationRequestCredentials" value="true" />
</b:bean>

In the bean myADProvider, even if I change the first constructor argument to fsapps.company.uni or company.uni or whatever else it doesn't change anything to the following error. The problem seems due to the fact after the binding a search is performed with the wrong filter looking for something like (&(userPrincipalName={0})(objectClass=user)) instead of (&(sAMAccountName={0})(objectClass=user)). Since I wasn't able to figure out how to change this while you can do it with the LDAP provider, I then switched to the LDAP provider trying to make it working without success neither stumbling upon a problem about at the same place. I also upgraded my Spring Framework from 3.1.2 to 4.1.6.RELEASE and Spring Security from 3.1.2 to 4.0.0.RELEASE after reading there was an issue with the ActiveDirectoryLdapAuthenticationProvider in the hope the issue would have been resolved in the meantime the original discussions about this issue being with version 3.x.

Here is my configuration for the LDAP setup trying to make the authentication working with Active Directory:

<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/security"
    xmlns:oauth="http://www.springframework.org/schema/security/oauth"
    xsi:schemaLocation="http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-4.0.xsd
    http://www.springframework.org/schema/security/oauth
    http://www.springframework.org/schema/security/spring-security-oauth.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.1.xsd">

<!-- Définitions globales de sécurité -->
<global-method-security pre-post-annotations="enabled" />

<!-- Configuration de l'accès et du formulaire -->
<!-- Permettre l'accès libre aux feuilles de style, polices et images -->
<http pattern='/resources/css/**' security="none" />
<http pattern='/resources/fonts/**' security="none" />
<http pattern='/resources/images/**' security="none" />

<http use-expressions="true" disable-url-rewriting="true">

    <!-- Limitation à une seule session utilisateur concurrente -->
    <session-management invalid-session-url="/identite?time=1">
        <concurrency-control max-sessions="1" expired-url="/identite?time=1" />
    </session-management>

    <!-- Définitions pour le formulaire de la page JSP d'identification -->
    <form-login login-page="/identite" login-processing-url="/identite.proc" default-target-url="/" always-use-default-target="true" authentication-failure-url="/identite?err=1" username-parameter="username" password-parameter="password" />
    <csrf disabled="true" />

    <logout logout-url="/logout" logout-success-url="/identite?out=1" delete-cookies="JSESSIONID" invalidate-session="true" />

    <!-- Utiliser un canal chiffré pour les échanges -->
    <intercept-url requires-channel="https" pattern="/identite*" access="permitAll()" />
    <intercept-url requires-channel="https" pattern="/**" access="isAuthenticated()" />
</http>

<!-- Fournisseurs d'identité pour le formulaire (au final, LDAP) -->
<ldap-server url="ldap://fsapps.company.uni/dc=fsapps,dc=company,dc=uni" port="389" />
<authentication-manager erase-credentials="true">
    <ldap-authentication-provider role-prefix="none"
        user-search-filter="(&amp;(sAMAccountName={0})(objectClass=user))"
        group-search-filter="(&amp;(member={0})(objectClass=group))"
        user-search-base="dc=fsapps,dc=company,dc=uni">
    </ldap-authentication-provider>
    <!-- <authentication-provider ref="myADProvider" ></authentication-provider> -->
</authentication-manager>

<!--
<b:bean id="myADProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <b:constructor-arg value="fsapps.company.uni" />
    <b:constructor-arg value="ldap://fsapps.company.uni:389/" />
    <b:property name="convertSubErrorCodesToExceptions" value="true" />
    <!- -
    <b:property name="useAuthenticationRequestCredentials" value="true" />
    - ->
</b:bean>
<b:bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler"></b:bean>
-->

</b:beans>

This time, I left the bean configuration part with the AD provider for reference. This isn't working neither.

So, how can I pass the user and group search paths to the AD provider? Or alternatively how can I configure the LDAP provider to work with AD and complete authentication?

Here is the message I receive in my log with the LDAP setup, the AD setup is explained above (Bad Credentials essentially due to a wrong search path):

2015-04-15 16:19:13,252 DEBUG (o.s.s.a.ProviderManager.authenticate) [http-8443-1] Authentication attempt using org.springframework.security.ldap.authentication.LdapAuthenticationProvider MDC{}
2015-04-15 16:19:13,252 DEBUG (o.s.s.l.a.AbstractLdapAuthenticationProvider.authenticate) [http-8443-1] Processing authentication request for user: ba5glag MDC{}
2015-04-15 16:19:13,252 DEBUG (o.s.s.l.s.FilterBasedLdapUserSearch.searchForUser) [http-8443-1] Searching for user 'myuser', with user search [ searchFilter: '(&(sAMAccountName={0})(objectClass=user))', searchBase: 'dc=fsapps,dc=company,dc=uni', scope: subtree, searchTimeLimit: 0, derefLinkFlag: false ] MDC{}
2015-04-15 16:19:13,299 DEBUG (o.s.s.a.DefaultAuthenticationEventPublisher.publishAuthenticationFailure) [http-8443-1] No event was found for the exception org.springframework.security.authentication.InternalAuthenticationServiceException MDC{}
2015-04-15 16:19:13,299 ERROR (o.s.s.w.a.AbstractAuthenticationProcessingFilter.doFilter) [http-8443-1] An internal error occurred while trying to authenticate the user. MDC{}
org.springframework.security.authentication.InternalAuthenticationServiceException: Uncategorized exception occured during LDAP processing; nested exception is javax.naming.NamingException: [LDAP: error code 1 - 00000000: LdapErr: DSID-0C090627, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, vece(unprintable character here)]; remaining name 'dc=fsapps,dc=company,dc=uni'
        at org.springframework.security.ldap.authentication.LdapAuthenticationProvider.doAuthentication(LdapAuthenticationProvider.java:207) ~[spring-security-ldap-4.0.0.RELEASE.jar:?]
        at org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider.authenticate(AbstractLdapAuthenticationProvider.java:82) ~[spring-security-ldap-4.0.0.RELEASE.jar:?]

I hope I provide all the required information for someone to provide some hints and guidance to solve this configuration problem.

UPDATE 2015-04-16 ~10:20

I figured out how to add the search filter with the ActiveDirectoryLdapAuthenticationProvider. I also watched with Wireshark the exchange between my application server and the AD server to see what actually was done. Here are my findings, I believe I am not far to a solution to my problem. My updated security-applicationContext.xml, I have thrown away the ldap-server stuff since I concentrate now on making ActiveDirectoryLdapAuthenticationProvider to work. So, now this is a cleaner configuration to read:

<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/security"
    xmlns:oauth="http://www.springframework.org/schema/security/oauth"
    xsi:schemaLocation="http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-4.0.xsd
    http://www.springframework.org/schema/security/oauth
    http://www.springframework.org/schema/security/spring-security-oauth.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.1.xsd">

    <!-- Définitions globales de sécurité -->
    <global-method-security pre-post-annotations="enabled" />

    <!-- Configuration de l'accès et du formulaire -->
    <!-- Permettre l'accès libre aux feuilles de style, polices et images -->
    <http pattern='/resources/css/**' security="none" />
    <http pattern='/resources/fonts/**' security="none" />
    <http pattern='/resources/images/**' security="none" />
    <http pattern='/resources/js/**' security="none" />

    <http use-expressions="true" disable-url-rewriting="true">

        <!-- Limitation à une seule session utilisateur concurrente -->
        <session-management invalid-session-url="/identite?expiree=1">
            <concurrency-control max-sessions="1" expired-url="/identite?expiree=1" />
        </session-management>

        <!-- Définitions pour le formulaire de la page JSP d'identification -->
        <form-login login-page="/identite" login-processing-url="/identite.proc" default-target-url="/" always-use-default-target="true" authentication-failure-url="/identite?err=1" username-parameter="username" password-parameter="password" />
        <csrf disabled="true" />

        <logout logout-url="/logout" logout-success-url="/identite?termine=1" delete-cookies="JSESSIONID" invalidate-session="true" />

        <!-- Utiliser un canal chiffré pour les échanges -->
        <intercept-url requires-channel="https" pattern="/identite*" access="permitAll()" />
        <intercept-url requires-channel="https" pattern="/**" access="isAuthenticated()" />
        <access-denied-handler error-page="/erreur403" />
    </http>

    <!-- Fournisseurs d'identité pour le formulaire (au final, LDAP) -->
    <authentication-manager erase-credentials="true">
         <authentication-provider ref="myADProvider" />
    </authentication-manager>

    <b:bean id="myADProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
        <b:constructor-arg value="fsapps.company.uni" />
        <b:constructor-arg value="ldap://fsapps.company.uni:389/" />
        <b:property name="searchFilter" value="(&amp;(sAMAccountName={0})(objectClass=user))" />
        <b:property name="convertSubErrorCodesToExceptions" value="true" />
        <b:property name="useAuthenticationRequestCredentials" value="true" />
    </b:bean>
    <b:bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />

</b:beans>

Few comments on the configuration. In myADProvider bean definition, the constructor's first argument fsapps.company.uni is used to create a principal in the form [email protected] and to build the baseObject for the search request with a baseObject of the form: dc=fsapps,dc=company,dc=uni.

The second constructor's argument is used to establish the communication. The next element of property type with name="searchFilter" will invoke the setSearchFilter() method of the class ActiveDirectoryLdapAuthenticationProvider to set the search filter (this was what I was seeking originally to make some progress). So, it is now set to search (&(sAMAccountName={0})(objectClass=user)).

With this setup, I can see in WireShark the LDAP conversation and the binding succeeds. The bind response is success. Next, the search request succeeds as well but does not return any result. Hence, I am still getting the following in my log:

2015-04-16 10:30:28,201 DEBUG (o.s.s.a.ProviderManager.authenticate) [http-8443-2] Authentication attempt using org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider MDC{}
2015-04-16 10:30:28,201 DEBUG (o.s.s.l.a.AbstractLdapAuthenticationProvider.authenticate) [http-8443-2] Processing authentication request for user: myusername MDC{}
2015-04-16 10:30:28,294 DEBUG (o.s.s.l.SpringSecurityLdapTemplate.searchForSingleEntryInternal) [http-8443-2] Searching for entry under DN '', base = 'dc=fsapps,dc=company,dc=uni', filter = '(&(sAMAccountName={0})(objectClass=user))' MDC{}
2015-04-16 10:30:28,294 INFO (o.s.s.l.SpringSecurityLdapTemplate.searchForSingleEntryInternal) [http-8443-2] Ignoring PartialResultException MDC{}
2015-04-16 10:30:28,310 DEBUG (o.s.s.w.a.AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication) [http-8443-2] Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials MDC{}
2015-04-16 10:30:28,310 DEBUG (o.s.s.w.a.AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication) [http-8443-2] Updated SecurityContextHolder to contain null Authentication MDC{}
2015-04-16 10:30:28,310 DEBUG (o.s.s.w.a.AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication) [http-8443-2] Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@2876b359 MDC{}

I believe I am almost there, but still can use a little help if someone can provide any. After careful examination, it appears the first constructor's argument being used to construct the baseObject AND the userPrincipalName is my problem. In our setup, the userPrincipalName is using a domain name which is not the domain name in the LDAP url (i.e. campus.company.com). So far so good, I can then change the argument to match our domain name used to construct our users' principals. Now, the problem is this will change the baseObject which must match the LDAP url's schema (i.e. fsapps.company.uni).

From the documentation, there is only one method to set the search filter however the constructor can use one, two or three arguments. The third argument is providing the baseObject value.

My problem is then solved by the following configuration where I have to use all the available setters and constructor's arguments to make it working. The myADProvider bean definition is as follow:

    <b:bean id="myADProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
        <b:constructor-arg value="campus.company.com" />
        <b:constructor-arg value="ldap://fsapps.company.uni:389/" />
        <b:constructor-arg value="dc=fsapps,dc=company,dc=uni" />
        <b:property name="searchFilter" value="(&amp;(userPrincipalName={0})(objectClass=user))" />
        <b:property name="convertSubErrorCodesToExceptions" value="true" />
    </b:bean>

In my case, the searchFilter property could have been omitted now since I fallback on the default value. Despite this very long description of my setup and problem. I hope someone else could benefit from it.

Here is the link to the ActiveDirectorLdapAuthenticationProvider class documentation: http://docs.spring.io/autorepo/docs/spring-security/4.0.0.RELEASE/apidocs/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.html

I found WireShark very helpful to see what was going on between the application server and the AD server to debug this problem.

3

3 Answers

7
votes

I was not able to solve this problem using Context.REFERRAL = "follow" in fact the problem lies in the code of method searchForUser() of the class ActiveDirectoryLdapProvider. In this method, the method SpringSecurityLdapTemplate.searchForSingleEntryInternal() is called with the bindPrincipal which is in fact the userPrincipalName composed from the arguments passed to the constructor in the first argument and the username. So, even if you set your search filter to anything else than userPrincipalName, it will be passed a userPrincipalName as argument 0. Hence, the filter with sAMAccountName will not work with a UPN and throw an exception.

Either searchForUser() should be modified or augmented to detect the searchFilter is needing a username and not a UPN, either extra setters are provided to set the arguments using patterns for the searchFilter.

But there is no way to make this class working correctly in such situation without modifying the code. That's what I finally did. I wrote my own class basically a carbon copy of the original ActiveDirectoryLdapAUthenticationProvider with one single and simple modification to searchForUser() passing the username instead of the bindPrincipal to searchForSingleEntryInternal().

It is bit a nonsense you can enter whatever search filter you want but forced to use only a single argument which is actually the userPrincipalName and nothing else.

1
votes

I sort of ran into similar issue and did some research. In our AD with the domain "my.company.com" we seemed to have two sets of users, one set with UPN in the format [email protected] and in other case it is in the form of [email protected].

And when I use org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider and try to authenticate users with [email protected] and [email protected] - only one of them works depending on what I pass to the constructor for the domain name.

I am no AD expert, but looking at Microsoft tech net article: https://technet.microsoft.com/en-us/library/cc739093(v=ws.10).aspx it appears like while UPN suffix normally is same as domain name, it is not a must and could be suffixed with something else. Quoting that article

The second part of the UPN, the UPN suffix, identifies the domain in which the user account is located. This UPN suffix can be the DNS domain name, the DNS name of any domain in the forest, or it can be an alternative name created by an administrator and used just for log on purposes. This alternative UPN suffix does not need to be a valid DNS name.

In Active Directory, the default UPN suffix is the DNS name of the domain in which user account created. In most cases, this is the domain name registered as the enterprise domain on the Internet. Using alternative domain names as the UPN suffix can provide additional logon security and simplify the names used to log on to another domain in the forest.

For example, if your organization uses a deep domain tree, organized by department and region, domain names can get quite long. The default user UPN for a user in that domain might be sales.westcoast.microsoft.com. The logon name for a user in that domain would be [email protected]. Creating a UPN suffix of "microsoft" would allow that same user to log on using the much simpler logon name of user@microsoft. For more information about user accounts, see User and computer accounts and Object names.

But I think ActiveDirectoryLdapAuthenticationProvider.java seems to be making an assumption that domain name is same as UPN suffix. I made a local fix to ActiveDirectoryLdapAuthenticationProvider.java to not make that assumption:

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

Now both users with their different UPN suffixes are searchable. If my assumptions are correct, I may open a bug with Spring security.

1
votes

In Spring Security 4.1.1 / SpringBoot 1.4.0 environment, I do this like that (in Java):

@Configuration
public class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter
{
   public void init (AuthenticationManagerBuilder aAuth) throws Exception
   {

      ActiveDirectoryLdapAuthenticationProvider
              myProvider = new ActiveDirectoryLdapAuthenticationProvider (ldapDomain, ldapUrl);
      aAuth.authenticationProvider (myProvider);
      aAuth.eraseCredentials (false);
   }
}

I don't run into any problem and the user can logon using the sAMAccountName.