Skip to content

Integrating ActiveDirectory on Windows NTLM using Spring Security and custom authorization to simulate a single-sign-on web application

Goals
Authenticate a Spring web application deployed on Windows NTLM network using ActiveDirectory, manage Roles in web application (custom authorization), and simulate a single-sign-on environment, where a use can simply access a url and on successful local windows login, application would redirect to home page. So there’s no login screen required. Integrate with Spring Security framework is desirable. If possible, the solution should be reusable across multiple Spring web apps.

STRATEGIES:
Though Spring Security provides tools to integrate LDAP, a seamless integration of ActiveDirectory in a web application using spring security is still a pain. I recently came across this problem and after some research found Waffle Security the best tool for this job. I had to modify the source code to customize the solution based on my requirements. Here’s a writeup of my findings:

Spring Security LDAP
* Spring LDAP Security Reference
* Spring uses ActiveDirectoryLdapAuthenticationProvider, which delegates the work to LdapAuthenticator and LdapAuthoritiesPopulator which are responsible for authenticating the user and retrieving the user’s set of GrantedAuthoritys respectively.UserDetails can be populated using DefaultLdapAuthoritiesPopulator.
* A local LDAP instance can be easily setup (and tested) in a web app.
* You must update ldap properties in users.ldif (sample ldap user info) for authentication/authorization to work. However this doesn’t work as a single-sign on environment, since additional filters need to be written.

Hence this approach was not suited for a single-sign-on requirement.

Tomcat 7 integration
* Not sure how Tomcat integration is useful since configuring Realms and other server settings is an additional step outside of the App. Each environment has to ensure these settings exist, which means local development and test, stage, prod configuration grow exponentially. Also, the authentication object should to be populated in Spring’s context (ie., the application’s context). This leads to Spring being aware of Tomcat’s Realms and settings + the web app supporting Tomcat’s authentication (SPNEGO) etc.,
* Tomcat integration is geared towards plain servlet apps.

Due to the above reasons I didn’t find this solution extensible.

Spring Security Kerberos/SPNEGO Extension
* For Kerberos (unix or windows) based, client based (browser) authentication, this extension from Spring is apt
* You need some knowledge on the server side (setup kerberos etc.,) so this may not be an “out of the box” solution
* Reference example is here

Ideally this solution would’ve been great but at the point of this writing (to my findings), this solution doesn’t integrate with Active Directory on NTLM.

Kerberos + NTLM with Waffle Filter support with Spring Security
* More flexible than the above example since it supports both Kerberos and Windows NTLM. Waffle will allow you to get rid of IIS (as a pass through) as well. Unlike many other implementations WAFFLE on Windows does not usually require any server-side Kerberos keytab setup, it’s a drop-in solution.
* Integrates well with Spring Security
* Project Site is here.
* Reference example is here.
* Was able to get this working with custom classes that extend Waffle’s filter and custom Authentication object. WORKS as a single-sign within a corporate network and roles are correctly mapped into granted authorities.

