My fork of airsonic with experimental fixes and improvements. See branch "custom"
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
airsonic-custom/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java

204 lines
9.4 KiB

package org.airsonic.player.security;
import org.airsonic.player.service.JWTSecurityService;
import org.airsonic.player.service.SecurityService;
import org.airsonic.player.service.SettingsService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.security.SecureRandom;
@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class GlobalSecurityConfig extends GlobalAuthenticationConfigurerAdapter {
private static final Logger LOG = LoggerFactory.getLogger(GlobalSecurityConfig.class);
static final String FAILURE_URL = "/login?error=1";
static final String DEVELOPMENT_REMEMBER_ME_KEY = "airsonic";
@Autowired
private SecurityService securityService;
@Autowired
private CsrfSecurityRequestMatcher csrfSecurityRequestMatcher;
@Autowired
SettingsService settingsService;
@Autowired
CustomUserDetailsContextMapper customUserDetailsContextMapper;
@Autowired
ApplicationEventPublisher eventPublisher;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
if (settingsService.isLdapEnabled()) {
auth.ldapAuthentication()
.contextSource()
.managerDn(settingsService.getLdapManagerDn())
.managerPassword(settingsService.getLdapManagerPassword())
.url(settingsService.getLdapUrl())
.and()
.userSearchFilter(settingsService.getLdapSearchFilter())
.userDetailsContextMapper(customUserDetailsContextMapper);
}
auth.userDetailsService(securityService);
String jwtKey = settingsService.getJWTKey();
if (StringUtils.isBlank(jwtKey)) {
LOG.warn("Generating new jwt key");
jwtKey = JWTSecurityService.generateKey();
settingsService.setJWTKey(jwtKey);
settingsService.save();
}
auth.authenticationProvider(new JWTAuthenticationProvider(jwtKey));
}
private static String generateRememberMeKey() {
byte[] array = new byte[32];
new SecureRandom().nextBytes(array);
return new String(array);
}
@Configuration
@Order(1)
public class ExtSecurityConfiguration extends WebSecurityConfigurerAdapter {
public ExtSecurityConfiguration() {
super(true);
}
@Bean(name = "jwtAuthenticationFilter")
public JWTRequestParameterProcessingFilter jwtAuthFilter() throws Exception {
return new JWTRequestParameterProcessingFilter(authenticationManager(), FAILURE_URL);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http = http.addFilter(new WebAsyncManagerIntegrationFilter());
http = http.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
http
.antMatcher("/ext/**")
.csrf().requireCsrfProtectionMatcher(csrfSecurityRequestMatcher).and()
.headers().frameOptions().sameOrigin().and()
.authorizeRequests()
.antMatchers("/ext/stream/**", "/ext/coverArt*", "/ext/share/**", "/ext/hls/**")
.hasAnyRole("TEMP", "USER").and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi();
}
}
@Configuration
@Order(2)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
RESTRequestParameterProcessingFilter restAuthenticationFilter = new RESTRequestParameterProcessingFilter();
restAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
restAuthenticationFilter.setSecurityService(securityService);
restAuthenticationFilter.setEventPublisher(eventPublisher);
http = http.addFilterBefore(restAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// Try to load the 'remember me' key.
//
// Note that using a fixed key compromises security as perfect
// forward secrecy is not guaranteed anymore.
//
// An external entity can then re-use our authentication cookies before
// the expiration time, or even, given enough time, recover the password
// from the MD5 hash.
//
// See: https://docs.spring.io/spring-security/site/docs/3.0.x/reference/remember-me.html
String rememberMeKey = settingsService.getRememberMeKey();
boolean development = SettingsService.isDevelopmentMode();
if (StringUtils.isBlank(rememberMeKey) && !development) {
// ...if it is empty, generate a random key on startup (default).
LOG.debug("Generating a new ephemeral 'remember me' key in a secure way.");
rememberMeKey = generateRememberMeKey();
} else if (StringUtils.isBlank(rememberMeKey) && development) {
// ...if we are in development mode, we can use a fixed key.
LOG.warn("Using a fixed 'remember me' key because we're in development mode, this is INSECURE.");
rememberMeKey = DEVELOPMENT_REMEMBER_ME_KEY;
} else {
// ...otherwise, use the custom key directly.
LOG.info("Using a fixed 'remember me' key from system properties, this is insecure.");
}
http
.csrf()
.requireCsrfProtectionMatcher(csrfSecurityRequestMatcher)
.and().headers()
.frameOptions()
.sameOrigin()
.and().authorizeRequests()
.antMatchers("/recover*", "/accessDenied*",
"/style/**", "/icons/**", "/flash/**", "/script/**",
"/sonos/**", "/login", "/error")
.permitAll()
.antMatchers("/personalSettings*", "/passwordSettings*",
"/playerSettings*", "/shareSettings*", "/passwordSettings*")
.hasRole("SETTINGS")
.antMatchers("/generalSettings*", "/advancedSettings*", "/userSettings*",
"/musicFolderSettings*", "/databaseSettings*", "/transcodeSettings*", "/rest/startScan*")
.hasRole("ADMIN")
.antMatchers("/deletePlaylist*", "/savePlaylist*")
.hasRole("PLAYLIST")
.antMatchers("/download*")
.hasRole("DOWNLOAD")
.antMatchers("/upload*")
.hasRole("UPLOAD")
.antMatchers("/createShare*")
.hasRole("SHARE")
.antMatchers("/changeCoverArt*", "/editTags*")
.hasRole("COVERART")
.antMatchers("/setMusicFileInfo*")
.hasRole("COMMENT")
.antMatchers("/podcastReceiverAdmin*")
.hasRole("PODCAST")
.antMatchers("/**")
.hasRole("USER")
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/index", true)
.failureUrl(FAILURE_URL)
.usernameParameter("j_username")
.passwordParameter("j_password")
// see http://docs.spring.io/spring-security/site/docs/3.2.4.RELEASE/reference/htmlsingle/#csrf-logout
.and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")).logoutSuccessUrl(
"/login?logout")
.and().rememberMe().key(rememberMeKey);
}
}
}