To create a custom authentication, you need to generate all the necessary components for your case and then link them correctly to build a fully functional application. In this topic, we'll delve into the process of configuring Spring Security using both standard and custom components.
Project setup
In the previous discussion, we arranged a demo project and developed all the needed components, except for a config class and the REST controller. Here's what we have so far:
The project build file:
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'
}The token entity:
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 repository:
Java
public interface AccessTokenRepository extends CrudRepository<AccessToken, Long> {
Optional<AccessToken> findByToken(String token);
}Kotlin
interface AccessTokenRepository : JpaRepository<AccessToken, Long> {
fun findByToken(token: String): AccessToken?
}The custom Authentication implementation:
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
}
}The custom AuthenticationProvider implementation:
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
}The 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 {
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)
}
}Now we're all set to complete the project and test it using various scenarios to ensure it's working as anticipated. Let's begin!
Token generation
For simplicity, all requests will be handled by a single REST controller, and the logic will be embedded in its methods. We will integrate the AccessTokenRepository into the controller class, and when a client sends a POST request to the /token endpoint, we'll generate a token, save it in the database, and then return it as a string to the client. When a client sends a POST request to the /action endpoint, we'll return a string signifying the access token used for authentication:
Java
@RestController
public class DemoController {
private final AccessTokenRepository repository;
public DemoController(AccessTokenRepository repository) {
this.repository = repository;
}
@PostMapping("/token")
public String token() {
var bytes = KeyGenerators.secureRandom(10).generateKey();
var hexString = new BigInteger(1, bytes).toString(16);
var token = new AccessToken();
token.setToken(hexString);
var timestamp = System.currentTimeMillis() + 30_000; // TTL = 30 seconds
token.setExpiresAt(new Date(timestamp));
repository.save(token);
return hexString;
}
@PostMapping("/action")
public String action(Authentication authentication) {
return "Requested action has been performed with token=" + authentication.getCredentials();
}
}Kotlin
@RestController
class DemoController(private val repository: AccessTokenRepository) {
@PostMapping("/token")
fun token(): String {
val bytes = KeyGenerators.secureRandom(10).generateKey()
val hexString = BigInteger(1, bytes).toString(16)
val timestamp = System.currentTimeMillis() + 30_000 // TTL = 30 seconds
val token = AccessToken(
token = hexString,
expiresAt = Date(timestamp)
)
repository.save(token)
return hexString
}
@PostMapping("/action")
fun action(authentication: Authentication): String =
"Requested action has been performed with token=${authentication.credentials}"
}The token gets created as 10 random bytes that are later converted into a hexadecimal string. Also, we set the token's expiration time to 30 seconds after the current time. The resulting string isn't very long, but considering the token's short lifespan, the chance of collision is quite low. In a real project, the token length should be thoughtfully estimated, and a retry action should be provided in case of a collision.
Configuring the filter chain
Our AccessTokenFilter needs an AuthenticationManager object. However, we can't simply inject it as a bean or access it during the SecurityFilterChain building. There's still a way to obtain the AuthenticationManager, but it will require some additional steps. We will need to create a custom configurer class that extends AbstractHttpConfigurer and then apply it to the HttpSecurity builder.
Java
public class MyConfigurer extends AbstractHttpConfigurer<MyConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) {
AuthenticationManager manager = builder.getSharedObject(AuthenticationManager.class);
builder.addFilterAfter(
new AccessTokenFilter(manager),
UsernamePasswordAuthenticationFilter.class
);
}
}Kotlin
class MyConfigurer : AbstractHttpConfigurer<MyConfigurer, HttpSecurity>() {
override fun configure(builder: HttpSecurity) {
val authenticationManager = builder.getSharedObject(AuthenticationManager::class.java)
builder.addFilterAfter(
AccessTokenFilter(authenticationManager),
UsernamePasswordAuthenticationFilter::class.java
)
}
}In the MyConfigurer class, we override one of the AbstractHttpConfigurer methods to get the AuthenticationManager instance, inject it into our AccessTokenFilter, and insert the filter into the filter chain after the UsernamePasswordAuthenticationFilter. This method allows us to gain a configured AuthenticationManager object.
The specific placement of a new security filter in the security filter chain is significant if you wish to accomplish certain behaviors of multiple authentication filters, like in our case. Regardless, any authentication filter must be before the AuthorizationFilter because authorization should always follow successful authentication. In addition to addFilterAfter, you can use addFilter, addFilterBefore and addFilterAt methods, depending on where you want to inject the new filter.
Now let's develop a config class to finish the security configuration:
Java
@Configuration
public class SecurityConfig {
private final AccessTokenAuthenticationProvider provider;
public SecurityConfig(AccessTokenAuthenticationProvider provider) {
this.provider = provider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
var configurer = new MyConfigurer();
http.apply(configurer);
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/token").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(provider)
.userDetailsService(userDetailsService())
.csrf(AbstractHttpConfigurer::disable)
.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("user")
.password("{noop}123")
.authorities("ROLE_USER")
.build();
var userManager = new InMemoryUserDetailsManager();
userManager.createUser(user);
return userManager;
}
}Kotlin
@Configuration
class SecurityConfig(private val provider: AccessTokenAuthenticationProvider) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val configurer = MyConfigurer()
http.apply(configurer)
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/token").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(provider)
.userDetailsService(userDetailsService())
.csrf { it.disable() }
.build()
}
@Bean
fun userDetailsService(): UserDetailsService {
val user = User.withUsername("user")
.password("{noop}123")
.authorities("ROLE_USER")
.build()
val userManager = InMemoryUserDetailsManager()
userManager.createUser(user)
return userManager
}
}For simplicity, we created a single hardcoded user and an in-memory UserDetailsService. We also don't hash the user's password to keep the code short. Note that first we create an instance of the MyConfigurer class and apply it to the HttpSecurity. After that, we configure the filter chain using the HttpSecurity builder as usual.
We require that all requests are authenticated except for /token. In addition, we add our custom AuthenticationProvider to the list of providers and explicitly specify which UserDetailsService implementation we use. We deactivate the cross-site reference forgery (CSRF) protection so we can test the application using Postman. By default, the CSRF protection is enabled for modifying requests, such as POST, PUT, and DELETE, and in our case, it's easier to disable this protection than to set the proper csrf token in Postman.
Authentication showcase
If we start our application and send a request to the POST /token endpoint with the correct username and password (the Authorization tab, Basic Auth option), we'll get a random token:
Then we can add the Access-Token header to a request to the POST /action endpoint, paste the received token as the header's value, and receive the following response:
The request was successfully authenticated, and the Authentication contained the proper token string as credentials. Notice that we didn't use basic HTTP authentication in this instance!
If we try to use the same token again, the authentication will fail. The same will happen if we use a token 30 seconds after its creation:
Notice that without a valid token, any request will be rejected, even if we use basic HTTP authentication with the correct user credentials. You can test it yourself.
Conclusion
In this topic, you learned how to create a secure random token using the KeyGenerators class provided by Spring Security. You also learned about getting the AuthenticationManager during the security configuration process and how to apply a custom configurator to the HttpSecurity builder. This allows you to combine custom authentication and standard basic HTTP within a single SecurityFilterChain, enabling multiple authentication methods in a project. Finally, you learned how to test different authentication scenarios using Postman.