Introduction

Web application with kerberos authentication has one problem if accessed from the same windows machine NTLM will be used instead of Kerberos, because of this in Spring Kerberos you will get following exception.

Caused by: GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)
  at sun.security.jgss.GSSHeader.<init>(GSSHeader.java:80)
  at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:287)
  at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:267)

This is a major problem for developers because most of the developer run and test their web application in the same machine. One workaround for this problem is get the user name from system property using System.getProperty("user.name").

This post explains how to do the above workaround in Spring Security.

Note: The above exception can occur in other situation also this solution won’t fix those issues. Ref this stackoverflow question.

Prerequisites

  1. Configure Spring Kerberos

Steps

After the kerberos setup to use it in local machine you have to create a spring authentication flow for that you need three classes.

1. Authentication Filter for Localhost authentication

Authentication Filter will check what kind of request and create appropriate authentication token. For localhost authentication the request will be checked weather the request coming from the same machine then create a LocalhostAuthenticationToken and pass it to the authentication spring manager.

package com.seenukarthi.security.kerberos.localhost;

import java.io.IOException;

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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.GenericFilterBean;

/**
 * {@code LocalhostAuthenticationFilter} is Kerberos authentication filter local
 * user if the application is accessed from same machine as server since if the
 * server and client are same machine browser will always send NTLM token insted
 * of Kerberos token.
 *
 * @author Karthikeyan Vaithilingam
 * @see LocalhostAuthenticationProvider
 * @see LocalhostAuthenticationToken
 */
public class LocalhostAuthenticationFilter extends GenericFilterBean {

    private static final Logger LOGGER = LoggerFactory
            .getLogger(LocalhostAuthenticationFilter.class);

    private AuthenticationManager authenticationManager;
    private AuthenticationSuccessHandler successHandler;
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();

    /**
     * <p>Getter for the field <code>authenticationManager</code>.</p>
     *
     * @return the authenticationManager
     */
    public AuthenticationManager getAuthenticationManager() {
        return authenticationManager;
    }

    /**
     * <p>Setter for the field <code>authenticationManager</code>.</p>
     *
     * @param authenticationManager the authenticationManager to set
     */
    public void setAuthenticationManager(
            AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public AuthenticationSuccessHandler getSuccessHandler() {
        return successHandler;
    }

    public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
        this.successHandler = successHandler;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
     * javax.servlet.ServletResponse, javax.servlet.FilterChain)
     */

    /**
     * {@inheritDoc}
     * <p/>
     * In this filter if the remote address and the local address is same then
     * the request is from the same machine so {@link LocalhostAuthenticationToken}
     * will be created using the username which running the server and using authentication
     * manager the user will be validated for the application by {@link LocalhostAuthenticationProvider}.
     * The above process will be skiped if the user is already authenticated.
     */
    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {

        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;
        if (request.getLocalAddr().equals(request.getRemoteAddr())) {
            final String username = System.getProperty("user.name").toUpperCase();
            if (authenticationIsRequired(username)) {
                LOGGER.info("Request is local");
                LocalhostAuthenticationToken authRequest = new LocalhostAuthenticationToken(username);
                authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
                Authentication authResult = authenticationManager.authenticate(authRequest);
                SecurityContextHolder.getContext().setAuthentication(authResult);
                if(null != successHandler){
                    successHandler.onAuthenticationSuccess(request,response,authResult);
                    return;
                }
            }
        }
        chain.doFilter(request, response);
    }

    /**
     * Checks for is authentication required for the user.
     *
     * @param username authenticating username
     * @return boolean (is authentication required)
     */
    private boolean authenticationIsRequired(String username) {

        Authentication existingAuth = SecurityContextHolder.getContext()
                .getAuthentication();
        if (existingAuth == null || !existingAuth.isAuthenticated()) {
            return true;
        }

        if (existingAuth instanceof LocalhostAuthenticationToken) {
            String existingUsername = existingAuth.getName();
            if (existingUsername.indexOf('@') != -1) {
                existingUsername = existingUsername.substring(0,
                        existingUsername.indexOf('@'));
            }
            return !existingUsername.equalsIgnoreCase(username);
        }
        if (existingAuth instanceof AnonymousAuthenticationToken) {
            return true;
        }
        return false;
    }
}

As in the above example the class should extend org.springframework.web.filter.GenericFilterBean and on the doFilter method check for the request comes from the same machine by using request.getLocalAddr().equals(request.getRemoteAddr()) if the condition is true create a LocalhostAuthenticationToken then pass it to the authentication manager.

2. Authentication Token

The authentication token should extend org.springframework.security.authentication.AbstractAuthenticationToken

package com.seenukarthi.security.kerberos.localhost;

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

/**
 * {@code LocalhostAuthenticationToken} is Kerberos authentication token for local user if the
 * application is accessed from same machine as server since if the server and client are same
 * machine browser will always send NTLM token insted of Kerberos token.
 *
 * @author Karthikeyan Vaithilingam
 * @see LocalhostAuthenticationFilter
 * @see LocalhostAuthenticationProvider
 */
public class LocalhostAuthenticationToken extends AbstractAuthenticationToken {

	/**
	 *
	 */
	private static final long serialVersionUID = -8313121312116264280L;

	private final Object principal;

	/**
	 * <p>Constructor for LocalhostAuthenticationToken.</p>
	 *
	 * @param principal a {@link java.lang.Object} object.
	 */
	public LocalhostAuthenticationToken(Object principal) {
		super(null);
		this.principal = principal;
		setAuthenticated(false);
	}

