Spring Security: Custom Pre-Authentication Flow

Spring Security: Custom Pre-Authentication Flow

·

12 min read

Introduction

In one of my project setups, the authentication process is offloaded to Keycloak and set up with an Open Policy Agent sidecar in a typical Kubernetes deployment. Any request that comes to the service, will be pre-authenticated, and all credentials/information are stored in the headers.

Since I'm using Spring Boot with Spring Security, I wanted to figure out if there's a way to integrate seamlessly within the application through Spring Security without a custom solution (i.e. writing custom filters). After some research, I figured that the most suitable way to do so is via Pre-Authentication Scenarios described in the documentation.

Goal

The goal is to be able to use the header(s) provided, construct and build into an Authentication object, specifically a PreAuthenticatedAuthenticationToken object, which can then use for all security-related needs.

In this tutorial, I explore how to make use of RequestHeaderAuthenticationFilter, an existing implementation provided by Spring Security, that relies on a header to identify and extract the username.

Spring Stack

At the time of writing, I am using the latest iteration of Spring Boot 2.7.13 with Spring Security 5.8.4 to ensure minimal changes are required when upgrading to Spring Boot 3.x and Spring Security 6.x in the future.

Brief Intro to Spring Security

Before I go any further, I want to briefly go through the architecture of Spring Security, specifically in this context.

  • When a client sends an HTTP request to the web server

  • It passes through several Servlet Filters, one of which is DelegatingFilterProxy (created by Spring) to bridge between Servlet Lifecycle and Spring ApplicationContext

  • Spring Security creates a FilterChainProxy to support SecurityFilterChain where it can have multiple instances of SecurityFilterChain

  • Each of the SecurityFilterChain can have one or more Filter registered, in this case, we are looking specifically at RequestHeaderAuthenticationFilter which extends AbstractPreAuthenticatedProcessingFilter

Implementation

Although Spring comes with some existing implementation, it doesn't quite fit my use case as I have a custom UserDetails and I need to load my user permission from the database, hence I also need to write my custom AuthenticationUserDetailsService and so on. Fortunately, Spring is super flexible and extensible, hence, I can easily extend and write my implementation as needed.

Without further ado, let's see how to implement this.

Initialize project

This project is generated from start.spring.io

Overwrite Spring Security version

Spring Boot 2.7.13 comes with Spring Security 5.7.9 by default, so I have to overwrite the version to use Spring Security 5.8.4.

In pom.xml, add the following under properties section

<properties>
    <spring-security.version>5.8.4</spring-security.version>
</properties>

Start Application

This is an extracted portion of the logs when starting the application

2023-07-08 17:05:12.169  WARN 8820 --- [  restartedMain] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: 70e6538f-cc9f-4764-9959-cd1d2a6cd5b5

This generated password is for development use only. Your security configuration must be updated before running your application in production.

2023-07-08 17:05:12.500  INFO 8820 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@3f690985, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@458bfb19, org.springframework.security.web.context.SecurityContextPersistenceFilter@5e6593, org.springframework.security.web.header.HeaderWriterFilter@1cce17a1, org.springframework.security.web.csrf.CsrfFilter@238de12d, org.springframework.security.web.authentication.logout.LogoutFilter@36d43e9f, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@3c373438, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@7fd33069, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@6c864ee5, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5a740d9c, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@329f9df5, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@27623b68, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@66e2dd0b, org.springframework.security.web.session.SessionManagementFilter@692b3e18, org.springframework.security.web.access.ExceptionTranslationFilter@13ca9f33, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1cd5e041]

It's quite difficult to see from the logs what filters are being applied, so let's see it from a different format.

Security filter chain: [
  DisableEncodeUrlFilter
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CsrfFilter
  LogoutFilter
  UsernamePasswordAuthenticationFilter
  DefaultLoginPageGeneratingFilter
  DefaultLogoutPageGeneratingFilter
  BasicAuthenticationFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]

A total of 16 default filters

Two key things to learn from the log:

  1. Default Generated Password

  2. Default SecurityFilterChain

Without writing a single line of code, the application is protected by default through a series of sensible defaults (with best practices) provided out of the box by Spring Security. Isn't that awesome?

Read more of what's happening behind the scene in the documentation

Configure SecurityFilterChain

Create WebSecurityConfig class to define our own SecurityFilterChain and let's disable everything that is not required.

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .formLogin(AbstractHttpConfigurer::disable)
            .httpBasic(AbstractHttpConfigurer::disable)
            .anonymous(AbstractHttpConfigurer::disable)
            // we don't need to enable csrf, as 1) no view, 2) no session cookie authn
            .csrf(AbstractHttpConfigurer::disable)
            // disable logout
            .logout(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .build();
    }
}

Exclude UserDetailsServiceAutoConfiguration to prevent it from creating a default user and (generated) password via the application.yaml

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration

