Computer scienceBackendSpring BootSpring SecuritySpring Security internalsAuthentication methods

Custom authentication: Creating components

17 minutes read

You've already learned about Spring Security architecture and its components, as well as how to use built-in authentication methods. However, there might be situations where you need to implement a custom authentication process not provided by Spring Security by default. In this topic, we'll look at a practical example to help you better understand how Spring Security components work together and where to insert your custom components.

Token based authentication

We're going to create a demo project where an authenticated user can request a one-time token with a limited lifespan and later use that token for authentication without providing any credentials.

spring security token authentication

According to this scheme, the POST /token endpoint will be secured by basic HTTP authentication and a user will have to provide their login and password to get the one-time token. To access the POST /action endpoint, the user will have to provide the granted token as the value of the Access-Token header (you can choose any name for this header). The token does not contain any information about the user; it's a basic bearer token. Since the project will use two types of authentication, this sets up a few challenges we will solve in the process.

Project setup

Here are the dependencies required to build this project:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    runtimeOnly 'com.h2database:h2'
}

First, let's think about what data a single-use token with a limited time-to-live (TTL) should store? It's supposed to be a bearer token that doesn't contain any user information. It should have a unique value for identification and an expiration time. Since we're going to store tokens in the database, we can create the token entity as follows:

Java
@Entity
public class AccessToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String token;
    private Date expiresAt;

    // getters and setters
}
Kotlin
@Entity
class AccessToken(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    @Column(unique = true)
    var token: String = "",
    var expiresAt: Date = Date()
)

The token property is the unique string value of the token and the expiresAt is the time when the token expires and can no longer be used for authentication.

The AccessToken entity will need a corresponding repository with a method to find a token by its string value:

Java
public interface AccessTokenRepository extends CrudRepository<AccessToken, Long> {
    Optional<AccessToken> findByToken(String token);
}
Kotlin
interface AccessTokenRepository : JpaRepository<AccessToken, Long> {
    fun findByToken(token: String): AccessToken?
}

Additionally, we'll need a REST controller to handle requests:

Java
@RestController
public class DemoController {

    @PostMapping("/token")
    public String token() {
        return "Not yet implemented";
    }

    @PostMapping("/action")
    public String action() {
        return "Requested action has been performed!";
    }
}
Kotlin
@RestController
class DemoController {

    @PostMapping("/token")
    fun token(): String = "Not yet implemented"

    @PostMapping("/action")
    fun action(): String = "Requested action has been performed!"
}

This is the basic project setup, and we'll make it work step by step. Let's start by creating all necessary Spring Security components.

Security filter

You should know that any request has to pass through the security filter chain. We need to add a custom security filter to this chain to intercept the requests we want to authenticate. Our filter will extend the OncePerRequestFilter abstract class to ensure it's called exactly once for each request. We'll apply this filter only to a single endpoint and if authentication fails, we'll reject the request immediately and return an error message to the client. Here's the template for our custom security filter:

Java
public class AccessTokenFilter extends OncePerRequestFilter {
    private final RequestMatcher matcher = 
            new AntPathRequestMatcher("/action", HttpMethod.POST.name());

    private final AuthenticationEntryPoint authenticationEntryPoint = (request, response, ex) -> {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(ex.getMessage());
    };

    private final AuthenticationManager manager;

    public AccessTokenFilter(AuthenticationManager manager) {
        this.manager = manager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        // TODO: not yet implemented 
    }
}
Kotlin
class AccessTokenFilter(private val manager: AuthenticationManager) : OncePerRequestFilter() {
    private val matcher = AntPathRequestMatcher("/action", HttpMethod.POST.name())
    private val authenticationEntryPoint = AuthenticationEntryPoint { _, response, ex ->
        response.status = HttpServletResponse.SC_UNAUTHORIZED
        response.writer.write(ex.message ?: "Exception message is null")
    }

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        // TODO: not yet implemented
    }
}

We'll need these class fields to arrange the authentication process.

We'll attempt to authenticate only POST requests to the /action endpoint. We'll use the RequestMatcher class to check if the incoming request requires authentication. We'll use the AuthenticationEntryPoint implementation to send a response to the client if authentication fails. Finally, we'll delegate the authentication process to the AuthenticationManager to utilize the Spring Security architecture.