	/**
	 * <p>Constructor for LocalhostAuthenticationToken.</p>
	 *
	 * @param principal a {@link java.lang.Object} object.
	 * @param authorities a {@link java.util.Collection} object.
	 */
	public LocalhostAuthenticationToken(Object principal,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		super.setAuthenticated(true);
	}

	/**
	 * <p>getCredentials.</p>
	 *
	 * @return a {@link java.lang.Object} object.
	 */
	public Object getCredentials() {
		return null;
	}

	/**
	 * <p>Getter for the field <code>principal</code>.</p>
	 *
	 * @return the principal
	 */
	public Object getPrincipal() {
		return this.principal;
	}

}

3. Authentication Provider

Authentication provider should implement org.springframework.security.authentication.AuthenticationProvider, which does the actual authentication. In our case the user should be checked in a datastore like database and get the authorities for the user.

package com.seenukarthi.security.kerberos.localhost;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

import com.seenukarthi.security.exeception.SecurityException;

/**
 * {@code LocalhostAuthenticationProvider} is Kerberos authentication provider for local user
 * if the application is accessed from same machine as server since if the server and client
 * are same machine browser will always send NTLM token insted of Kerberos token.
 *
 * @author Karthikeyan Vaithilingam
 * @see LocalhostAuthenticationFilter
 * @see LocalhostAuthenticationToken
 */
public class LocalhostAuthenticationProvider implements AuthenticationProvider,
        InitializingBean {

    private UserDetailsService userDetailsService;

    /*
     * (non-Javadoc)
     *
     * @see
     * org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    /** {@inheritDoc} */
    @Override
    public void afterPropertiesSet(){
        if(userDetailsService == null) {
            throw new SecurityException("property userDetailsService is null");
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.springframework.security.authentication.AuthenticationProvider#
     * authenticate(org.springframework.security.core.Authentication)
     */
    /** {@inheritDoc} */
    @Override
    public Authentication authenticate(Authentication authentication){
        LocalhostAuthenticationToken auth = (LocalhostAuthenticationToken) authentication;
        String username = auth.getName();
        UserDetails userDetails = this.userDetailsService
                .loadUserByUsername(username);
        LocalhostAuthenticationToken output = new LocalhostAuthenticationToken(
                userDetails, userDetails.getAuthorities());
        return output;
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.springframework.security.authentication.AuthenticationProvider#supports
     * (java.lang.Class)
     */
    /** {@inheritDoc} */
    @Override
    public boolean supports(Class<?> authentication) {
        return LocalhostAuthenticationToken.class.isAssignableFrom(authentication);
    }

    /**
     * <p>Setter for the field <code>userDetailsService</code>.</p>
     *
     * @param detailsService a {@link org.springframework.security.core.userdetails.UserDetailsService} object.
     */
    public void setUserDetailsService(UserDetailsService detailsService) {
        this.userDetailsService = detailsService;
    }
}

4. Spring Security Configuration.

The Spring Security configuration has to be changed. The following is modified xml configuration from Spring Security Kerberos blog.

<sec:http entry-point-ref="spnegoEntryPoint">
    <sec:intercept-url pattern="/secure/**" access="IS_AUTHENTICATED_FULLY" />
    <sec:custom-filter ref="locahostAuthenticationProcessingFilter" before="BASIC_PROCESSING_FILTER" />
    <sec:custom-filter ref="spnegoAuthenticationProcessingFilter" position="BASIC_PROCESSING_FILTER" />
</sec:http>

<bean id="spnegoEntryPoint" class="org.springframework.security.extensions.kerberos.web.SpnegoEntryPoint" />

<bean id="spnegoAuthenticationProcessingFilter" class="org.springframework.security.extensions.kerberos.web.SpnegoAuthenticationProcessingFilter">
    <property name="authenticationManager" ref="authenticationManager" />
</bean>

<bean id="locahostAuthenticationProcessingFilter" class="com.seenukarthi.security.kerberos.localhost.LocalhostAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
</bean>

<sec:authentication-manager alias="authenticationManager">
    <sec:authentication-provider ref="localhostAuthenticationProvider" />
    <sec:authentication-provider ref="kerberosServiceAuthenticationProvider" />
</sec:authentication-manager>

<bean id="kerberosServiceAuthenticationProvider" class="org.springframework.security.extensions.kerberos.KerberosServiceAuthenticationProvider">
    <property name="ticketValidator">
        <bean class="org.springframework.security.extensions.kerberos.SunJaasKerberosTicketValidator">
            <property name="servicePrincipal" value="HTTP/web.springsource.com" />
            <property name="keyTabLocation" value="classpath:http-web.keytab" />
        </bean>
    </property>
    <property name="userDetailsService" ref="dummyUserDetailsService" />
</bean>

<bean id="localhostAuthenticationProvider" class="com.seenukarthi.security.kerberos.localhost.LocalhostAuthenticationProvider">
    <property name="userDetailsService" ref="dummyUserDetailsService" />
</bean>

<!-- Just returns the User authenticated by Kerberos and gives him the ROLE_USER -->
<bean id="dummyUserDetailsService" class="org.springframework.security.extensions.kerberos.sample.DummyUserDetailsService"/>

Refrences

  1. Kerberos
  2. Spring Security Kerberos
  3. “Defective Token Deteced” error (NTLM not Kerberos) with Kerberos/Spring Security/IE/Active Directory
  4. NTLM