Run the application again, and the updated SecurityFilterChain looks like this now

Security filter chain: [
  DisableEncodeUrlFilter
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  SessionManagementFilter
  ExceptionTranslationFilter
]

Down to 8 filters (50% less!)

Start the application again, and the log that states the generated password is now gone (disable it via application.yaml).

Configure custom UserDetails

It is very common to have a custom version of UserDetails, and mine is no different.

public class PreAuthUserDetails implements UserDetails {
    private final String username;
    private final List<GrantedAuthority> authorities;

    public PreAuthUserDetails(String username, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.authorities = new ArrayList<>(authorities);
    }

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

    @Override
    public String getPassword() {
        return "N/A";
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

Configure RequestHeaderAuthenticationFilter

As mentioned, I want to reuse the existing implementation as much as possible and overwrite it only when necessary. So let's create a RequestHeaderAuthenticationFilter bean and configure it.

@Bean
public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter() {
    RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter();
    requestHeaderAuthenticationFilter.setPrincipalRequestHeader("X-User");
    requestHeaderAuthenticationFilter.setExceptionIfHeaderMissing(true);

    return requestHeaderAuthenticationFilter;
}

What RequestHeaderAuthenticationFilter does is extracting the principal from X-User header which will be used in AbstractPreAuthenticatedProcessingFilter#doAuthenticate to create PreAuthenticatedAuthenticationToken object.

While writing this blog, I realize that RequestHeaderAuthenticationFilter is not registered as part of the Spring Security Filter Chain but as a ServletFilter. So I wrote a separate post to explain why

Start the application now, and the following error will surface

Caused by: java.lang.IllegalArgumentException: An AuthenticationManager must be set
        at org.springframework.util.Assert.notNull(Assert.java:201) ~[spring-core-5.3.28.jar:5.3.28]
        at org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter.afterPropertiesSet(AbstractPreAuthenticatedProcessingFilter.java:127) ~[spring-security-web-5.8.4.jar:5.8.4]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.28.jar:5.3.28]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.28.jar:5.3.28]
        ... 56 common frames omitted

This is because RequestHeaderAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter which requires an implementation of AuthenticationManager and this brings us to the next point.

Register AuthenticationManager

An AuthenticationManager must be provided to perform the authenticate method. But before that, there are two key components (ProviderManager and AuthenticationProvider) we need to understand first.

AuthenticationManager is the API that defines how Spring Security’s Filters perform authentication.

ProviderManager

ProviderManager is the most commonly used implementation of AuthenticationManager

AuthenticationProvider

Multiple AuthenticationProviders can be injected into ProviderManager. Each AuthenticationProvider performs a specific type of authentication.

The explanation is taken directly from the documentation here, here and here.

In short, for RequestHeaderAuthenticationFilter to perform doAuthenticate. It requires the implementation of AuthenticationManager which outsources the actual authentication through the AuthenticationProvider, which relies on AuthenticationUserDetailsService to create and return the UserDetails object.

Since I am using a custom UserDetails object, I will need to create a custom AuthenticationUserDetailsService to construct our PreAuthUserDetails object.

There is no need to create custom PreAuthenticatedAuthenticationProvider class since it has all I needed

Configure AuthenticationUserDetailsService

public class PreAuthUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {

    @Override
    public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) throws UsernameNotFoundException {
        // hard-coded authority
        List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ADMIN"));
        return this.buildUserDetails(token, authorities);
    }

    protected UserDetails buildUserDetails(PreAuthenticatedAuthenticationToken token, Collection<? extends GrantedAuthority> authorities) {
        return new PreAuthUserDetails(token.getName(), authorities);
    }

}

The nice thing about this is that it doesn't care how the user details object is constructed. It could be loaded from the database, hard-coded, or anything as long it returns the UserDetails object.

For this, I will create a hard-coded authority first. In the later part of the blog, I will explore how to get this from the headers.

Update WebSecurityConfig

With all the necessary classes created, and configured. It is time to update the WebSecurityConfig class to reflect the changes I've made thus far.

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfig {

    @Bean // 1
    public AuthenticationProvider authenticationProvider() {
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(new PreAuthUserDetailsService());

        return provider;
    }

    @Bean // 2
    public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider) {
        return new ProviderManager(authenticationProvider);
    }

    @Bean // 3
    public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter(AuthenticationManager authenticationManager) {
        RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter();
        requestHeaderAuthenticationFilter.setPrincipalRequestHeader("X-User");
        requestHeaderAuthenticationFilter.setExceptionIfHeaderMissing(true);
        requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);

        return requestHeaderAuthenticationFilter;
    }

    @Bean // 4
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .formLogin(AbstractHttpConfigurer::disable)
            .httpBasic(AbstractHttpConfigurer::disable)
            .anonymous(AbstractHttpConfigurer::disable)
            // we don't need to enable csrf, as 1) no view, 2) no session cookie authn
            .csrf(AbstractHttpConfigurer::disable)
            // disable logout
            .logout(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .build();
    }
}
  1. Register AuthenticationProvider bean and setting PreAuthUserDetailsService

  2. Register AuthenticationManager bean

  3. Update RequestHeaderAuthenticationFilter bean to set AuthenticationManager

  4. No change