The request filtering logic will be inside the doFilterInternal method, provided by the parent class, which we need to override. The logic will be as follows:

  • If the request is a POST /action request, try to authenticate it, otherwise, pass it to the next filter in the chain.

  • If the request has the Access-Token header, extract the token from it. Otherwise, throw a BadCredentialsException.

  • Create a non-authenticated Authentication object and put the token inside it.

  • Pass the Authentication object to the AuthenticationManager and receive back an authenticated Authentication object. If an AuthenticationException is thrown during the authentication process, catch it and invoke the AuthenticationEntryPoint to send an error response to the client.

  • Place the authenticated Authentication object in the SecurityContext and pass the request to the next filter in the chain.

In this demo project, we're going to use custom Authentication and AuthenticationProvider implementations, so let's create them before finishing the AccessTokenFilter class.

Custom AuthenticationProvider

We'll express the token authentication logic in a custom AuthenticationProvider implementation. As you may know, AuthenticationProvider has the supports method that accepts a Class and returns true if it supports authentication of the provided Authentication class or false if not. We want to make sure that our AuthenticationProvider implementation supports only Authentication objects that contain the data we need and no other Authentication objects. The easiest way to achieve this is to create our own implementation of the Authentication interface, let's call it AccessTokenAuthentication, and the corresponding AuthenticationProvider implementation will only support authentication with the AccessTokenAuthentication class.

The Authentication interface is overloaded with methods, so we only need to implement some of them:

Java
public class AccessTokenAuthentication implements Authentication {
    private final String token;
    private boolean authenticated = false;

    public AccessTokenAuthentication(String token) { this.token = token; }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(); }

    @Override
    public Object getCredentials() { return token; }

    @Override
    public Object getDetails() { return null; }

    @Override
    public Object getPrincipal() { return "bearer access token"; }

    @Override
    public boolean isAuthenticated() { return authenticated; }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        authenticated = isAuthenticated;
    }

    @Override
    public String getName() { return "bearer access token"; }
}
Kotlin
class AccessTokenAuthentication(private val token: String) : Authentication {
    private var isAuthenticated = false

    override fun getName(): String = "bearer access token"

    override fun getAuthorities(): Collection<GrantedAuthority> = emptyList()

    override fun getCredentials(): Any = token

    override fun getDetails(): Any = "No details"

    override fun getPrincipal(): Any = "bearer access token"

    override fun isAuthenticated(): Boolean = isAuthenticated

    override fun setAuthenticated(isAuthenticated: Boolean) {
        this.isAuthenticated = isAuthenticated
    }
}

In our case, the AccessTokenAuthentication contains the token string that will be used by the AuthenticationProvider and a boolean field to indicate if the Authentication object is authenticated or not. You can implement this class differently according to your requirements but remember that the getAuthorities() and getPrincipal() methods must not return null for an authenticated object.

Now we can create a custom AuthenticationProvider that supports the AccessTokenAuthentication class:

Java
@Component
public class AccessTokenAuthenticationProvider implements AuthenticationProvider {
    private final AccessTokenRepository repository;

    public AccessTokenAuthenticationProvider(AccessTokenRepository repository) {
        this.repository = repository;
    }

    @Transactional(noRollbackFor = BadCredentialsException.class)
    @Override
    public Authentication authenticate(Authentication authentication) {
        var token = authentication.getCredentials().toString();

        AccessToken accessToken = repository
                .findByToken(token)
                .orElseThrow(() -> new BadCredentialsException("Invalid access token"));

        repository.deleteById(accessToken.getId());

        if (new Date().after(accessToken.getExpiresAt())) {
            throw new BadCredentialsException("Invalid access token");
        }

        authentication.setAuthenticated(true);
        return authentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return AccessTokenAuthentication.class.equals(authentication);
    }
}
Kotlin
@Component
class AccessTokenAuthenticationProvider(
    private val repository: AccessTokenRepository
) : AuthenticationProvider {

    @Transactional(noRollbackFor = [BadCredentialsException::class])
    override fun authenticate(authentication: Authentication): Authentication {
        val token = authentication.credentials.toString()

        val accessToken = repository.findByToken(token)
            ?: throw BadCredentialsException("Invalid token")

        repository.deleteById(requireNotNull(accessToken.id))

        if (Date().after(accessToken.expiresAt)) {
            throw BadCredentialsException("Invalid token")
        }

        authentication.isAuthenticated = true
        return authentication
    }

    override fun supports(authentication: Class<*>?): Boolean =
        AccessTokenAuthentication::class.java == authentication
}

