Custom Authentication with Spring Boot

Spring Boot Series

Example project for securing REST endpoints with custom authentication.

Introduction

In the previous article, we discussed adding an Authorization header and a custom security scheme to a Spring Boot application for stateless API security. In this article, we’ll discuss how to enable Restful username/password authentication.

Rest Authentication

In Spring Security, it’s been fairly effortless to enable username/password authentication through Form Login, which is a vestige of a bygone era of simple login screens and stateful servers before single page applications were prevalent (or even existed). Admittedly, once an HTTP POST with URL encoded form data is no longer viable, it’s likely that username/password authentication is not viable either. Most likely, we’ll want a multi-factor authentication flow. We’ll discuss this in a future post.

For now, let’s look at how to bypass the traditional form login, but use the same concepts with a JSON-based API.

Set Up

Let’s define a build for our project. Here’s a pom.xml skeleton to get us started:

<?xml version="1.0" encoding="utf-8" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.insource</groupId>
    <artifactId>customauth-security-example</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>customauth-security-example</name>
    <description>Example project for securing REST endpoints with custom authentication.</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.13.RELEASE</version>
        <relativePath />
    </parent>

    <repositories>
        <repository>
            <id>spring-plugins-releases</id>
            <url>http://repo.spring.io/plugins-release</url>
        </repository>
    </repositories>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>1.5.13.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>

        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>${java.version}</source>
                        <target>${java.version}</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

Let’s also define an entry point for our application:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Lastly, let’s define an endpoint we want to be able to secure:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
public class HelloController {
    @GetMapping("/hello")
    public String sayHello() {
        return "Hello, World";
    }
}

Custom Auth Filter

Instead of using the traditional formLogin() configurer, let’s author our own simple filter. In fact, we can extend the existing form login filter, called UsernamePasswordAuthenticationFilter, and provide a tiny bit of code to get access to a request body.

Create the following class:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

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;
import java.util.HashMap;

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
	private static final String BODY_ATTRIBUTE = CustomAuthenticationFilter.class.getSimpleName() + ".body";

	private final ObjectMapper objectMapper;

	public CustomAuthenticationFilter(ObjectMapper objectMapper) {
		this.objectMapper = objectMapper;
	}

	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		// Parse the request body as a HashMap and populate a request attribute
		if (requiresAuthentication(request, response)) {
			UsernamePasswordRequest usernamePasswordRequest = objectMapper.readValue(request.getInputStream(), UsernamePasswordRequest.class);
			request.setAttribute(BODY_ATTRIBUTE, usernamePasswordRequest);
		}

		super.doFilter(req, res, chain);
	}

	protected String obtainUsername(HttpServletRequest request) {
		UsernamePasswordRequest usernamePasswordRequest = (UsernamePasswordRequest) request.getAttribute(BODY_ATTRIBUTE);
		return usernamePasswordRequest.get(getUsernameParameter());
	}

	protected String obtainPassword(HttpServletRequest request) {
		UsernamePasswordRequest usernamePasswordRequest = (UsernamePasswordRequest) request.getAttribute(BODY_ATTRIBUTE);
		return usernamePasswordRequest.get(getPasswordParameter());
	}

	private static class UsernamePasswordRequest extends HashMap<String, String> {
		// Nothing, just a type marker
	}
}

This class makes use of everything provided by UsernamePasswordAuthenticationFilter which in turn extends AbstractAuthenticationProcessingFilter. It will terminate processing of the request if it finds a request that matches, so no @RestController will be invoked (just as with Form Login).

In addition to the UsernamePasswordAuthenticationToken and other window dressing that is created by the parent class, we take over processing the request body. Using a simple ObjectMapper, we can convert an arbitrary key/value JSON structure into a HashMap.

Once the body is parsed, we can easily obtain an arbitrarily named username and password, just as with Form Login. We use a bit of request attribute trickery just to satisfy the method calls made by the parent class.

Customize Authentication

Once we have a basic custom filter in place to do authentication (note we didn’t have to code that part), let’s turn our attention to configuring Spring Security.

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy;
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**")
            .addFilterAfter(customAuthFilter(), RequestHeaderAuthenticationFilter.class)
            .authorizeRequests()
                .antMatchers("/login", "/csrf").permitAll()
                .anyRequest().authenticated()
            .and()
                .csrf().csrfTokenRepository(csrfTokenRepository())
            .and()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
            .and()
                .formLogin().disable();
    }

    @Bean
    public UsernamePasswordAuthenticationFilter customAuthFilter() throws Exception {
        UsernamePasswordAuthenticationFilter authenticationFilter = new CustomAuthenticationFilter(objectMapper());
        authenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
        authenticationFilter.setUsernameParameter("username");
        authenticationFilter.setPasswordParameter("password");
        authenticationFilter.setAuthenticationManager(authenticationManagerBean());
        authenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
        authenticationFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());

        return authenticationFilter;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password("password").roles("USER").and()
            .withUser("admin").password("admin").roles("ADMIN");
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new SimpleUrlAuthenticationSuccessHandler("/");
    }

    @Bean
    public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new CompositeSessionAuthenticationStrategy(Arrays.asList(
            new ChangeSessionIdAuthenticationStrategy(),
            new CsrfAuthenticationStrategy(csrfTokenRepository())
        ));
    }

    @Bean
    public CsrfTokenRepository csrfTokenRepository() {
        return new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());
    }

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new Http401AuthenticationEntryPoint("MyRealm");
    }

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}

