I have a custom authentication mechanism for my app. I have token authentication for /api/**
endpoints and login form for /manager/**
and /viewer/**
. I have @RestController
for api and @Contoller
web pages. The problem occures when I access the pages - the static content doesn't load and request are responded with net::ERR_ABORTED 401 (Unauthorized)
. The request for the page (/viewer/home
) also responded with 401 but has actual html body which is then rendered and dtos are also loaded. Worth mentioning that calling in browser http://localhost:8080/css/operator.css
returns a css file without errors.
@GetMapping("/viewer/home")
public String operatorHome(Model model) {
// logic...
model.addAttribute("files", dtos);
return "operator-home";
}
The security configuration is:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Autowired
private AuthenticationService authService;
@Bean
@Order(1)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/manager/**").hasAuthority(Role.MANAGER.name())
.requestMatchers("/viewer/**").hasAuthority(Role.OPERATOR.name())
.requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().permitAll()
)
.formLogin((form) -> form.successHandler(new CustomAuthenticationSuccessHandler()))
.logout(LogoutConfigurer::permitAll)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/api/**").hasAuthority(Role.USER.name())
.anyRequest().permitAll()
)
.addFilterBefore(new TokenAuthenticationFilter(authService), UsernamePasswordAuthenticationFilter.class)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.httpBasic(Customizer.withDefaults())
;
return http.build();
}
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(authService);
}
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
}
The problem persists even if I comment out second filterchain (api) and leave anyRequest().permitAll()
. I tested rest api in Postman and security works just fine there.
HTML:
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <!-- this is fine -->
<link rel="stylesheet" type="text/css" th:href="@{/css/operator.css}"> <!-- this is not loaded -->
Logs for retreiving the HTML page:
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.security.web.FilterChainProxy : Securing GET /viewer/home
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=test_operator, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[OPERATOR]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=9E3858A4DC3CD548374F4874C21539D6], Granted Authorities=[OPERATOR]]]
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.security.web.FilterChainProxy : Secured GET /viewer/home
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet : GET "/viewer/home", parameters={}
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.lavkatech.audiorecognition.controller.WebController#operatorHome(Model)
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] org.hibernate.SQL : select ao1_0.id,ao1_0.audio_len,ao1_0.checked_by_id,ao1_0.checked_on,ao1_0.file_loc,ao1_0.file_name,ao1_0.file_size,ao1_0.file_text,ao1_0.is_checked,ao1_0.op_end_time,ao1_0.op_start_time,ao1_0.requested_by_value from files ao1_0
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8, application/signed-exchange;v=b3;q=0.7]
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet : Completed 401 UNAUTHORIZED
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Securing GET /css/operator.css
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Secured GET /css/operator.css
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : GET "/css/operator.css", parameters={}
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : Completed 401 UNAUTHORIZED
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=test_operator, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[OPERATOR]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=9E3858A4DC3CD548374F4874C21539D6], Granted Authorities=[OPERATOR]]]
As was pointed out by @dur in the comments the second filter wasn't ever reached. Also two filtes were initialized in the configuration.
1:
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(authService);
}
2:
.addFilterBefore(new TokenAuthenticationFilter(authService), UsernamePasswordAuthenticationFilter.class)
Because the filter was exposed in a bean it was used for the autorization together with Dao which is specified in the webFilterChain
as formLogin
. To handle it I used securityMatcher
(like here) to distinguish the responsible filter chain.
http.securityMatcher( request ->
Optional.ofNullable(
request.getHeader(HttpHeaders.AUTHORIZATION))
.map(h -> h.startsWith("Bearer ")
).orElse(false)
);
also got rid of the bean with filter and swapped the order. The complete security config is:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Autowired
private AuthenticationService authService;
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher( request ->
Optional.ofNullable(
request.getHeader(HttpHeaders.AUTHORIZATION))
.map(h -> h.startsWith("Bearer ")
).orElse(false)
);
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/api/**").hasAuthority(Role.USER.name())
.anyRequest().authenticated()
)
.addFilterBefore(new TokenAuthenticationFilter(authService), UsernamePasswordAuthenticationFilter.class)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.httpBasic(Customizer.withDefaults())
;
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/manager/**").hasAuthority(Role.MANAGER.name())
.requestMatchers("/viewer/**").hasAuthority(Role.OPERATOR.name())
.requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form.successHandler(new CustomAuthenticationSuccessHandler()))
.logout(LogoutConfigurer::permitAll)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
}