Skip to content

Commit

Permalink
Fix SecurityConfig
Browse files Browse the repository at this point in the history
  • Loading branch information
theotherp committed Apr 6, 2024
1 parent 802979c commit 2ddaeec
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down
171 changes: 71 additions & 100 deletions core/src/main/java/org/nzbhydra/auth/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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();
}


}
1 change: 0 additions & 1 deletion core/src/main/resources/config/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 2ddaeec

Please sign in to comment.