post-photo

How to make a custom Username Password Authentication Filter with Spring Security

Introduction

Basically I’ll show you how to use Spring Security and how to customize it if you want. First we’ll implement the basic authentication with basic responses and other stuff, and I’ll show you how to customize the login and logout process as you wish :)

Simple Authentication

After you added the required dependencies described on Spring’s website, you want to create a WebSecurityConfig class, that tells Spring’s website how you want to authenticate your users, and what you want to do

Let’s say we have a pretty simple Controller we want to protect with authentication.

package com.wanari.customlogin.example.controller;
 
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/test")
public class AuthenticationTestController {
    @RequestMapping(
        method = RequestMethod.GET
    )
    public ResponseEntity<String> test() {
        return ResponseEntity.ok("You can only see this after a successful login :)");
    }
}

And the WebSecurityConfig

package com.wanari.customlogin.example.config.security;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wanari.customlogin.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    private final UserService userService;
    private final ObjectMapper objectMapper;
 
    public WebSecurityConfig(UserService userService, ObjectMapper objectMapper) {
        this.userService = userService;
        this.objectMapper = objectMapper;
    }
 
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable() //We don't need CSRF for this example
            .authorizeRequests()
            .anyRequest().authenticated() // all request requires a logged in user
 
            .and()
            .formLogin()
            .loginProcessingUrl("/login") //the URL on which the clients should post the login information
            .usernameParameter("login") //the username parameter in the queryString, default is 'username'
            .passwordParameter("password") //the password parameter in the queryString, default is 'password'
            .successHandler(this::loginSuccessHandler)
            .failureHandler(this::loginFailureHandler)
 
            .and()
            .logout()
            .logoutUrl("/logout") //the URL on which the clients should post if they want to logout
            .logoutSuccessHandler(this::logoutSuccessHandler)
            .invalidateHttpSession(true)
 
            .and()
            .exceptionHandling() //default response if the client wants to get a resource unauthorized
            .authenticationEntryPoint(new Http401AuthenticationEntryPoint("401"));
    }
 
    private void loginSuccessHandler(
        HttpServletRequest request,
        HttpServletResponse response,
        Authentication authentication) throws IOException {
 
        response.setStatus(HttpStatus.OK.value());
        objectMapper.writeValue(response.getWriter(), "Yayy you logged in!");
    }
 
    private void loginFailureHandler(
        HttpServletRequest request,
        HttpServletResponse response,
        AuthenticationException e) throws IOException {
 
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        objectMapper.writeValue(response.getWriter(), "Nopity nop!");
    }
 
    private void logoutSuccessHandler(
        HttpServletRequest request,
        HttpServletResponse response,
        Authentication authentication) throws IOException {
 
        response.setStatus(HttpStatus.OK.value());
        objectMapper.writeValue(response.getWriter(), "Bye!");
    }
}

As you see there is a UserService that, for now, has a really simple implementation

package com.wanari.customlogin.example.service;
 
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
 
import java.util.ArrayList;
 
@Service
public class UserService implements UserDetailsService {
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        if("user".equals(login)) {
            return new User(login, "password", new ArrayList<>());
        } else {
            throw new UsernameNotFoundException("User not found with login: " + login);
        }
    }
}

By implementing the ‘loadUserByUsername’ function, you tell Spring the password of your user, and the authorities it should grant to the authenticated user. For now authorities is an empty List. Spring will compare your user’s password, and the password form the API. If they match. By default a ‘PlaintextPasswordEncoder’ used by Spring, but later i’ll show you how to encode your passwords :)

text

After you added these lines to your project, you can try out the following actions from your favourite REST Client (I use Postman)

text

Reading request body

In the next step we’ll change our application to read the request body instead of the query string.

IMPORTANT!

You can only read the request body’s stream once. If you want to use the data in different services for example, you should cache the body somehow (you can find a lot of solutions if you google it). Let’s assume that we only want to read the body once :)

First we need a CustomUsernamePasswordAuthenticationFilter and we have to tell Spring to use our filter instead of the default one.

package com.wanari.customlogin.example.config.security.filter;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wanari.customlogin.example.config.security.dto.LoginRequest;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
public class RequestBodyReaderAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
 
    private static final Log LOG = LogFactory.getLog(RequestBodyReaderAuthenticationFilter.class);
 
    private static final String ERROR_MESSAGE = "Something went wrong while parsing /login request body";
 
    private final ObjectMapper objectMapper = new ObjectMapper();
 
    public RequestBodyReaderAuthenticationFilter() {
    }
 
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String requestBody;
        try {
            requestBody = IOUtils.toString(request.getReader());
            LoginRequest authRequest = objectMapper.readValue(requestBody, LoginRequest.class);
 
            UsernamePasswordAuthenticationToken token
                = new UsernamePasswordAuthenticationToken(authRequest.login, authRequest.password);
 
            // Allow subclasses to set the "details" property
            setDetails(request, token);
 
            return this.getAuthenticationManager().authenticate(token);
        } catch(IOException e) {
            LOG.error(ERROR_MESSAGE, e);
            throw new InternalAuthenticationServiceException(ERROR_MESSAGE, e);
        }
    }
}