There’s a few things going on here, so let’s break it down.

Configuration

First, we wire in our custom extension of UsernamePasswordAuthenticationFilter. It’s best to define an order for the filter to fit into the filter chain. In this case, it doesn’t clash with anything in the defaults, so we could skip this step, but in case we add pre-auth (see previous tutorials), the addFilterAfter() ensures it will be after that filter if present. In this case, it fires pretty early in the chain.

Next, we manually open up the /login and /csrf routes and lock down everything else.

We also need to make sure our CSRF protection is consistent between the default filter chain and our custom filter, so we need to define the glue piece manually, which is the HttpSessionCsrfTokenRepository. We use this later as well.

Then we disable the default form login, which would put another UsernamePasswordAuthenticationFilter into the filter chain and we definitely don’t want that.

UsernamePasswordAuthenticationFilter

In order to configure our filter, we need several additional things.

First, we define an ObjectMapper to use with our custom JSON parsing inside the filter.

Then, we define the request matcher. We don’t have helper methods for this custom filter but it’s not hard to do it manually with an AntPathRequestMatcher. This makes it identical to the default form login configuration, but with JSON instead of form fields.

Also similar to the defaults, we set up the username and password fields that will hold our principal and credentials. I’ve explicitly set them to call out where to configure them for your needs.

We then define a SessionAuthenticationStrategy, since we don’t get any defaults for free. I haven’t ensured this is perfectly consistent with the defaults, so comments are welcome, but in this example, we’re also adding session-fixation and CSRF protection to the filter chain with a CompositeSessionAuthenticationStrategy. The CsrfAuthenticationStrategy uses the same CsrfTokenRepository we defined above, which also gets used by our own custom controller (shown below) to expose the CSRF token.

We also define an AuthenticationEntryPoint to throw a 401 Unauthorized with a WWW-Authenticate response header containing our custom realm name when unauthenticated API calls are made.

Update: If you are using Spring Boot 2.x, please note that the Http401AuthenticationEntryPoint class has been removed. For reference, view this file on GitHub if you need to copy it and define it within your project.

Lastly, we define a simple AuthenticationManager and AuthenticationSuccessHandler. The important thing about the AuthenticationManager is we need to expose it as a bean so we can add it to our custom filter.

Note: This is also useful if we need to access it from somewhere within our application, as the default security configurer does not expose any of these objects as beans. You may need that, for example, if you want to build a password management screen where you need to re-test the user's credentials prior to changing them.

Expose the CSRF

We need to add one piece that’s missing from the form generated by the DefaultLoginPageGeneratingFilter. I’ve not seen any tutorials for how to do this, but the docs cover this deep into the weeds of Spring Security. We can use the CsrfTokenArgumentResolver to get a handle on the CsrfToken automatically.

Let’s add a @RestController to our application:

import org.springframework.http.HttpStatus;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthController {
    @GetMapping("/")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void getIndex() {
    }

    @GetMapping("/login")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void getLogin() {
    }

    @GetMapping("/csrf")
    public CsrfToken getCsrf(CsrfToken csrfToken) {
        return csrfToken;
    }
}

The GET / and GET /login routes are optional, but creates a simple landing page that tells you that you’ve logged in and out successfully.

The GET /csrf route replaces the _csrf hidden attribute from the Form Login page by utilizing the aforementioned CsrfTokenRepository through the CsrfTokenArgumentResolver. API consumers will need to obtain the CSRF prior to invoking the /login route, as the entire application has CSRF protection enabled. Invoking it produces the following output:

{
    "token": "2f1f8b7f-660f-4285-8a08-b29c789c8f60",
    "headerName": "X-CSRF-TOKEN",
    "parameterName": "_csrf"
}

Here is a sample CURL request for using the CSRF token:

curl -X POST \
  http://localhost:8080/login \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -H 'X-CSRF-TOKEN: 2f1f8b7f-660f-4285-8a08-b29c789c8f60' \
  -d '{
	"username": "user",
	"password": "password"
  }'

X-CSRF-TOKEN is the default name of the header required by the CsrfFilter that was enabled with csrf() in our WebSecurityConfigurerAdapter.

Conclusion

In this article, we’ve learned how to create a custom username/password authentication filter, and manually configure Spring Security to use it. We also learned how to expose the CSRF token through our REST API with consistent CSRF protection throughout the application.

Share this
Stateless API Security with Spring Boot, Part 2 Innocuous Code Lurks Around Every Corner
Posted by Steve Riesenberg
Steve Riesenberg

I'm an author, developer, father, musician, and everything in between. In 2016, I founded InSource Software with the goal of making software development fun again, and to create a sustainable model for including the customer in the process. Oh, and building great software. That too...

Related Posts: