Spring Security -- 6) Multiple Authentication Filters && Providers

  • |
  • 06 January 2021
Post image

In this post, let’s implement two steps authentication mechanism. This will be similar to JWT authentication but instead of JWT I will use my implementation. After legitimate user makes the login request, my response will include the header value called Authorization and user can be able to access to restricted endpoints using the Authorization header’s value.

You may see the previous post which includes simple project to understand filter, provider and authentication.

By the way, if you like the my blog contents, you can support me via patreon :)

If you only need to see the code, here is the github link

Project Overview

This project will be kind of a JWT token authentication. First, User will be authenticated against the database via username and password. After database authentication, I will generate one-time-password(otp) for that user which is actually random code. After that user will send request which also includes this new random otp. Then I will check the otp value, if the otp value is correct, then I will generate Authorization header’s value and save it to my database (in this simple project database will be the HashSet, don’t do this in real case scenario(s)) and my response will include Authorization: {{Authorization header’s value }} header which is the random token to access the restricted endpoint(s).

For other requests than the /login, I will get the Authorization header’s value and check the whether the value is correct or not. If it is not correct, authentication will fail.

This might not be considered as secure application. There could be situation where access token can be stolen!! For instance, otp value in the project has no expiration time, but in real case it should be…

After starting the project run the following command (I assume that there is user with test_user and password=1234 in your users table )

If you try the connect the restricted endpoint without authorization, the you will get an exception:

$ curl  -X GET http://localhost:8080/hello

// log in the console:
org.springframework.security.authentication.BadCredentialsException: Authorization value is not correct
	at com.mehmetozanguven.springsecuritymultipleproviders.service.providers.TokenAuthProvider.authenticate(TokenAuthProvider.java:27) ~[classes/:na]

Before getting the authorization token, you should create an record into the otp table:

$ curl -H "username:test_user" -H "password:1234" -X GET http://localhost:8080/login

Right now look at the otp table, you should have one record:

testdatabase=# select * from otp;
 id | username  |    otp
----+-----------+------------
  1 | test_user | RXyObQYNDr

Right now, run the following curl command:

