Customizing Spring Security with Multiple Authentications

Spring Boot offers an easier way to create new web applications or web services. The Security module in the Spring framework enables us to plug in different authentication mechanisms. In some cases, we needed to provide multiple authentication mechanisms for our web service. These authentication mechanisms can be standard or custom.

springauth-1

We had a similar requirement while working on an in-house project to develop a web service for distributing and renewing Kerberos key tabs. The project is named Kite, and it is a web service built on Spring Boot. We initially added SPNEGO to authenticate users of our Kite service.

Enabling SPNEGO Authentication using Spring Security

To enable security and add SPNEGO, we needed to make changes to our pom.xml. The relevant POM changes are shown here:

<dependencies>
  <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>4.0.4.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>4.0.4.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.security.kerberos</groupId>
    <artifactId>spring-security-kerberos-web</artifactId>
    <version>1.0.1.RELEASE</version>
  </dependency>
</dependencies>

We also needed to add Java code to configure SPENGO authentication. The relevant parts of the Java code related to hooking up SPENGO authentication are shown below:

@Configuration @EnableWebSecurity 
public class SecurityConfig extends WebSecurityConfigurerAdapter { 

  @Override 
  protected void configure(HttpSecurity http) throws Exception { 
    http.exceptionHandling() 
   .authenticationEntryPoint(spnegoEntryPoint())
   .and().authorizeRequests().anyRequest().authenticated() 
   .and().formLogin().loginPage("/login").permitAll() 
   .and().logout().permitAll() 
   .and().addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManagerBean()), BasicAuthenticationFilter.class); 
  } 

  @Autowired 
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(kerberosAuthenticationProvider()) 
    .authenticationProvider(kerberosServiceAuthenticationProvider()); 
  } 

  @Bean 
  public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
    KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider(); 
    SunJaasKerberosClient client = new SunJaasKerberosClient(); 
    provider.setKerberosClient(client); 
    provider.setUserDetailsService(dummyUserDetailsService()); 
    return provider; 
  } 

  @Bean 
  public SpnegoEntryPoint spnegoEntryPoint() { 
    return new SpnegoEntryPoint(); 
  } 

  @Bean 
  public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter( AuthenticationManager authenticationManager) {
    SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter(); 
    filter.setAuthenticationManager(authenticationManager); 
    return filter; 
  } 

  @Bean 
  public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() { 
    KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider(); 
    provider.setTicketValidator(sunJaasKerberosTicketValidator()); provider.setUserDetailsService(dummyUserDetailsService()); 
    return provider; 
  } 

  @Bean 
  public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
    SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator(); 
    ticketValidator.setServicePrincipal(kiteConfiguration.getSpnegoPrincipal()); 
    ticketValidator.setKeyTabLocation(new FileSystemResource(kiteConfiguration.getKeytab())); 
    ticketValidator.setDebug(true); return ticketValidator; 
  } 
}

These POM and source code changes enable users to authenticate via Kerberos. Some of our users needed an alternate form of authentication, where the users present a one-time-use token. To hook up our custom token authentication, we took the following steps:

  1. Implement token authentication logic as TokenAuthenticationFilter by extending AbstractAuthenticationProcessingFilter.
  2. Plug in TokenAuthenticationFilter via FilterRegistrationBean.

Custom TokenAuthenticationFilter

The custom token-based authentication filter extends AbstractAuthenticationProcessingFilter. Here we override the doFilter to implement our custom authentication logic.

public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
 private static final String SECURITY_TOKEN_KEY = "token";
 private TokenManager tokenManager;

 public TokenAuthenticationFilter(TokenManager tm) {
   super("/");
   tokenManager = tm;
 }

 @Override
 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;
   String token = request.getParameter(SECURITY_TOKEN_KEY);
   if (token != null) {
     Authentication authResult;
     try {
       authResult = attemptAuthentication(request, response, token);
       if (authResult == null) {
         response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
         return;
       }
     } catch (AuthenticationException failed) {
       response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
       return;
    }

    try {
      SecurityContextHolder.getContext().setAuthentication(authResult);
    } catch (Exception e) {
      logger.error(e.getMessage(), e);
      if (e.getCause() instanceof AccessDeniedException) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return;
      }
    }
  }
  chain.doFilter(request, response);// return to others spring security filters
}

 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response, String token)
 throws AuthenticationException, IOException, ServletException {
   AbstractAuthenticationToken userAuthenticationToken = authUserByToken(token);
   if (userAuthenticationToken == null)
     throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
    return userAuthenticationToken;
 }

 private AbstractAuthenticationToken (String tokenRaw) {
 AbstractAuthenticationToken authToken = null;
   try {
     String user = tokenManager.verifyAndExtractUser(tokenRaw);
     if (user != null) {
       user = user + "@REALM";
       Principal securityUser = new SecurityUser(user);
       return new PreAuthenticatedAuthenticationToken(securityUser, null, null);
     }
   } catch (Exception e) {
     logger.error("Error during authUserByToken", e);
   }
   return authToken;
 }
}

Note that in authUserByToken, we created a SecurityUser object, and this needs to be of the format user@REALM.

Adding TokenAuthenticationFilter

To plug in the new authentication mechanism, we can use the FilterRegistrationBean.

The code snippet below is added to the SecurityConfig above:

@Bean
 public FilterRegistrationBean filterRegistrationBean() {
   FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
   TokenAuthenticationFilter tokenAuthenticationFilter = new TokenAuthenticationFilter();
   filterRegistrationBean.setFilter(tokenAuthenticationFilter);
   filterRegistrationBean.setEnabled(true);
   return filterRegistrationBean;
 }

With these code changes, we can add our custom authentication logic in Spring in addition to the existing authentication mechanisms.