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.
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 /actionrequest, try to authenticate it, otherwise, pass it to the next filter in the chain.If the request has the
Access-Tokenheader, extract the token from it. Otherwise, throw aBadCredentialsException.Create a non-authenticated
Authenticationobject and put the token inside it.Pass the
Authenticationobject to theAuthenticationManagerand receive back an authenticatedAuthenticationobject. If anAuthenticationExceptionis thrown during the authentication process, catch it and invoke theAuthenticationEntryPointto send an error response to the client.Place the authenticated
Authenticationobject in theSecurityContextand 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.