Implementation Summary
* The above reference example provided by Waffle is a good start, however that code also assumes authorization loaded from ActiveDirectory. In order to customize authorization, you must extend waffle.spring.NegotiateSecurityFilter. Since Waffle doesn’t open up api to override (Duh#TemplatePatternWouldBeSoNice), I had to copy its implementation and make changes myself.


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import waffle.servlet.WindowsPrincipal;
import waffle.spring.NegotiateSecurityFilter;
import waffle.util.AuthorizationHeader;
import waffle.windows.auth.IWindowsIdentity;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Custom Authentication Filter extending Waffle's built-in Filter.
 * Loads User Roles from Db and uses custom AuthenticationToken.
 * For the most part its same as NegotiateSecurityFilter. See #Note below for the key difference
 */
public class CustomAuthenticationFilter extends NegotiateSecurityFilter {

    Logger logger = LoggerFactory.getLogger(CustomAuthenticationFilter.class);

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        logger.info(request.getMethod() + " " + request.getRequestURI() + ", contentlength: " + request.getContentLength());
        AuthorizationHeader authorizationHeader = new AuthorizationHeader(request);

        // authenticate user
        if (!authorizationHeader.isNull()
                && getProvider().isSecurityPackageSupported(authorizationHeader.getSecurityPackage())) {

            // log the user in using the token
            IWindowsIdentity windowsIdentity = null;

            try {
                windowsIdentity = getProvider().doFilter(request, response);
                if (windowsIdentity == null) {
                    return;
                }
            } catch (Exception e) {
                logger.warn("error logging in user: " + e.getMessage());
                sendUnauthorized(response, true);
                return;
            }

            if (!getAllowGuestLogin() && windowsIdentity.isGuest()) {
                logger.warn("guest login disabled: " + windowsIdentity.getFqn());
                sendUnauthorized(response, true);
                return;
            }

            try {
                logger.debug("logged in user: " + windowsIdentity.getFqn() +
                        " (" + windowsIdentity.getSidString() + ")");
                WindowsPrincipal principal = new WindowsPrincipal(
                        windowsIdentity, getPrincipalFormat(), getRoleFormat());
                logger.debug("roles: " + principal.getRolesString());

                /// NOTE: This is where you customize auth. You'd need your own token and initialization mechanism different from 
                /// Waffle's built in token implementation
                // Populate Authentication Token along with GrantedAuthorities
                CustomAuthenticationToken authentication = new CustomAuthenticationToken(principal);
                SecurityContextHolder.getContext().setAuthentication(authentication);
                logger.info("successfully logged in user: " + windowsIdentity.getFqn());
            } finally {
                windowsIdentity.dispose();
            }
        }

        chain.doFilter(request, response);
    }

    /**
     * Send a 401 Unauthorized along with protocol authentication headers.
     *
     * @param response HTTP Response
     * @param close    Close connection.
     */
    private void sendUnauthorized(HttpServletResponse response, boolean close) {
        try {
            getProvider().sendUnauthorized(response);
            if (close) {
                response.setHeader("Connection", "close");
            } else {
                response.setHeader("Connection", "keep-alive");
            }
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            response.flushBuffer();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

Here’s the custom Authentication Token:

import xxx.domain.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import waffle.servlet.WindowsPrincipal;

import java.util.Collection;
import java.util.List;

/**
 * Custom Authentication Token to comply with custom Waffle Authentication mechanism.
 * Variation of Waffle's WindowsAuthenticationToken. See #Note below
 */
public class CustomAuthenticationToken implements Authentication {

    Logger logger = LoggerFactory.getLogger(CustomAuthenticationToken.class);

    private static final long serialVersionUID = 1L;
    private WindowsPrincipal windowsPrincipal = null;
    private org.springframework.security.core.userdetails.User principal = null;
    private Collection<GrantedAuthority> authorities = null;

    // Move this to a config
    public static final String MY_DOMAIN = "<your corporate domain>";

    /**
     * Constructor that fully initializes the principal
     *
     * @param windowsPrincipal windows principal
     */
    public CustomAuthenticationToken(WindowsPrincipal windowsPrincipal) {
        this.windowsPrincipal = windowsPrincipal;
        // Strip domainName in <domainName>\\username to get mapped username in App
        String username = windowsPrincipal.getName().substring(MY_DOMAIN.length()+1);
        logger.debug("App Login Info derived from ActiveDirectory: " + username);

        // Load roles
        Collection<GrantedAuthority> authorities = loadAuthorities(username);

        // Create UserDetails object
        this.principal = new org.springframework.security.core.userdetails.User(username, "", true, true,
                true, true, authorities);
    }

    /**
     * Loads granted authorities by looking up User=>Roles.
     * Throws UsernameNotFoundException if principal isn't mapped mapped to Db
     * @return
     */
    private Collection<GrantedAuthority> loadAuthorities(String username) {

        // Match User Account from Db
        User account = null;
        try {
            // NOTE: I Use Spring Roo's inbuilt finder but you can easily replace the below code by JDBC/JPA/Hibernate Dao or Service
            account = User.findUserByName(username);
        }
        catch (Exception e) {
            logger.info("A unique account " + username + " could not be found: " + e.getMessage());
            throw new UsernameNotFoundException("A unique account " + username + " could not be found");
        }

        // Load User Roles
        List<String> roles = account.getRoleNames();
        logger.debug("Loaded Roles from Database: " + roles.toString());
        Collection<GrantedAuthority> grantedAuthorities = toGrantedAuthorities(roles);
        this.authorities = grantedAuthorities;
        return authorities;
    }

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }

    public Object getCredentials() {
        return null;
    }

    public Object getDetails() {
        return windowsPrincipal;
    }

    public Object getPrincipal() {
        return principal;
    }

    public boolean isAuthenticated() {
        return (principal != null);
    }

    public void setAuthenticated(boolean authenticated) throws IllegalArgumentException {
        throw new IllegalArgumentException();
    }

    public String getName() {
        return principal.getUsername();
    }

    private static Collection<GrantedAuthority> toGrantedAuthorities(List<String> roles) {
        List<GrantedAuthority> result = new ArrayList<GrantedAuthority>();

        for (String role : roles) {
            result.add(new GrantedAuthorityImpl(role));
        }

        return result;
    }
}

And here’s the Spring Config (very similar to the Waffle’s reference example):

<?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:sec="http://www.springframework.org/schema/security"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">

    <bean id="loggerListener" class="org.springframework.security.authentication.event.LoggerListener" />

    <!-- Windows authentication provider -->
    <bean id="waffleWindowsAuthProvider" class="waffle.windows.auth.impl.WindowsAuthProviderImpl"/>

    <!-- Collection of security filters -->
    <bean id="negotiateSecurityFilterProvider" class="waffle.servlet.spi.NegotiateSecurityFilterProvider">
        <constructor-arg ref="waffleWindowsAuthProvider"/>
    </bean>
    <bean id="basicSecurityFilterProvider" class="waffle.servlet.spi.BasicSecurityFilterProvider">
        <constructor-arg ref="waffleWindowsAuthProvider"/>
    </bean>

    <bean id="waffleSecurityFilterProviderCollection" class="waffle.servlet.spi.SecurityFilterProviderCollection">
        <constructor-arg>
            <list>
                <ref bean="negotiateSecurityFilterProvider"/>
                <ref bean="basicSecurityFilterProvider"/>
            </list>
        </constructor-arg>
    </bean>

    <bean id="negotiateSecurityFilterEntryPoint" class="waffle.spring.NegotiateSecurityFilterEntryPoint">
        <property name="provider" ref="waffleSecurityFilterProviderCollection"/>
    </bean>

    <!-- Waffle authentication provider -->
    <sec:authentication-manager alias="authenticationProvider"/>

    <!-- Custom Authentication Filter -->
    <bean id="waffleNegotiateSecurityFilter" class="xxx.CustomAuthenticationFilter">
        <property name="provider" ref="waffleSecurityFilterProviderCollection"/>
        <property name="allowGuestLogin" value="false"/>
        <property name="principalFormat" value="fqn"/>
        <property name="roleFormat" value="both"/>
    </bean>

    <!-- Core config-->
    <sec:http entry-point-ref="negotiateSecurityFilterEntryPoint">
        <sec:custom-filter ref="waffleNegotiateSecurityFilter" position="BASIC_AUTH_FILTER"/>

        <sec:logout logout-url="/app/logout" logout-success-url="/app/home"/>
        <sec:access-denied-handler error-page="/app/accessdenied"/>

        <sec:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>
        <sec:intercept-url pattern="/app/resource/*" filters="none"/>
        <sec:intercept-url pattern="/app/javax.*/**" filters="none"/>
    </sec:http>

</beans>

That’s it.

The above spring config is loaded via web.xml, along with other spring contexts.

Now when user tries to access the application url, CustomAuthenticationFilter correctly recognizes the authenticated Windows user and redirects to app home page. Yes it’s a single-sign-on!

To simplify testing, I’d recommended having a “Spring-LDAP-security” for dev and switch to Waffle Filter for prod environments.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

One Comment

  1. Brent Perry wrote:

    Great blog post. I’ve found surprisingly little information on the best way to do a Spring Security + WAFFLE implementation with roles. I have posts on StackOverflow and the waffle Google group with little to no feedback.

    My question to you is:

    Where and how do you define your role-based restrictions? If I were to use the default Waffle authorization derived from Active Directory, would I need to use the above customization to define role-based access?

    Attempts to use hasRole or simply access=”ROLE_” have results in Spring Security coming back with prompts for credentials and rejections (HTTP 403).

    Any insight would be helpful…

    Wednesday, February 20, 2013 at 6:18 pm | Permalink

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*