Pass an additional parameter with spring security login page

In this article, we will look at the how to pass an additional parameter with Spring Security login page.

Pass an additional parameter with spring security login page

There are multiple way to pass an additional parameter with Spring security page but we will look at the 2 main approach in this section. Here are the 2 options to accomplish this:

  1. Pass parameter using the Authentication Filter Approach.
  2. For most advance cases, we will use the AuthenticationDetailsSource

To understand, let’s assume we want to pass a security token code with each login request as an additional parameter with Spring Security login page to use it in our authentication process.

1. Project and Maven Setup

This article is part of our Spring Security series and you can download the source code from our GitHub Repository. If you are creating a new project, you can use the Spring Initializr to create the initial project setup. This is how our pom.xml looks like

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.1.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository  -->
	</parent>
	<groupId>com.javadevjournal</groupId>
	<artifactId>spring-security-series</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>Spring Security Tutorial Series</name>
	<description>Series to explain the core Spring security concepts.</description>

	<dependencies>
		<!-- we need dev tool only for development and not for the production -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity5</artifactId>
			<version>3.0.4.RELEASE</version>
		</dependency>

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

</project>

2. Custom Authentication Filter

As we know that spring security user the security filters to start the authentication process. Spring security uses the UsernamePasswordAuthenticationFilter to handle the form based login process. The simple and easiest option to pass an additional parameter with Spring Security login page is to create a custom filter by extending the UsernamePasswordAuthenticationFilter and handle any additional parameters. Let’s see how to do this.

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private static final String customToken = "jdjCustomToken";

    /**
     * This is override of UsernamePasswordAuthenticationFilter
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        UsernamePasswordAuthenticationToken authRequest = getAuth(request);
        setDetails(request, authRequest);
        return getAuthenticationManager().authenticate(authRequest);
    }

    /*
        We are combing the username and our custom token to pass on this data to the underlying system.
        As an alternate, you can also save it in session but keep in mind that this will only be available
        where HTTP session is available.
     */
    private UsernamePasswordAuthenticationToken getAuth(final HttpServletRequest request) {
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String customToken = obtainCustomToken(request);

        String usernameDomain = String.format("%s%s%s", username.trim(),
            String.valueOf(Character.LINE_SEPARATOR), customToken);

        return new UsernamePasswordAuthenticationToken(
            usernameDomain, password);
    }

    @Nullable
    protected String obtainCustomToken(HttpServletRequest request) {
        return request.getParameter(customToken);
    }
}

Above code is simple and self explanatory. We are getting the username and password similar to the UsernamePasswordAuthenticationFilter and then getting our customToken from the HTTPServletRequest. We are combining both username and customToken and passing the token to the AuthenticationManager. The next step is to customize our custom user details service to use this information for authentication.

2.1 Spring Security UserDetails Service

Spring security DAOAuthentication Provider uses the UserDetails service to get the user information from the underlying database. The UserDetailsService contract defines a single method called loadUserByUsername. We will use this method to extract both username and custom token to execute our authentication.

@Service("userDetailsService")
public class CustomUserDetailService implements UserDetailsService {

    @Resource
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        //Let's split the email to get both values
        String[] usernameAndCustomToken = StringUtils.split(email, String.valueOf(Character.LINE_SEPARATOR));

        //if the String arrays is empty or size is not equal to 2, let's throw exception
        if (Objects.isNull(usernameAndCustomToken) || usernameAndCustomToken.length != 2) {
            throw new UsernameNotFoundException("User not found");
        }
        final String userName = usernameAndCustomToken[0];
        final String customToken = usernameAndCustomToken[1]; // use it based on your requirement
        final UserEntity customer = userRepository.findByEmail(userName);
        if (customer == null) {
            throw new UsernameNotFoundException(email);
        }
        boolean enabled = !customer.isAccountVerified(); // we can use this in case we want to activate account after customer verified the account
        UserDetails user = User.withUsername(customer.getEmail())
            .password(customer.getPassword())
            .disabled(customer.isLoginDisabled())
            .authorities(getAuthorities(customer)).build();

        return user;
    }
}

2.2. Spring Security Configuration

Now we have created our custom security filter and customized the class. The next step is to configure these details with Spring security. We will do the following steps:

  1. Register our custom security filter with Spring Security.
  2. Inject the AuthenticationManager with our custom security filter.

To register the customer security filter, we will use the addFilterBefore method of the HTTPSecurity class.

@EnableWebSecurity
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(authFilter(), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .antMatchers("/login", "/register", "/home")
            .permitAll()
            .antMatchers("/account/**").hasAnyAuthority("CUSTOMER", "ADMIN")
            .and()
            ....
    }
}

The last step to pass an additional parameter with spring security login page is to pass the AutenticationManager and Authentication Failure Handler to the custom security filter.

@Bean
public CustomAuthenticationFilter authFilter() throws Exception {
    CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManagerBean());
    filter.setAuthenticationFailureHandler(failureHandler());
    return filter;
}

2.3. Login Page

The last step is to add the additional field to the login page. This will allow customer to fill the custom token before submitting the login request. A simple input field is excellent for our need.

<div class="input-group mb-3">
  <input type="text" name="jdjCustomToken" class="form-control" placeholder="Token"/>
  <div class="input-group-append">
    <div class="input-group-text">
      <span class="fas fa-lock"></span>
    </div>
  </div>
</div>

If we run our application and open the login page, we will see an additional input field.

Pass an additional parameter with spring security login page

If we don’t pass the token, we will get the login failure error, while the with correct username and password with token, system will let you login.

how to pass an additional parameter with spring security login page

3. Using Custom Authentication Token

To pass an additional parameter with spring security login page for more advance use cases, we can use a custom AuthenticationToken in combination with the Custom security filter as described in the above section. We will pass this additional information to the Spring security authentication using a custom UsernamePasswordAuthenticationToken and custom authentication provider. You need to create a custom authentication provider and handle the support() method accordingly. Here is a sample code to start with

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

   // inject anything as per your requirement

    @Override
    public Authentication authenticate(Authentication authentication) {

       // run your authetication work here
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(CustomUsernamePasswordAuthenticationToken.class);
    }
}

Summary

In this article, we talked about how to pass an additional parameter with Spring Security login page. Passing additional parameters like security token or other details is a very common use case for any modern application. We talked about the simple solution to handle these parameters using the Spring Security filter. At the end of this article, we talked about creating our own custom token and handle this using the custom authentication provider. As always, the source code for this article is available on our GitHub repository. Let us know in case you think there are other better option to pass an additional parameter with Spring Security login page or we can handle it differently.