I'm implementing the flow when user obtains cookie by one-time temporary URL and accesses protected resource using the cookie as authentication token
For this purpose there is CookieAuthenticationFilter
, UserAuthProvider
and SecurityConfig
:
CookieAuthenticationFilter
public class CookieAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
Cookie cookie = Stream.of(Optional.ofNullable(request.getCookies())
.orElse(new Cookie[0]))
.filter(entry -> "token".equals(entry.getName()))
.findFirst()
.orElse(null);
SecurityContextHolder.getContext()
.setAuthentication(new PreAuthenticatedAuthenticationToken(
cookie != null ? cookie.getValue() : "",
null));
filterChain.doFilter(request, response);
}
}
UserAuthProvider
@Component
public class UserAuthProvider implements AuthenticationProvider {
private final AuthenticationService authenticationService;
public UserAuthProvider(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
User user = null;
if (authentication instanceof PreAuthenticatedAuthenticationToken) {
user = authenticationService.validateUser((String) authentication.getPrincipal());
}
return user != null ? new UserAuthenticationToken(user) : null;
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, UserAuthProvider userAuthProvider) throws Exception {
AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class);
builder.authenticationProvider(userAuthProvider);
return builder.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity security, HttpSession session) throws Exception {
security
.csrf(customizer -> customizer.disable())
.addFilterBefore(new CookieAuthenticationFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests(configurer -> configurer
.requestMatchers("/*").authenticated()
.anyRequest().permitAll())
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return security.build();
}
}
Thing is that authenticate
method of UserAuthProvided
is not triggered
Spring Boot 3.3.1
Thanks!
Currently your filter is flawed as you are retrieving and directly setting the SecurityContext
. What you should do is call the AuthenticationManager.authenticate
method with the token you create. That in turn will trigger your AuthenticationProvider
.
Another thing you can do to make things a bit easier is to extend the AbstractPreAuthenticatedProcessingFilter
and provide an AuthenticationUserDetailsService
for integration with the standard PreAuthenticatedAuthenticationProvider
. This will leverage more of the pre-build components instead of re-inventing all of it yourself.
Your filter would look something like this.
public class CookieAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
var cookie = WebUtils.getCookie(request, "token");
return cookie != null ? cookie.getValue() : null;
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "";
}
}
Then you would need an AuthenticationUserDetailsService
which delegates to your AuthenticationService
.
public class CookieAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
private final AuthenticationSerivce authenticationSerivce;
public CookieAuthenticationUserDetailsService(AuthenticationSerivce authenticationSerivce) {
this.authenticationSerivce = authenticationSerivce;
}
@Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) throws UsernameNotFoundException {
// load user from service.
return null;
}
}
Finally you need to configure all this. When doing complex configuration it is often easier to write a Configurer
then to try to access all the beans. The following configurer would configure your pre-auth.
public class CookieAuthenticationConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<JeeConfigurer<H>, H> {
private CookieAuthenticationFilter authenticationFilter;
private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService;
public void init(H http) {
PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
authenticationProvider.setPreAuthenticatedUserDetailsService(this.getUserDetailsService());
authenticationProvider = this.postProcess(authenticationProvider);
http.authenticationProvider(authenticationProvider).setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
}
public void configure(H http) {
CookieAuthenticationFilter filter = this.getFilter(http.getSharedObject(AuthenticationManager.class), http);
http.addFilter(filter);
}
public CookieAuthenticationConfigurer<H> authenticatedUserDetailsService(AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticatedUserDetailsService) {
this.authenticationUserDetailsService = authenticatedUserDetailsService;
return this;
}
private CookieAuthenticationFilter getFilter(AuthenticationManager authenticationManager, H http) {
if (this.authenticationFilter == null) {
this.authenticationFilter = new CookieAuthenticationFilter();
this.authenticationFilter.setAuthenticationManager(authenticationManager);
this.authenticationFilter.setSecurityContextHolderStrategy(this.getSecurityContextHolderStrategy());
this.authenticationFilter = this.postProcess(this.authenticationFilter);
}
return this.authenticationFilter;
}
private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> getUserDetailsService() {
return this.authenticationUserDetailsService != null ? this.authenticationUserDetailsService : new PreAuthenticatedGrantedAuthoritiesUserDetailsService();
}
}
The beauty of this is you don't need to expose the shared Spring Security objects like the AuthenticationManager
etc. Not doing so also saves you from risking to eagerly instantiate beans and leading to startup issues. Next to that the use of the standard components also allows for better integration with Spring / Spring Security as it will also call success/failure handlers, fire events etc. etc.
Your SecurityConfig
can use this configurer as follows.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity security, AuthenticationSerivce authService) throws Exception {
security
.csrf(AbstractHttpConfigurer::disable)
.with(new CookieAuthenticationConfigurer<>(), (pre) -> pre.authenticatedUserDetailsService(new CookieAuthenticationUserDetailsService(authService)))
.authorizeHttpRequests(configurer -> configurer
.requestMatchers("/*").authenticated()
.anyRequest().permitAll())
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return security.build();
}
}
Notice the with
part, that is all you need to register your filter etc. no need to declare @Bean
methods to expose shared objects etc.