This is in line with what was drawn in the diagram above on the dependency between each class

At this stage, I have everything ready and the application should also start up just fine. Next, I will write some tests to verify if this is indeed working as intended.

Verification

Endpoint

Write a simple endpoint that returns UserDetails.

@RestController
public class MeController {

    @GetMapping("/me")
    public UserDetails me(@AuthenticationPrincipal UserDetails userDetails) {
        return userDetails;
    }
}

@AuthenticationPrincipal helps to resolve the current authenticated user

The controller exposes an HTTP GET API endpoint (/me) that returns the authenticated user details.

Test Case

Then write a test to verify the interaction.

import org.springframework.http.MediaType;

@WebMvcTest(MeController.class)
@Import(WebSecurityConfig.class)
class MeControllerTests {
    @Autowired
    private MockMvc mockMvc;

    @Test
    void whenCallMe_shouldGetValidResponse2() throws Exception {
        this.mockMvc
            .perform(MockMvcRequestBuilders
                .get("/me")
                .header("X-User", "joseph"))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("joseph"))
            .andExpect(MockMvcResultMatchers.jsonPath("$.authorities").isArray())
            .andExpect(MockMvcResultMatchers.jsonPath("$.authorities[0].authority").value("ADMIN"))
            .andDo(MockMvcResultHandlers.print());
    }
}

Note that for sliced tests such as @WebMvcTest, we need to import our security configuration since Spring Boot 2.7.x due to the deprecated WebSecurityConfigurerAdapter.

To send the request via curl

curl localhost:8080/me -H "X-User: joseph"

The test does the following

  • Configure mockMvc to simulate an HTTP GET request to /me with X-User header

  • Assert the (JSON) response contains the username and authorities

  • Print out the log

Here's the log of the request after the test case is ran

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /me
       Parameters = {}
          Headers = [X-User:"joseph"]
             Body = null
    Session Attrs = {SPRING_SECURITY_CONTEXT=SecurityContextImpl [Authentication=PreAuthenticatedAuthenticationToken [Principal=PreAuthUserDetails(username=joseph, authorities=[ADMIN]), Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ADMIN]]]}

Handler:
             Type = com.bwgjoseph.springsecuritycustompreauthenticationflow.MeController
           Method = com.bwgjoseph.springsecuritycustompreauthenticationflow.MeController#me(UserDetails)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json", 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 type = application/json
             Body = {"username":"joseph","authorities":[{"authority":"ADMIN"}],"enabled":true,"credentialsNonExpired":true,"accountNonExpired":true,"accountNonLocked":true,"password":"N/A"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

It works! Of course, it does, I've spent quite some time writing this blog post, ensuring that it works, so why wouldn't it? 🤣🤣

Configure AuthenticationDetailsSource

But.... is that all? Not quite. Remember that I hard-coded the authorities to ADMIN in our existing implementation? Now, how can we get the value from the other header key? Knowing that RequestHeaderAuthenticationFilter supports only getting one header value, which is the username/principal. So how do we extract the additional headers from the request headers, and use it?

For this, I need to create a custom implementation of AuthenticationDetailsSource and WebAuthenticationDetails and pass it to RequestHeaderAuthenticationFilter. Before that, let's try to understand why I need to do so.

If we look at the current implementation of AbstractPreAuthenticatedProcessingFilter and zoom into the following code

PreAuthenticatedAuthenticationToken authenticationRequest = new PreAuthenticatedAuthenticationToken(principal, credentials);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));

We can learn that there is a property - details that we can store any information as long as it is an Object type through AuthenticationDetailsSource. Knowing this, I can then use it to store the additional headers information through the custom implementation of AuthenticationDetailsSource and provide it to RequestHeaderAuthenticationFilter.

public class PreAuthAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new PreAuthenticationDetails(context);
    }

}

The key here is the implementation of PreAuthenticationDetails where it will grab the header from the request.

@EqualsAndHashCode(callSuper = true)
public class PreAuthenticationDetails extends WebAuthenticationDetails implements GrantedAuthoritiesContainer {
    private static final String HEADER_AUTHORITY = "X-Authorities";
    private final List<String> authorities;

    public PreAuthenticationDetails(HttpServletRequest request) {
        super(request);
        this.authorities = List.of(request.getHeader(HEADER_AUTHORITY).split(","));
    }