This class is annotated with @Component because we need to inject the AccessTokenRepository and we want Spring to handle the injection for us. This is not a requirement, and we could have done the repository injection manually. The supports method returns true if the provided class is AccessTokenAuthentication and false otherwise. This means that AuthenticationManager will call this AuthenticationProvider only for AccessTokenAuthentication objects and not for any other Authentication implementations.

The authentication logic retrieves the token value from the Authentication object and fetches the corresponding AccessToken entity from the database. If no entity is found, the token is not valid and a BadCredentialsException is thrown. Then the entity is deleted from the database because it's a one-time token. If the token has already expired, a BadCredentialsException is thrown. If the token is valid, the Authentication object is authenticated and returned to the caller. That's it!

You don't actually have to throw exceptions and can simply return the unauthenticated Authentication to the caller and adjust the security filter logic accordingly. There's always room for creativity.

Do the filtering

Now everything is ready and we can implement the doFilterInternal method:

Java
public class AccessTokenFilter extends OncePerRequestFilter {
    private final RequestMatcher matcher = 
            new AntPathRequestMatcher("/action", HttpMethod.POST.name());

    private final AuthenticationEntryPoint authenticationEntryPoint = (request, response, ex) -> {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(ex.getMessage());
    };

    private final AuthenticationManager manager;

    public AccessTokenFilter(AuthenticationManager manager) {
        this.manager = manager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        if (matcher.matches(request)) {
            try {
                var token = request.getHeader("Access-Token");
                if (token != null) {
                    Authentication authentication = new AccessTokenAuthentication(token);
                    authentication = manager.authenticate(authentication);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    filterChain.doFilter(request, response);
                    return;
                }
                throw new BadCredentialsException("Access token is required");
            } catch (AuthenticationException e) {
                authenticationEntryPoint.commence(request, response, e);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}
Kotlin
class AccessTokenFilter(private val manager: AuthenticationManager) : OncePerRequestFilter() {
    private val matcher = AntPathRequestMatcher("/action", HttpMethod.POST.name())
    private val authenticationEntryPoint = AuthenticationEntryPoint { _, response, ex ->
        response.status = HttpServletResponse.SC_UNAUTHORIZED
        response.writer.write(ex.message ?: "Exception message is null")
    }

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        if (matcher.matches(request)) {
            try {
                val token = request.getHeader("Access-Token")
                if (token != null) {
                    var authentication: Authentication = AccessTokenAuthentication(token)
                    authentication = manager.authenticate(authentication)
                    SecurityContextHolder.getContext().authentication = authentication
                    filterChain.doFilter(request, response)
                    return
                }
                throw BadCredentialsException("Access token is required")
            } catch (e: AuthenticationException) {
                authenticationEntryPoint.commence(request, response, e)
                return
            }
        }
        filterChain.doFilter(request, response)
    }
}

This completes the filter algorithm. For matching requests, it fetches the Access-Token header value, creates an Authentication object, and tries to authenticate it via the AuthenticationManager. The authenticated Authentication object is then put into the SecurityContext and the request is passed to the next filter in the chain. If authentication fails, the filter invokes the AuthenticationEntryPoint logic and breaks the chain.

Now, all components of the custom authentication method are ready. In the next topic, we'll wire them all together to add token authentication alongside basic HTTP authentication.

Conclusion

In this topic, you've explored the process of creating custom authentication components for a token authentication flow. You've learned how to create custom implementations of the Authentication and AuthenticationProvider interfaces. Furthermore, you've understood how a security filter works to process requests and how to use the RequestMatcher, AuthenticationManager, and AuthenticationEntryPoint to implement the filter logic.

16 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo