From 2ddaeec5bf540d8c98033f765926f8032091ff31 Mon Sep 17 00:00:00 2001 From: TheOtherP Date: Sat, 6 Apr 2024 12:58:54 +0200 Subject: [PATCH] Fix SecurityConfig --- .../auth/HeaderAuthenticationFilter.java | 13 +- .../org/nzbhydra/auth/SecurityConfig.java | 171 ++++++++---------- .../resources/config/application.properties | 1 - 3 files changed, 74 insertions(+), 111 deletions(-) diff --git a/core/src/main/java/org/nzbhydra/auth/HeaderAuthenticationFilter.java b/core/src/main/java/org/nzbhydra/auth/HeaderAuthenticationFilter.java index 4843a49db..37601508c 100644 --- a/core/src/main/java/org/nzbhydra/auth/HeaderAuthenticationFilter.java +++ b/core/src/main/java/org/nzbhydra/auth/HeaderAuthenticationFilter.java @@ -27,19 +27,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.Objects; -public class HeaderAuthenticationFilter extends BasicAuthenticationFilter { +public class HeaderAuthenticationFilter extends OncePerRequestFilter { private static final Logger logger = LoggerFactory.getLogger(HeaderAuthenticationFilter.class); @@ -48,8 +46,7 @@ public class HeaderAuthenticationFilter extends BasicAuthenticationFilter { private final String internalApiKey; - public HeaderAuthenticationFilter(AuthenticationManager authenticationManager, HydraUserDetailsManager userDetailsManager, AuthConfig authConfig) { - super(authenticationManager); + public HeaderAuthenticationFilter(HydraUserDetailsManager userDetailsManager, AuthConfig authConfig) { this.userDetailsManager = userDetailsManager; this.authConfig = authConfig; //Must be provided by wrapper @@ -68,7 +65,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse final AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("key", "internalApi", AuthorityUtils.createAuthorityList("ROLE_ADMIN")); token.setDetails(new HydraWebAuthenticationDetails(request)); SecurityContextHolder.getContext().setAuthentication(token); - onSuccessfulAuthentication(request, response, token); logger.debug("Authorized access to {} via internal API key", request.getRequestURI()); chain.doFilter(request, response); return; @@ -109,7 +105,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); auth.setDetails(new HydraWebAuthenticationDetails(request)); SecurityContextHolder.getContext().setAuthentication(auth); - onSuccessfulAuthentication(request, response, auth); } catch (UsernameNotFoundException e) { handleInvalidAuth(request, response, "Invalid username provided with auth header"); return; @@ -124,8 +119,6 @@ public void loadNewConfig(AuthConfig authConfig) { private void handleInvalidAuth(HttpServletRequest request, HttpServletResponse response, String msg) throws IOException { SecurityContextHolder.clearContext(); - BadCredentialsException badCredentialsException = new BadCredentialsException(msg); - onUnsuccessfulAuthentication(request, response, badCredentialsException); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, msg); logger.warn(msg); } diff --git a/core/src/main/java/org/nzbhydra/auth/SecurityConfig.java b/core/src/main/java/org/nzbhydra/auth/SecurityConfig.java index c893cf6a0..7fc42a791 100644 --- a/core/src/main/java/org/nzbhydra/auth/SecurityConfig.java +++ b/core/src/main/java/org/nzbhydra/auth/SecurityConfig.java @@ -1,6 +1,5 @@ package org.nzbhydra.auth; -import jakarta.servlet.http.HttpServletRequest; import org.nzbhydra.NzbHydra; import org.nzbhydra.config.BaseConfig; import org.nzbhydra.config.ConfigChangedEvent; @@ -13,20 +12,17 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.Order; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.channel.ChannelProcessingFilter; -import org.springframework.security.web.authentication.WebAuthenticationDetails; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.firewall.DefaultHttpFirewall; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.filter.ForwardedHeaderFilter; @Configuration(proxyBeanMethods = false) @@ -52,7 +48,7 @@ public class SecurityConfig { private HeaderAuthenticationFilter headerAuthenticationFilter; @Bean - public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { BaseConfig baseConfig = configProvider.getBaseConfig(); boolean useCsrf = Boolean.parseBoolean(System.getProperty("main.useCsrf")); if (configProvider.getBaseConfig().getMain().isUseCsrf() && useCsrf) { @@ -61,113 +57,97 @@ public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager //https://docs.spring.io/spring-security/reference/5.8/migration/servlet/exploits.html#_i_am_using_angularjs_or_another_javascript_framework CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); requestHandler.setCsrfRequestAttributeName(null); - http.csrf() - .csrfTokenRepository(csrfTokenRepository) - .csrfTokenRequestHandler(requestHandler); + http.csrf(configurer -> configurer.csrfTokenRepository(csrfTokenRepository) + .csrfTokenRequestHandler(requestHandler)); + } else { logger.info("CSRF is disabled"); - http.csrf().disable(); + http.csrf(AbstractHttpConfigurer::disable); } - http.headers() - .httpStrictTransportSecurity().disable() - .frameOptions().disable(); + http + .headers(customizer -> customizer.httpStrictTransportSecurity(HeadersConfigurer.HstsConfig::disable) + .frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)); if (baseConfig.getAuth().getAuthType() == AuthType.BASIC || NzbHydra.isNativeBuild()) { - http = http - .httpBasic() - .authenticationDetailsSource(new WebAuthenticationDetailsSource() { - @Override - public WebAuthenticationDetails buildDetails(HttpServletRequest context) { - return new HydraWebAuthenticationDetails(context); - } - }) - .and(); + http.httpBasic(httpBasic -> { + if (baseConfig.getAuth().getAuthType() == AuthType.BASIC || NzbHydra.isNativeBuild()) { + httpBasic.authenticationDetailsSource(HydraWebAuthenticationDetails::new); + } + }); } else if (baseConfig.getAuth().getAuthType() == AuthType.FORM) { - http = http - .formLogin() - .loginPage("/login") - .loginProcessingUrl("/login") - .defaultSuccessUrl("/") - .permitAll() - .authenticationDetailsSource(new WebAuthenticationDetailsSource() { - @Override - public WebAuthenticationDetails buildDetails(HttpServletRequest context) { - return new HydraWebAuthenticationDetails(context); - } - }) - .and(); + http.formLogin(formLogin -> { + formLogin.loginPage("/login") + .loginProcessingUrl("/login") + .defaultSuccessUrl("/") + .permitAll() + .authenticationDetailsSource(HydraWebAuthenticationDetails::new); + }); } - http.authorizeHttpRequests().requestMatchers("/actuator/health/ping").permitAll(); + http.authorizeHttpRequests(customizer -> customizer.requestMatchers("/actuator/health/ping").permitAll()); if (baseConfig.getAuth().isAuthConfigured() || NzbHydra.isNativeBuild()) { - http = http - .authorizeHttpRequests() - .requestMatchers("/internalapi/") - .authenticated() - .requestMatchers("/websocket/") - .authenticated() - .requestMatchers("/actuator/**") - .hasRole("ADMIN") - .requestMatchers(new AntPathRequestMatcher("/static/**")) - .permitAll() - .anyRequest() -// .authenticated() //Does not include anonymous - .hasAnyRole("ADMIN", "ANONYMOUS", "USER") - .and() - .logout() - .permitAll() - .logoutUrl("/logout") - .logoutSuccessUrl("/") - .deleteCookies("remember-me") - .invalidateHttpSession(true) - .clearAuthentication(true) - .and() - ; - enableAnonymousAccessIfConfigured(http); + http.authorizeHttpRequests(authorizeRequests -> { + authorizeRequests + .requestMatchers("/internalapi/").authenticated() + .requestMatchers("/websocket/").authenticated() + .requestMatchers("/actuator/**").hasRole("ADMIN") + .requestMatchers("/static/**").permitAll() + .anyRequest().hasAnyRole("ADMIN", "ANONYMOUS", "USER"); + + }) + .logout(customizer -> { + if (baseConfig.getAuth().isAuthConfigured() || NzbHydra.isNativeBuild()) { + customizer + .permitAll() + .logoutUrl("/logout") + .logoutSuccessUrl("/") + .deleteCookies("remember-me") + .invalidateHttpSession(true) + .clearAuthentication(true); + } + }); + if (!hydraAnonymousAuthenticationFilter.getAuthorities().isEmpty()) { + http.anonymous(customizer -> { + try { + //Create an anonymous auth filter. If any of the areas are not restricted the anonymous user will get its role + customizer.authenticationFilter(hydraAnonymousAuthenticationFilter); + hydraAnonymousAuthenticationFilter.enable(); + } catch (Exception e) { + logger.error("Unable to configure anonymous access", e); + } + }); + } if (baseConfig.getAuth().isRememberUsers()) { - int rememberMeValidityDays = configProvider.getBaseConfig().getAuth().getRememberMeValidityDays(); - if (rememberMeValidityDays == 0) { - rememberMeValidityDays = 1000; //Can't be disabled, three years should be enough - } - http = http - .rememberMe() - .alwaysRemember(true) - .tokenValiditySeconds(rememberMeValidityDays * SECONDS_PER_DAY) - .userDetailsService(userDetailsService) - .and(); + http.rememberMe(customizer -> { + if (baseConfig.getAuth().isRememberUsers()) { + int rememberMeValidityDays = configProvider.getBaseConfig().getAuth().getRememberMeValidityDays(); + if (rememberMeValidityDays == 0) { + rememberMeValidityDays = 1000; //Can't be disabled, three years should be enough + } + customizer + .alwaysRemember(true) + .tokenValiditySeconds(rememberMeValidityDays * SECONDS_PER_DAY) + .userDetailsService(userDetailsService); + } + }); } - - headerAuthenticationFilter = new HeaderAuthenticationFilter(authenticationManager, hydraUserDetailsManager, configProvider.getBaseConfig().getAuth()); + headerAuthenticationFilter = new HeaderAuthenticationFilter(hydraUserDetailsManager, configProvider.getBaseConfig().getAuth()); http.addFilterAfter(headerAuthenticationFilter, BasicAuthenticationFilter.class); http.addFilterAfter(asyncSupportFilter, BasicAuthenticationFilter.class); - } else { - http.authorizeHttpRequests().anyRequest().permitAll(); + http.authorizeHttpRequests(customizer -> customizer.anyRequest().permitAll()); } - http.exceptionHandling().accessDeniedHandler(authAndAccessEventHandler); + http.exceptionHandling(customizer -> customizer.accessDeniedHandler(authAndAccessEventHandler)) + .userDetailsService(hydraUserDetailsManager); http.addFilterBefore(new ForwardedForRecognizingFilter(), ChannelProcessingFilter.class); //We need to extract the original IP before it's removed and not retrievable anymore by the ForwardedHeaderFilter http.addFilterAfter(new ForwardedHeaderFilter(), ForwardedForRecognizingFilter.class); + return http.build(); } - private void enableAnonymousAccessIfConfigured(HttpSecurity http) { - //Create an anonymous auth filter. If any of the areas are not restricted the anonymous user will get its role - try { - if (!hydraAnonymousAuthenticationFilter.getAuthorities().isEmpty()) { - http.anonymous().authenticationFilter(hydraAnonymousAuthenticationFilter); - - hydraAnonymousAuthenticationFilter.enable(); - - } - - } catch (Exception e) { - logger.error("Unable to configure anonymous access", e); - } - } - @EventListener public void handleNewConfig(ConfigChangedEvent configChangedEvent) { if (headerAuthenticationFilter != null) { @@ -181,14 +161,5 @@ public DefaultHttpFirewall defaultHttpFirewall() { return new DefaultHttpFirewall(); } - @Bean - public AuthenticationManager authManager(HttpSecurity http) - throws Exception { - return http.getSharedObject(AuthenticationManagerBuilder.class) - .userDetailsService(hydraUserDetailsManager) - .and() - .build(); - } - } diff --git a/core/src/main/resources/config/application.properties b/core/src/main/resources/config/application.properties index 807a9241b..e18dc8e50 100644 --- a/core/src/main/resources/config/application.properties +++ b/core/src/main/resources/config/application.properties @@ -74,7 +74,6 @@ server.servlet.encoding.force=true spring.thymeleaf.mode=HTML server.shutdown=graceful server.compression.enabled=true -spring.mvc.throw-exception-if-no-handler-found=true #Performance logging #logging.level.org.thymeleaf=TRACE