I'm building a micro service with REST endpoints protected by APIKeys for use inside a corporate firewall. I'm trying to add a small, simple UI to the application that will allow a user to login and invoke some of those endpoints.
To that end I have created the following two SecurityFilterChains. Each one works well independently. I can login and retrieve web pages that are authorized to my user, and I can invoke the REST endpoints with the correct API Key.
What's not working is that a REST call without the API Key, but bearing the JSESSIONID of an authenticated user is denied access. I'd like the FilterChain attached to /api/** to permit the call if the necessary role is conveyed by the session associated to the JSESSIONID.
@Bean
@Order(1)
public SecurityFilterChain filterChainREST(HttpSecurity http)
throws Exception
{
APIKeyFilter filter = new APIKeyFilter(API_KEYS);
http.antMatcher("/api/**")
.addFilterBefore(filter, AnonymousAuthenticationFilter.class)
.csrf().disable()
.authorizeHttpRequests(requests -> requests
.antMatchers("/api/1/mgr/**").hasAnyRole("ADMIN", "MGR")
.antMatchers("/api/1/admin/**").hasRole("ADMIN")
.anyRequest().denyAll())
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain filterChainWebUI(HttpSecurity http)
throws Exception
{
http
// Since we have TWO authentication entry points, we must
// have an antMatcher *before* authorizeHttpRequests so this
// filter chain will only respond to those requests.
.antMatcher("/**") // catch anything not caught by filter #1
.csrf().disable()
.authorizeHttpRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/mgr/**").hasAnyRole("ADMIN", "MGR")
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin(Customizer.withDefaults())
.logout(l->l.deleteCookies("JSESSIONID"))
.sessionManagement().
sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.exceptionHandling().accessDeniedPage("/error/accessDenied.html");
return http.build();
}
(The APIKeyFilter invokes SecurityContextHolder.getContext().setAuthentication(auth);
to establish the granted role after it validates the API Key.) If it does not find a valid APIKey, it just invokes filterChain.doFilter()
.
The "/api/**" filter chain session management is set to SessionCreationPolicy.STATELESS so it never picks up user's session from JSESSIONID.
As mentioned in Spring security documentation, if SessionCreationPolicy.STATELESS is used in session management, login is required on every request .
Configuring Persistence for Stateless Authentication
Sometimes there is no need to create and maintain a
HttpSession
for example, to persist the authentication across requests. Some authentication mechanisms like HTTP Basic are stateless and, therefore, re-authenticates the user on every request.https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
if you want to "/api/*" endpoints to be accessible for both api user and loged in user remove STATELESS session policy and make it as same second filter chain session management (SessionCreationPolicy.IF_REQUIRED).
Your code should be like;
@Bean @Order(1) public SecurityFilterChain filterChainREST(HttpSecurity http) throws Exception { APIKeyFilter filter = new APIKeyFilter(API_KEYS); http.antMatcher("/api/**") .addFilterBefore(filter, AnonymousAuthenticationFilter.class) .csrf().disable() .authorizeHttpRequests(requests -> requests .antMatchers("/api/1/mgr/**").hasAnyRole("ADMIN", "MGR") .antMatchers("/api/1/admin/**").hasRole("ADMIN") .anyRequest().denyAll()) .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); return http.build(); } @Bean @Order(2) public SecurityFilterChain filterChainWebUI(HttpSecurity http) throws Exception { http // Since we have TWO authentication entry points, we must // have an antMatcher *before* authorizeHttpRequests so this // filter chain will only respond to those requests. .antMatcher("/**") // catch anything not caught by filter #1 .csrf().disable() .authorizeHttpRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/mgr/**").hasAnyRole("ADMIN", "MGR") .antMatchers("/login*").permitAll() .anyRequest().authenticated() .and() .formLogin(Customizer.withDefaults()) .logout(l->l.deleteCookies("JSESSIONID")) .sessionManagement(). sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .exceptionHandling().accessDeniedPage("/error/accessDenied.html"); return http.build(); }