Suresh Payankannur

Showing posts with label ldap. Show all posts
Showing posts with label ldap. Show all posts

Monday, September 15, 2014

Spring Security and Active Directory without Manager Username/Password

Recently I run into an issue to integrate an application with corporate LDAP. Typical LDAP requires a bind. If the server does not allow anonymous bind, then a manager/admin username and password must be supplied.

But when dealing with Active Directory, one can use the incoming user and password of an authentication request to do the binding. This is a non-standard way to integrate with a typical LDAP server. Spring Security has direct support for this type of configuration and setup.

<security:authentication-manager alias="authenticationManager">
  <security:authentication-provider ref="adAuthenticationProvider"/>
</security:authentication-manager>

<bean id="adAuthenticationProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
  <constructor-arg value="mycompany.com"/>
  <constructor-arg value="ldap://my-company-active-directory-url"/>
  <property name="useAuthenticationRequestCredentials" value="true"/>
  <property name="convertSubErrorCodesToExceptions" value="true"/>
</bean>

Wednesday, August 27, 2014

Spring, Spring Data, RestEasy, Apache Shiro and LDAP

Here are the basic configurations needed to integrate Spring, Spring Data, RestEasy, Apache Shiro and LDAP authentication.

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation=
    "http://java.sun.com/xml/ns/j2ee
     http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">

    <!-- LogBack Configuration File  -->
    <context-param>
      <param-name>logbackConfigLocation</param-name>
      <param-value>/WEB-INF/logback.xml</param-value>
    </context-param>

    <!-- Define the spring context locations -->
    <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/test-service-config.xml</param-value>
    </context-param>

    <!--
        Filters
    -->

    <filter>
      <filter-name>jpaFilter</filter-name>
      <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
      <param-name>targetBean</param-name>
      <param-value>jpaFilter</param-value>
    </init-param>
    </filter>

    <filter-mapping>
      <filter-name>jpaFilter</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>

  <filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
      <param-name>targetBean</param-name>
      <param-value>shiroFilter</param-value>
    </init-param>

    <init-param>
      <param-name>targetFilterLifecycle</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher> 
    <dispatcher>FORWARD</dispatcher> 
    <dispatcher>INCLUDE</dispatcher> 
    <dispatcher>ERROR</dispatcher>
  </filter-mapping>

    <!--
      Listeners
    -->
  <listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener
    </listener-class>
  </listener>

 
  <listener>
    <listener-class>
      ch.qos.logback.ext.spring.web.LogbackConfigListener
    </listener-class>
  </listener>

  <!-- 
     Servlet
   -->

  <servlet>
    <servlet-name>testServlet</servlet-name>
    <display-name>Test Servlet</display-name>

    <description>
      A Test Servlet
    </description>

    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
 
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>
        /WEB-INF/test-web-config.xml
      </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>testServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

</web-app>

test-service-config.xml

<?xml version="1.0" encoding="utf-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xmlns:repository="http://www.springframework.org/schema/data/repository"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation=
       "http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/jdbc
        http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
        http://www.springframework.org/schema/data/jpa
        http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
        http://www.springframework.org/schema/data/repository
        http://www.springframework.org/schema/data/repository/spring-repository.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security.xsd">

  <context:annotation-config/>
  <context:component-scan base-package="org.suresh.api.rest.v1"/>
  <context:component-scan base-package="org.suresh.service"/>
  <context:component-scan base-package="org.suresh.controllers"/>

  <jpa:repositories base-package="org.suresh.repo"/>
  <jdbc:embedded-database id="testDb" type="HSQL"/>

  <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">

    <property name="dataSource" ref="testDb"/>
    <property name="persistenceUnitName" value="test"/>
    <property name="packagesToScan">
      <list>
        <value>org.suresh.model</value>
      </list>
    </property>

    <property name="jpaVendorAdapter">
      <bean
          class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <property name="generateDdl" value="true"/>
        <property name="database" value="HSQL"/>
      </bean>
    </property>
  </bean>

  <bean id="transactionManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
    <property name="dataSource" ref="testDb"/>
  </bean>

  <!-- Security -->
  <security:ldap-server
      root="dc=suresh,dc=org"
      ldif="classpath:resources/test-users.ldif" port="33389"/>

  <bean id="shiroSecurityManager"
        class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="ldapRealm"/>
  </bean>

  <bean id="ldapRealm" class="org.apache.shiro.realm.ldap.JndiLdapRealm">
    <property name="userDnTemplate"
              value="uid={0},ou=people,dc=suresh,dc=org"/>
    <property name="contextFactory" ref="ldapContextFactory"/>
  </bean>

  <bean id="ldapContextFactory"
        class="org.apache.shiro.realm.ldap.JndiLdapContextFactory">
    <property name="url"
              value="ldap://localhost:33389/dc=suresh,dc=org"/>
  </bean>

  <bean id="shiroFilter"
        class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="shiroSecurityManager"/>
    <property name="loginUrl"        value="/views/login.jsp"/>
    <property name="successUrl"      value="/views/home.jsp"/>

    <property name="filterChainDefinitions">
      <value>
        /api/**   = authc
        /views/** = authc
      </value>
    </property>

  </bean>

  <!-- enable shiro annotations -->
  <bean id="lifecycleBeanPostProcessor"
        class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

  <bean
      class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
      <property name="securityManager" ref="shiroSecurityManager"/>
    </bean>

  <!-- Filters -->
  <bean id="jpaFilter"
        class="org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter"/>
</beans>

test-web-config.xml

<?xml version="1.0" encoding="utf-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation=
       "http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

  <import resource="classpath:springmvc-resteasy.xml"/>

  <context:annotation-config/>
  <context:component-scan base-package="org.suresh.api.rest.v1"/>

  <mvc:annotation-driven/>


  <mvc:resources mapping="/images/**" location="/public/images/" order="-1"/>
  <mvc:resources mapping="/stylesheets/**"
                 location="/public/stylesheets/"    order="-1"/>
  <mvc:resources mapping="/javascripts/**"
                 location="/public/javascripts/"     order="-1"/>
  <mvc:default-servlet-handler/>


  <!-- RESTEasy prefix -->
  <bean id="resteasy.handlerMapping" parent="abstract.resteasy.handlerMapping">
    <property name="prefix" value="/api" />
  </bean>
</beans>

Sunday, July 31, 2011

Spring Security, LDAP & Active Directory

Earlier this year, I had to integrate the spring-security with Active Directory, for authentication and authorization. Eventhough there were some documentation available, I had to struggle a bit to get this working. So it will be a good idea to share my findings here. I have been looking for the minimal configuration that accomplishes the goal.

Notes:

  • The sAMAccountName attribute in AD stores the user login.
  • The persion tag will populate the the details of the user in org.springframework.security.ldap.userdetails.Person class. This is handy if one needs to get more than the username like first name, last name etc.
  • A number of ldap related tags are not documented well. Refer to the package org.springframework.security.config.ldap for details of the possible tags that can be used in the spring context.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:s="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       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">

  
  <s:http>
    <s:intercept-url access="IS_AUTHENTICATED_REMEMBERED" pattern="/secure/**">
      <s:form-login login-page="login.html"
                    default-target-url="/secure/homepage.do"
                    always-use-default-target="true"/>
    <s:logout/>
    </s:intercept-url>
 </s:http>

 <s:ldap-server id="ldap-server"
                url="ldap://host:389/"
                root=""
                manager-dn="cn=xxx,ou=xxx,DC=corp,dc=xxx,dc=com"
                manager-password="xxx"/>

  <s:authentication-manager alias="authenticationManager">
    <s:ldap-authentication-provider
        user-search-filter="(&amp;(objectclass=user)(sAMAccountName={0}))"
        user-search-base="dc=corp,dc=xxx,dc=com"
        group-search-filter="(&amp;(objectclass=group)(member={0}))"
        group-search-base="ou=xxx,dc=corp,dc=xxx,dc=com"
        user-details-class="person"
        role-prefix="none"
        />
  </s:authentication-manager>
</beans>
For example, if one wants to extract the full name of the user:
import org.springframework.security.ldap.userdetails.Person;

    public String getFullName() {
        Person person = (Person) getAuthentication().getPrincipal();
        String[] cn = person.getCn();

        StringBuilder sbuf = new StringBuilder("");
        if (cn != null  &&  cn.length > 0) {
            for (String s : cn) {
                sbuf.append(s).append(" ");
            }
        }
        return sbuf.toString().trim();
    }

Unit Testing

import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;

import static org.mockito.Mockito.*;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:spring-security-context.xml"})
public class AuthenticationTest {
    @Test
    public void testAuth() {
        // Authentication auth = mock(Authentication.class);
        // when(auth.getPrincipal()).thenReturn("rod");
        // when(auth.getCredentials()).thenReturn("koala");
        // Cannot mock authenticator as the ProviderManager checks for
        // an instance of UsrenamePasswordAuthenticationToken. So create
        // a concrete instance.
        Authentication auth = new UsernamePasswordAuthenticationToken("rod",
                                                                      "koala");
        auth = authenticationManager.authenticate(auth);
        assertTrue(auth.isAuthenticated());
    }

    @Autowired
    private AuthenticationManager authenticationManager;
}

Using embedded ldap server

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:s="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       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">

  <s:http>
    <s:intercept-url pattern="/secure/**" 
                     access="IS_AUTHENTICATED_REMEMBERED" />
    <s:form-login login-page="/login.html"
                  default-target-url="/secure/homepage.do" 
                  always-use-default-target="true"/>
    <s:logout />
  </s:http>

  <s:ldap-server ldif="classpath:users.ldif" port="33389"/>

  <s:authentication-manager alias="authenticationManager">
    <s:ldap-authentication-provider
        group-search-filter="member={0}"
        group-search-base="ou=groups"
        user-search-base="ou=people"
        user-search-filter="uid={0}"
        user-details-class="person"
        role-prefix="none"/>
  </s:authentication-manager>
</beans>

Blog Archive

Scroll To Top