    @Override
    public Collection<? extends GrantedAuthority> getGrantedAuthorities() {
        return this.authorities.stream().map(SimpleGrantedAuthority::new).toList();
    }

}
  1. In addition to extending WebAuthenticationDetails, I'm also implementing GrantedAuthoritiesContainer to indicate that this object (PreAuthenticationDetails) can be used to obtain user authorities

  2. I define the header to be of X-Authorities and expect it to be a comma-separated string format

This is just an example of how I can get additional headers value. The number of headers or the format of the headers is entirely dependent on the implementor.

Ensure that proper validation is done while parsing the header such as checking if the key exists, the value is null, isBlank and so on

Update PreAuthUserDetailsService

With that, I will be getting the authorities from the PreAuthenticationDetails instead of using the hard-coded values.

public class PreAuthUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {

    @Override
    public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) throws UsernameNotFoundException {
        // update to get the details from token, instead of hardcoded value
        PreAuthenticationDetails details = (PreAuthenticationDetails) token.getDetails();
        return this.buildUserDetails(token, details.getGrantedAuthorities());
    }

    protected UserDetails buildUserDetails(PreAuthenticatedAuthenticationToken token, Collection<? extends GrantedAuthority> authorities) {
        return new PreAuthUserDetails(token.getName(), authorities);
    }
}

Update WebSecurityConfig

Lastly, I need to update the WebSecurityConfig class again to configure RequestHeaderAuthenticationFilter to use my custom AuthenticationDetailsSource class.

@Bean
public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter(AuthenticationManager authenticationManager) {
    RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter();
    requestHeaderAuthenticationFilter.setPrincipalRequestHeader("X-User");
    requestHeaderAuthenticationFilter.setExceptionIfHeaderMissing(true);
    requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
    // add this line
    requestHeaderAuthenticationFilter.setAuthenticationDetailsSource(new PreAuthAuthenticationDetailsSource());

    return requestHeaderAuthenticationFilter;
}

Update Test Case

I also need to update my test case to include the additional header and assertions.

@Test
void whenCallMe_shouldGetValidResponse() throws Exception {
    this.mockMvc
        .perform(MockMvcRequestBuilders
            .get("/me")
            .header("X-User", "joseph")
            .header("X-Authorities", "moderator,user"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("joseph"))
        .andExpect(MockMvcResultMatchers.jsonPath("$.authorities").isArray())
        .andExpect(MockMvcResultMatchers.jsonPath("$.authorities[0].authority").value("moderator"))
        .andExpect(MockMvcResultMatchers.jsonPath("$.authorities[1].authority").value("user"))
        .andDo(MockMvcResultHandlers.print());
}

The output of the test is as follows

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /me
       Parameters = {}
          Headers = [X-User:"joseph", X-Authorities:"moderator,user"]
             Body = null
    Session Attrs = {SPRING_SECURITY_CONTEXT=SecurityContextImpl [Authentication=PreAuthenticatedAuthenticationToken [Principal=PreAuthUserDetails(username=joseph, authorities=[moderator, user]), Credentials=[PROTECTED], Authenticated=true, Details=PreAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[moderator, user]]]}

Handler:
             Type = com.bwgjoseph.springsecuritycustompreauthenticationflow.MeController
           Method = com.bwgjoseph.springsecuritycustompreauthenticationflow.MeController#me(UserDetails)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json", 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 type = application/json
             Body = {"username":"joseph","authorities":[{"authority":"moderator"},{"authority":"user"}],"enabled":true,"password":"N/A","accountNonExpired":true,"credentialsNonExpired":true,"accountNonLocked":true}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

To send the request via curl

curl localhost:8080/me -H "X-User: joseph" -H "X-Authorities: moderator,user"

With that, I have everything I needed for the entire implementation.

What's next?

I intend to build and package the implementation into a spring-boot-starter library shortly, and then open-source it internally within my organization so that other projects can benefit from it and use it with zero-configuration.

Conclusion

In summary, we have looked at

  • Spring Security Architecture

    • Understanding core components (AuthenticationManager, Filter Chain, ProviderManager, AuthenticationProvider)
  • Configuring SecurityFilterChain

  • Custom implementation of the various classes (PreAuthUserDetailsService, PreAuthAuthenticationDetailsSource, PreAuthenticationDetails) when the default implementation doesn't quite suit our needs

  • Digging into some of the internal implementations of Spring Security

  • Wiring up everything together

Documenting the various insights and digging into some of the internal implementations helps me to learn that it is not that difficult to implement it. And finally, allow me to gain more insights into Spring Security Architecture which has been quite elusive to me for the longest time. This should also serve as a note for myself when I start to forget about the details in the future!

Source Code

As usual, the full source code is available on GitHub

Reference