$ curl -H "username:test_user" -H "otp:RXyObQYNDr" -X GET http://localhost:8080/login -v
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET /login HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.69.1
> Accept: */*
> username:test_user
> otp:RXyObQYNDr
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
-------
< Authorization: 7c60f6b3-4047-4fc6-8a37-267661c574f4
-------
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 0
< Date: Mon, 04 Jan 2021 18:45:49 GMT
<
* Connection #0 to host localhost left intact

Authorization: 7c60f6b3-4047-4fc6-8a37-267661c574f4 is the token to access the restricted endpoint

Now access the restricted endpoint via Authorization value:

$ curl -H "Authorization:7c60f6b3-4047-4fc6-8a37-267661c574f4" -X GET http://localhost:8080/hello
hello

If you try to access with the wrong value:

$ curl -H "Authorization:test_value" -X GET http://localhost:8080/hello

// log in the console:
org.springframework.security.authentication.BadCredentialsException: Authorization value is not correct
	at com.mehmetozanguven.springsecuritymultipleproviders.service.providers.TokenAuthProvider.authenticate(TokenAuthProvider.java:27) ~[classes/:na]

Default Project Setup

In this simple project, you will need an JPA and database connection setup (I am going to use Postgresql):

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</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.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

Database Setup

Do not forget to add connection setup to database, here is my configuration:

# Spring DATASOURCE for local postgresql
spring.datasource.url=jdbc:postgresql://localhost:5432/testdatabase
spring.datasource.username=postgres
spring.datasource.password=1234

# For localdb, it would be good to re-create all-tables
spring.jpa.hibernate.ddl-auto=update

And please setup empty table called users which has 3 columns:

[mehmetozanguven@localhost ~]$ sudo -iu postgres
[postgres@localhost ~]$ psql
psql (12.5)
Type "help" for help.

postgres=# \c testdatabase
You are now connected to database "testdatabase" as user "postgres".
testdatabase=# drop table users ; // drop the table if you have previously...
DROP TABLE
testdatabase=# CREATE TABLE IF NOT EXISTS users (id serial PRIMARY KEY, username VARCHAR(50), password VARCHAR(50));
CREATE TABLE

You should also setup another table called otp :

testdatabase=# CREATE TABLE IF NOT EXISTS otp (id serial PRIMARY KEY, username VARCHAR(50), otp VARCHAR(50));
CREATE TABLE

Do not forget to add test user:

testdatabase=# INSERT INTO users (username, password) VALUES ('test_user', '1234');
INSERT 0 1
testdatabase=# select * from users;
 id | username  | password
----+-----------+----------
  1 | test_user | 1234
(1 row)

Password Encoder

I will use NoopPasswordEncoder for this simple project.

Entities, Repositories, Controller and Config Classes

Controller:

package com.mehmetozanguven.springsecuritymultipleproviders.controllers;

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

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
}

Entities:

package com.mehmetozanguven.springsecuritymultipleproviders.entities;

import javax.persistence.*;

@Entity
@Table(name = "users")
public class UserDTO {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;
}
package com.mehmetozanguven.springsecuritymultipleproviders.entities;

import javax.persistence.*;

@Entity
@Table(name = "otp")
public class OtpDTO {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "username")
    private String username;

    @Column(name = "otp")
    private String otp;
}

Repositories:

package com.mehmetozanguven.springsecuritymultipleproviders.repositories;

import com.mehmetozanguven.springsecuritymultipleproviders.entities.UserDTO;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<UserDTO, Long> {
     Optional<UserDTO> findUserDTOByUsername(String username);
}
package com.mehmetozanguven.springsecuritymultipleproviders.repositories;

import com.mehmetozanguven.springsecuritymultipleproviders.entities.OtpDTO;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OtpRepository extends JpaRepository<OtpDTO, Long> {
}

Model to convert UserDTO to UserDetails object:

public class SecureUser implements UserDetails {

    private final UserDTO userDTO;

    public SecureUser(UserDTO userDTO) {
        this.userDTO = userDTO;
    }

    /**
     * Because I will not look at the authority part for now
     * I am just creating dummy authority for the users
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(() -> "read");
    }

    @Override
    public String getPassword() {
        return userDTO.getPassword();
    }

    @Override
    public String getUsername() {
        return userDTO.getUsername();
    }
    // ...
}

UserDetailsService:

package com.mehmetozanguven.springsecuritymultipleproviders.service;

public class PostgresqlUserDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    public PostgresqlUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDTO userInDb = userRepository.findUserDTOByUsername(username)
                                .orElseThrow(() -> new UsernameNotFoundException("User not found in the db"));
        return new SecureUser(userInDb);
    }
}

Configuration:

@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {

    @Bean
    public PostgresqlUserDetailsService userDetailsService() {
        return new PostgresqlUserDetailsService(userRepository);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }
}

Creating Filter and Providers

I do not want to convert ServletRequest to the HttpServletRequest instead I will directly work on the HttpServletRequest extending OncePerRequestFilter

And also I am going to override method OncePerRequestFilter#shouldNotFilter(HttpServletRequest request) to determine for which path(s) this filter will be called.

UsernamePasswordAuthFilter

This filter will be called when path is /login

public class UsernamePasswordAuthFilter extends OncePerRequestFilter {

    // but I didn't defined any manager YET !!!
    // let's define the one inside the configuration
    private AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        String username = httpServletRequest.getHeader("username");
        String password = httpServletRequest.getHeader("password");
        String otp = httpServletRequest.getHeader("otp");

        if (otp == null){
            // authenticate via username and password and generate one time password
        }else{
            // authenticate via one time password and
            // generate Authorization header with random value
            // finally save the header's value elsewhere!!
        }

    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        if (isPathLogin(request)){
            // if path is login then return false, which means: call the doFilterInternal
            return false;
        }else{
            // do not call doFilterInternal
            return true;
        }
    }

    private boolean isPathLogin(HttpServletRequest request){
        return request.getServletPath().equals("/login");
    }
}

Before diving into authentication instances, first let’s define one manager in the configuration class:

@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {
	// ...

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

As you recall from the previous post , there could be Authentication instance and AuthenticationManager will call the appropriate AuthenticationProvider based on the type of the Authentication instance(s). Thus, I should create Authentication classes for each authentication logics.

Authentication Instances

For simplicity, I will extend the UsernamePasswordAuthenticationToken . Here is the Authentication class for otp authentication:

package com.mehmetozanguven.springsecuritymultipleproviders.service.authentications;

public class OtpAuthentication extends UsernamePasswordAuthenticationToken {
    // this constructor creates a Authentication instance which is not fully authenticated
    public OtpAuthentication(Object principal, Object credentials) {
        super(principal, credentials);
    }

    // this constructor creates a Authentication instance which is fully authenticated
    public OtpAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

For Username:password authentication, I will use UsernamePasswordAuthentication

package com.mehmetozanguven.springsecuritymultipleproviders.service.authentications;

public class UsernamePasswordAuthentication extends UsernamePasswordAuthenticationToken {
    // this constructor creates a Authentication instance which is not fully authenticated
    public UsernamePasswordAuthentication(Object principal, Object credentials) {
        super(principal, credentials);
    }

    // this constructor creates a Authentication instance which is fully authenticated
    public UsernamePasswordAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

After my Authentication instances are ready, Filter is ready to pass the instances to the AuthenticationManager

AuthFilter passes Authentication instances to AuthManager

After all Filter will get the result from the Manager.

public class UsernamePasswordAuthFilter extends OncePerRequestFilter {

    private AuthenticationManager authenticationManager;
    private OtpRepository otpRepository;
    private AuthorizationTokenHolder authorizationTokenHolder;

    public UsernamePasswordAuthFilter(AuthenticationManager authenticationManager,
                                      OtpRepository otpRepository,
                                      AuthorizationTokenHolder authorizationTokenHolder) {
        this.authenticationManager = authenticationManager;
        this.otpRepository = otpRepository;
        this.authorizationTokenHolder = authorizationTokenHolder;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        String username = httpServletRequest.getHeader("username");
        String password = httpServletRequest.getHeader("password");
        String otp = httpServletRequest.getHeader("otp");

        if (otp == null){
            // * if there is no one-time-password, then generate an OTP
            // 1) first make sure that, legitimate user is connecting to the your system
            UsernamePasswordAuthentication usernamePasswordAuthentication =
                    new UsernamePasswordAuthentication(username, password);
            // 2) authenticationManager will find the correct provider for that authentication
            Authentication resultForUsernamePassword = authenticationManager.authenticate(usernamePasswordAuthentication);
            // 3) Because authenticationManager.authenticate will return fully authenticated instance
            // ( otherwise it must throw an error), You can generate otp for authenticatied user
            // 4) Generate new otp
            String otpCode = RandomStringUtils.randomAlphabetic(10);
            // 5) save this new one-time-password for that user.
            OtpDTO otpDTO = new OtpDTO();
            otpDTO.setUsername(username);
            otpDTO.setOtp(otpCode);
            otpRepository.save(otpDTO);
        }else{
            // if there is a one-time-password, authenticate user via one-time-password(otp)
            OtpAuthentication otpAuthentication = new OtpAuthentication(username, otp);
            // authenticationManager will find the correct provider for that authentication
            Authentication resultForOtp = authenticationManager.authenticate(otpAuthentication);
            // after getting fully authenticated instance, generate authorization header' value
            String authValue = UUID.randomUUID().toString();
            // save the authorization value for checking future request(s)
            authorizationTokenHolder.add(authValue);
            // add new header to the response
            httpServletResponse.setHeader("Authorization", authValue);
        }
    }
	// ...
}

What I am missing is that, there should be provider(s) for these authentication processes. Let’s define the providers

Creating Providers

Let’s recap what the AuthenticationProvider does:

  • In general AuthenticationProvider contains two methods: authenticate() contains the authentication logic and supports() contains the logic which this authentication provider should be applied or not.

For the authenticate() method, there are three options:

  1. If the request is authenticated, should return authenticated Authentication instance
  2. If the request is not authenticated, throw AuthenticationException
  3. If the Authentication isn’t supported by this provider, then return null, in another words AuthenticationProvider will say the AuthenticationManager: “Hey AuthenticationManager please try to use another Providers, I am not responsible for this

Right now, I need two AuthenticationProvider for each Authentication processes (OtpAuthentication && UsernamePasswordAuthentication )

UsernamePasswordAuthProvider

package com.mehmetozanguven.springsecuritymultipleproviders.service.providers;

public class UsernamePassswordAuthProvider implements AuthenticationProvider {

    private PostgresqlUserDetailsService postgresqlUserDetailsService;
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();

        // loadByUsername will throw an exception if user is not found
        UserDetails userDetails = postgresqlUserDetailsService.loadUserByUsername(username);

        // after finding the user's details, check with the passwordEncoder
        if (passwordEncoder.matches(password, userDetails.getPassword())){
            // if everything is correct, create a fully authenticated object
            // for this simple project, authorities is hard-coded, do not care about
            return new UsernamePasswordAuthenticationToken(username, password, List.of(() -> "read"));
        }

        throw new BadCredentialsException("BadCredentialException");
    }

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

OtpAuthProvider

package com.mehmetozanguven.springsecuritymultipleproviders.service.providers;

public class OtpAuthProvider implements AuthenticationProvider {

    private OtpRepository otpRepository;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String otp = (String) authentication.getCredentials();

        OtpDTO otpDTO = otpRepository.findOtpDtoByUsername(username)
                .orElseThrow(() -> new BadCredentialsException("Bad Otp Credentials"));

        return new OtpAuthentication(username, otp, List.of(() -> "read"));
    }

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

The left is to add these providers to the configuration

Add the Providers to the configuration

@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {
    public UsernamePassswordAuthProvider usernamePassswordAuthProvider() {
        return new UsernamePassswordAuthProvider(...);
    }

    public OtpAuthProvider otpAuthProvider(){
        return new OtpAuthProvider(...);
    }

    // ...
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(usernamePassswordAuthProvider())
            .authenticationProvider(otpAuthProvider());
    }
}

Add the Filter to the configuration

@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {
    public UsernamePasswordAuthFilter usernamePasswordAuthFilter() throws Exception {
        return new UsernamePasswordAuthFilter(...);
    }

    // ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAt(usernamePasswordAuthFilter(), BasicAuthenticationFilter.class);
    }
}

I can create one-time-password, and using this otp I can also generate Authorization header. It is time to authenticate (allow users to access restricted enpoints) using this Authorization header.

Creating the second filter

The second filter will look at the Authorization header in the request and it will allow the request to access endpoint or not.

Because UsernamePasswordAuthFilter was responsible to process the authentication for only /login endpoint, all other endpoints must be filtered.

package com.mehmetozanguven.springsecuritymultipleproviders.service.filters;

public class TokenAuthFilter extends OncePerRequestFilter {

    private AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorization = request.getHeader("Authorization");

        TokenAuthentication tokenAuthentication = new TokenAuthentication(null, authorization);

        Authentication fullyAuthentication = authenticationManager.authenticate(tokenAuthentication);
        SecurityContextHolder.getContext().setAuthentication(fullyAuthentication);
        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        if (isPathLogin(request)){
            // if path is login then return true,
            // which means: do not run the TokenAuthFilter#doFilterInternal,
            // this authentication must be done by UsernamePasswordAuthFilter
            return true;
        }else{
            // for other endpoints run this authentication
            return false;
        }
    }

    private boolean isPathLogin(HttpServletRequest request){
        return request.getServletPath().equals("/login");
    }
}

As I have done previously, I should create a provider and authenticate instance for that filter. Provider will include the authentication login.

Second filter Authentication Instance

Right now, I will have TokenAuthentication which extends UsernamePasswordAuthenticationToken

package com.mehmetozanguven.springsecuritymultipleproviders.service.authentications;

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

import java.util.Collection;

public class TokenAuthentication extends UsernamePasswordAuthenticationToken {
    // this constructor creates a Authentication instance which is not fully authenticated
    public TokenAuthentication(Object principal, Object credentials) {
        super(principal, credentials);
    }

    // this constructor creates a Authentication instance which is fully authenticated
    public TokenAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

Now AuthenticationManager will call the correct authentication provider for that second filter. Let’s implement the provider

Second filter Authentication Provider


public class TokenAuthProvider implements AuthenticationProvider {

    private AuthorizationTokenHolder authorizationTokenHolder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String authorizationToken = (String) authentication.getCredentials();
        boolean isCorrectToken = authorizationTokenHolder.contains(authorizationToken);

        if (isCorrectToken){
            return new TokenAuthentication(null, authorizationToken, List.of(() -> "read"));
        }else {
            throw new BadCredentialsException("Authorization value is not correct");
        }
    }

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

Add the second filter and provider to the configuration

The last thing is to add these new filters and providers to the configuration:

@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {
    // ...

    // second filter and provider
    public TokenAuthFilter tokenAuthFilter() {
        // not using Autowired to avoid circular dependency issue
        return new TokenAuthFilter();
    }

    public TokenAuthProvider tokenAuthProvider() {
        return new TokenAuthProvider(...);
    }

	// ...

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.
                ...
                .authenticationProvider(tokenAuthProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAt(usernamePasswordAuthFilter(), BasicAuthenticationFilter.class)
            .addFilterAfter(tokenAuthFilter(), BasicAuthenticationFilter.class);
    }
}

I will continue with the next one …

You May Also Like