This is how our custom filter looks like. It is as simple as it seems, we just have to parse the request body into a Java class, create a token, and then use the default authenticaton method. After we created this class, we only have 2 tasks left. First is to provide our bean, second is to tell Spring to use our filter instead of the default one. To reach this we change out WebSecurityConfig class like this.

package com.wanari.customlogin.example.config.security;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wanari.customlogin.example.config.security.filter.RequestBodyReaderAuthenticationFilter;
import com.wanari.customlogin.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  
//...
 
    @Bean
    public RequestBodyReaderAuthenticationFilter authenticationFilter() throws Exception {
        RequestBodyReaderAuthenticationFilter authenticationFilter
            = new RequestBodyReaderAuthenticationFilter();
        authenticationFilter.setAuthenticationSuccessHandler(this::loginSuccessHandler);
        authenticationFilter.setAuthenticationFailureHandler(this::loginFailureHandler);
        authenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
        authenticationFilter.setAuthenticationManager(authenticationManagerBean());
        return authenticationFilter;
    }
  
// ...
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .anyRequest().authenticated()
 
//            .and()
//            .formLogin()
//            .loginProcessingUrl("/login")
//            .usernameParameter("login")
//            .passwordParameter("password")
//            .successHandler(this::loginSuccessHandler)
//            .failureHandler(this::loginFailureHandler)
 
            .and()
            .addFilterBefore(
                authenticationFilter(),
                UsernamePasswordAuthenticationFilter.class)
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(this::logoutSuccessHandler)
 
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(new Http401AuthenticationEntryPoint("401"));
    }
     
// ...
 
}

IMPORTANT!

Note the commented lines. After you provided your custom filter, you can no longer use the Spring HttpSecurity builder. If you still use it, you’ll configure the default Filter, not yours!

We tell Spring to use our implementatin by the “addFIlterBefore” function.

After this little modification, the test APIs work the same way, the only difference is that you should provide ‘login’ and ‘password’ params in the POST request body (and not the query string).

Encoding passwords

First we have to change our UserDetailsService a little

package com.wanari.customlogin.example.service;
 
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
 
import java.util.ArrayList;
 
@Service
public class UserService implements UserDetailsService {
 
    private final PasswordEncoder passwordEncoder;
 
    public UserService(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        if("user".equals(login)) {
            return new User(login, passwordEncoder.encode("password"), new ArrayList<>());
        } else {
            throw new UsernameNotFoundException("User not found with login: " + login);
        }
    }
}

Like this, our UserService provides a mock user that has an encoded password.

Second we have to change our WebSecurityConfig

package com.wanari.customlogin.example.config.security;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wanari.customlogin.example.config.security.filter.RequestBodyReaderAuthenticationFilter;
import com.wanari.customlogin.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    private final UserService userService;
    private final ObjectMapper objectMapper;
    private final PasswordEncoder passwordEncoder;
 
    public WebSecurityConfig(UserService userService, ObjectMapper objectMapper, PasswordEncoder passwordEncoder) {
        this.userService = userService;
        this.objectMapper = objectMapper;
        this.passwordEncoder = passwordEncoder;
    }
 
// ...
 
    @Bean
    public DaoAuthenticationProvider authProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userService);
        authProvider.setPasswordEncoder(passwordEncoder);
        return authProvider;
    }
 
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authProvider());
    }
  
// ...
 
}

As you see, we changed the configureGlobal a little bit, and added a DaoAuthenticationProvider bean. We have only one more thing to do, we have to provide our favourite PasswordEncoder in a separate class, like:

package com.wanari.customlogin.example.config.security;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
 
@Configuration
public class PasswordEncoderProvider {
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

After these modifications, the same APIs should work like before. The only difference is on the backend side, we “stored” our users with encrypted passwords, which is nice. :)

Summary

As you see, adding a custom authentication is not hard with Spring, the hard thing was to put all of these together. As far as I saw, no one on the internet wanted to customize the login process like I described above. I hope you found this post useful! If you want to take a look at the full example project, just click here.

See you later! :)

About us

Wanari is a custom software development studio based in Budapest, founded in 2000. Today we are a team of 35 and this is our tech blog. We share software development insights regularly. If you want to read more posts like this, keep up with #TeamWanari on Facebook or LinkedIn.

member photo

His favorite technologies are AngularJS and Java 8. He's been at Wanari as a full stack developer for almost 3 years.

Latest post by Alex Sükein

Solutions for a filterable sortable pageable list in Spring