Thursday, December 25, 2014

Homebrew LDAP authentication with Spring Security

The Spring Security was so interesting that I can't wait to make my own experiment at home. To do this, I'm using Apache Directory Studio to achieve this mission. For starter, I create the following structure in my LDAP:

dc=org
    |---dc=homebrew
             |---ou=java
                      |---ou=Counter Strike
                               |---cn=huahsin

Take note that when creating a CN object, SN attribute has to be define as well. Otherwise an error will be prompt. On Spring configuration site, I'm required to configure <ldap-server> element to establish connection against which authentication should take place. Following piece is more than enough for this purpose:
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xsi:schemalocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
      http://www.springframework.org/schema/security
      http://www.springframework.org/schema/security/spring-security-3.1.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.1.xsd">

   ...

   <ldap-server url="ldap://127.0.0.1:10389"/>

</beans:beans>
Also take note that there is additional attributes called manager-dn and manager-password in <ldap-server>, interestingly without these attributes put in <ldap-server> element, it wouldn't fail the authentication. The process were still go on. I think there must be a purpose for these attributes, just put this aside for later. The usage of that 2 attributes are shown below:
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xsi:schemalocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
      http://www.springframework.org/schema/security
      http://www.springframework.org/schema/security/spring-security-3.1.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.1.xsd">

   ...

   <ldap-server manager-dn="cn=huahsin,ou=Counter Strike,ou=java,dc=homebrew,dc=org" manager-password="abcde" url="ldap://127.0.0.1:10389"/>

</beans:beans>
Once LDAP connection has been established, authentication will start taking place immediately. Now I'll required <ldap-authentication-provider> for this purpose:
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xsi:schemalocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
      http://www.springframework.org/schema/security
      http://www.springframework.org/schema/security/spring-security-3.1.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.1.xsd">

   ...

   <authentication-manager alias="authenticationManager">
      <ldap-authentication-provider user-search-filter="cn={0}"/>
   </authentication-manager>
</beans:beans>
{0} is a placeholder for CN, it will be replace by real value during runtime. For my case, it would be huahsin. Note the usage of user-search-filter, never put cn={0},ou=Counter Strike,ou=java,dc=homebrew,dc=org for this attribute, the authentication will not get pass. That should be put in user-search-group, as shown below.
   ...
   <authentication-manager alias="authenticationManager">
      < ldap-authentication-provider group-search-base="ou=Counter Strike,ou=java,dc=homebrew,dc=org" user-search-filter="cn={0}"/>
   </authentication-manager>

   ...
Another note on the usage of group-search-base attribute is when I have the LDAP define in such a way:
< beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xsi:schemalocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
      http://www.springframework.org/schema/security
      http://www.springframework.org/schema/security/spring-security-3.1.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.1.xsd" >

   ...

   <ldap-server url="ldap://127.0.0.1:10389/dc=homebrew,dc=org"/>

</beans:beans>
I should have the ou=Counter Strike,ou=java define in group-search-base as shown below:
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xsi:schemalocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
      http://www.springframework.org/schema/security
      http://www.springframework.org/schema/security/spring-security-3.1.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.1.xsd">

   ...

   <authentication-manager alias="authenticationManager">
      <ldap-authentication-provider group-search-base="ou=Counter Strike,ou=java" user-search-filter="cn={0}" />
   </authentication-manager>
</beans:beans>
Otherwise following error would be seen.
 Exception thrown by application class 'org.springframework.ldap.support.LdapUtils.convertLdapException:174'

org.springframework.ldap.NameNotFoundException: [LDAP: error code 32 - NO_SUCH_OBJECT: failed for MessageType : SEARCH_REQUEST Message ID : 3 SearchRequest baseDn : 'ou=Counter Strike,ou=java,dc=homebrew,dc=org,dc=homebrew,dc=org' filter : '(uniqueMember=2.5.4.3=kok hoe+2.5.4.4=loh,2.5.4.11=Counter Strike,2.5.4.11=java,0.9.2342.19200300.100.1.25=homebrew,0.9.2342.19200300.100.1.25=org)' scope : whole subtree typesOnly : false Size Limit : no limit Time Limit : no limit Deref Aliases : deref Always attributes : 'cn', 'objectClass', 'javaSerializedData', 'javaClassName', 'javaFactory', 'javaCodeBase', 'javaReferenceAddress', 'javaClassNames', 'javaRemoteLocation' org.apache.directory.api.ldap.model.message.SearchRequestImpl@303434e3 ManageDsaITImpl Control Type OID : '2.16.840.1.113730.3.4.2' Criticality : 'false' ' : ERR_648 Invalid search base ou=Counter Strike,ou=java,dc=homebrew,dc=org,dc=homebrew,dc=org]; nested exception is javax.naming.NameNotFoundException: [LDAP: error code 32 - NO_SUCH_OBJECT: failed for MessageType : SEARCH_REQUEST Message ID : 3 SearchRequest baseDn : 'ou=Counter Strike,ou=java,dc=homebrew,dc=org,dc=homebrew,dc=org' filter : '(uniqueMember=2.5.4.3=kok hoe+2.5.4.4=loh,2.5.4.11=Counter Strike,2.5.4.11=java,0.9.2342.19200300.100.1.25=homebrew,0.9.2342.19200300.100.1.25=org)' scope : whole subtree typesOnly : false Size Limit : no limit Time Limit : no limit Deref Aliases : deref Always attributes : 'cn', 'objectClass', 'javaSerializedData', 'javaClassName', 'javaFactory', 'javaCodeBase', 'javaReferenceAddress', 'javaClassNames', 'javaRemoteLocation' org.apache.directory.api.ldap.model.message.SearchRequestImpl@303434e3 ManageDsaITImpl Control Type OID : '2.16.840.1.113730.3.4.2' Criticality : 'false' ' : ERR_648 Invalid search base ou=Counter Strike,ou=java,dc=homebrew,dc=org,dc=homebrew,dc=org]; remaining name 'ou=Counter Strike,ou=java,dc=homebrew,dc=org'
at org.springframework.ldap.support.LdapUtils.convertLdapException(LdapUtils.java:174)
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:306)
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:259)
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:606)
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:524)
at org.springframework.security.ldap.SpringSecurityLdapTemplate.searchForSingleAttributeValues(SpringSecurityLdapTemplate.java:173)
at org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator.getGroupMembershipRoles(DefaultLdapAuthoritiesPopulator.java:215)
at org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator.getGrantedAuthorities(DefaultLdapAuthoritiesPopulator.java:185)
at org.springframework.security.ldap.authentication.LdapAuthenticationProvider.loadUserAuthorities(LdapAuthenticationProvider.java:197)
at org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider.authenticate(AbstractLdapAuthenticationProvider.java:63)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:156)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:174)
at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:94)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:195)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:105)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:125)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.access.channel.ChannelProcessingFilter.doFilter(ChannelProcessingFilter.java:144)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:259)
at com.ibm.ws.webcontainer.filter.FilterInstanceWrapper.doFilter(FilterInstanceWrapper.java:194)
at [internal classes]
Caused by: javax.naming.NameNotFoundException: [LDAP: error code 32 - NO_SUCH_OBJECT: failed for MessageType : SEARCH_REQUEST Message ID : 3 SearchRequest baseDn : 'ou=Counter Strike,ou=java,dc=homebrew,dc=org,dc=homebrew,dc=org' filter : '(uniqueMember=2.5.4.3=kok hoe+2.5.4.4=loh,2.5.4.11=Counter Strike,2.5.4.11=java,0.9.2342.19200300.100.1.25=homebrew,0.9.2342.19200300.100.1.25=org)' scope : whole subtree typesOnly : false Size Limit : no limit Time Limit : no limit Deref Aliases : deref Always attributes : 'cn', 'objectClass', 'javaSerializedData', 'javaClassName', 'javaFactory', 'javaCodeBase', 'javaReferenceAddress', 'javaClassNames', 'javaRemoteLocation' org.apache.directory.api.ldap.model.message.SearchRequestImpl@303434e3 ManageDsaITImpl Control Type OID : '2.16.840.1.113730.3.4.2' Criticality : 'false' ' : ERR_648 Invalid search base ou=Counter Strike,ou=java,dc=homebrew,dc=org,dc=homebrew,dc=org]
at com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3112)
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:3033)
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2840)
at com.sun.jndi.ldap.LdapCtx.searchAux(LdapCtx.java:1849)
at com.sun.jndi.ldap.LdapCtx.c_search(LdapCtx.java:1772)
at com.sun.jndi.toolkit.ctx.ComponentDirContext.p_search(ComponentDirContext.java:386)
at com.sun.jndi.toolkit.ctx.PartialCompositeDirContext.search(PartialCompositeDirContext.java:356)
at com.sun.jndi.toolkit.ctx.PartialCompositeDirContext.search(PartialCompositeDirContext.java:339)
at javax.naming.directory.InitialDirContext.search(InitialDirContext.java:267)
at org.springframework.ldap.core.LdapTemplate$4.executeSearch(LdapTemplate.java:253)
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:293)
... 27 more
This stack trace gave me a very good hints on what is going on behind the scene. The error has already mention Invalid search base ou=Counter Strike,ou=java,dc=homebrew,dc=org,dc=homebrew,dc=org. My common logic sense told me that the value define in user-search-group has been concatenate with the one define in url. To fix this, dc=homebrew,dc=org should be remove either from <ldap-server> or <ldap-authentication-provider>.

Another complex scenario, what if there is another DC object being define in LDAP? For this experiment, I create another DC object named cybertron as shown below:

Root DSE
   |---dc=org
       |---dc=cybertron
   |---dc=org
       |---dc=homebrew
           |---ou=java
               |---ou=Counter Strike
                   |---cn=huahsin

And then I have the LDAP define in this way:
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xsi:schemalocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
      http://www.springframework.org/schema/security
      http://www.springframework.org/schema/security/spring-security-3.1.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.1.xsd">

   ...

   <ldap-server url="ldap://127.0.0.1:10389/dc=cybertron,dc=org"/>

< /beans:beans >
And I'm using this piece for authentication:
   ...
   <authentication-manager alias="authenticationManager">
      <ldap-authentication-provider group-search-base="ou=Counter Strike,ou=java,dc=homebrew,dc=org" user-search-filter="cn={0}"/>
   </authentication-manager>

   ...
Don't be foo that the above code would get pass, it wouldn't because the searching criteria would become like this ou=Counter Strike,ou=java,dc=homebrew,dc=org,dc=homebrew,dc=org,dc=cybertron,dc=org. That is the reason why it fail.

No comments: