parent
f13ed83d77
commit
f7bd43136b
@ -1,9 +1,25 @@ |
|||||||
package org.libresonic.player.controller; |
package org.libresonic.player.controller; |
||||||
|
|
||||||
|
import org.springframework.util.AntPathMatcher; |
||||||
|
import org.springframework.web.servlet.HandlerMapping; |
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
|
||||||
/** |
/** |
||||||
* This class has been created to refactor code previously present |
* This class has been created to refactor code previously present |
||||||
* in the MultiController. |
* in the MultiController. |
||||||
*/ |
*/ |
||||||
public class ControllerUtils { |
public class ControllerUtils { |
||||||
|
|
||||||
|
public static String extractMatched(final HttpServletRequest request){ |
||||||
|
|
||||||
|
String path = (String) request.getAttribute( |
||||||
|
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); |
||||||
|
String bestMatchPattern = (String ) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); |
||||||
|
|
||||||
|
AntPathMatcher apm = new AntPathMatcher(); |
||||||
|
|
||||||
|
return apm.extractPathWithinPattern(bestMatchPattern, path); |
||||||
|
|
||||||
|
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,324 @@ |
|||||||
|
package org.libresonic.player.domain; |
||||||
|
|
||||||
|
import com.google.common.base.Function; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.util.Date; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
public class MediaFileWithUrlInfo { |
||||||
|
|
||||||
|
private final MediaFile file; |
||||||
|
private final String coverArtUrl; |
||||||
|
private final String streamUrl; |
||||||
|
|
||||||
|
public MediaFileWithUrlInfo(MediaFile file, String coverArtUrl, String streamUrl) { |
||||||
|
this.file = file; |
||||||
|
this.coverArtUrl = coverArtUrl; |
||||||
|
this.streamUrl = streamUrl; |
||||||
|
} |
||||||
|
|
||||||
|
public String getStreamUrl() { |
||||||
|
return streamUrl; |
||||||
|
} |
||||||
|
|
||||||
|
public String getCoverArtUrl() { |
||||||
|
return coverArtUrl; |
||||||
|
} |
||||||
|
|
||||||
|
public int getId() { |
||||||
|
return file.getId(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setId(int id) { |
||||||
|
file.setId(id); |
||||||
|
} |
||||||
|
|
||||||
|
public String getPath() { |
||||||
|
return file.getPath(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setPath(String path) { |
||||||
|
file.setPath(path); |
||||||
|
} |
||||||
|
|
||||||
|
public String getFolder() { |
||||||
|
return file.getFolder(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setFolder(String folder) { |
||||||
|
file.setFolder(folder); |
||||||
|
} |
||||||
|
|
||||||
|
public File getFile() { |
||||||
|
return file.getFile(); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean exists() { |
||||||
|
return file.exists(); |
||||||
|
} |
||||||
|
|
||||||
|
public MediaFile.MediaType getMediaType() { |
||||||
|
return file.getMediaType(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setMediaType(MediaFile.MediaType mediaType) { |
||||||
|
file.setMediaType(mediaType); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isVideo() { |
||||||
|
return file.isVideo(); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isAudio() { |
||||||
|
return file.isAudio(); |
||||||
|
} |
||||||
|
|
||||||
|
public String getFormat() { |
||||||
|
return file.getFormat(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setFormat(String format) { |
||||||
|
file.setFormat(format); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isDirectory() { |
||||||
|
return file.isDirectory(); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isFile() { |
||||||
|
return file.isFile(); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isAlbum() { |
||||||
|
return file.isAlbum(); |
||||||
|
} |
||||||
|
|
||||||
|
public String getTitle() { |
||||||
|
return file.getTitle(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setTitle(String title) { |
||||||
|
file.setTitle(title); |
||||||
|
} |
||||||
|
|
||||||
|
public String getAlbumName() { |
||||||
|
return file.getAlbumName(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setAlbumName(String album) { |
||||||
|
file.setAlbumName(album); |
||||||
|
} |
||||||
|
|
||||||
|
public String getArtist() { |
||||||
|
return file.getArtist(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setArtist(String artist) { |
||||||
|
file.setArtist(artist); |
||||||
|
} |
||||||
|
|
||||||
|
public String getAlbumArtist() { |
||||||
|
return file.getAlbumArtist(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setAlbumArtist(String albumArtist) { |
||||||
|
file.setAlbumArtist(albumArtist); |
||||||
|
} |
||||||
|
|
||||||
|
public String getName() { |
||||||
|
return file.getName(); |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getDiscNumber() { |
||||||
|
return file.getDiscNumber(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setDiscNumber(Integer discNumber) { |
||||||
|
file.setDiscNumber(discNumber); |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getTrackNumber() { |
||||||
|
return file.getTrackNumber(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setTrackNumber(Integer trackNumber) { |
||||||
|
file.setTrackNumber(trackNumber); |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getYear() { |
||||||
|
return file.getYear(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setYear(Integer year) { |
||||||
|
file.setYear(year); |
||||||
|
} |
||||||
|
|
||||||
|
public String getGenre() { |
||||||
|
return file.getGenre(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setGenre(String genre) { |
||||||
|
file.setGenre(genre); |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getBitRate() { |
||||||
|
return file.getBitRate(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setBitRate(Integer bitRate) { |
||||||
|
file.setBitRate(bitRate); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isVariableBitRate() { |
||||||
|
return file.isVariableBitRate(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setVariableBitRate(boolean variableBitRate) { |
||||||
|
file.setVariableBitRate(variableBitRate); |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getDurationSeconds() { |
||||||
|
return file.getDurationSeconds(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setDurationSeconds(Integer durationSeconds) { |
||||||
|
file.setDurationSeconds(durationSeconds); |
||||||
|
} |
||||||
|
|
||||||
|
public String getDurationString() { |
||||||
|
return file.getDurationString(); |
||||||
|
} |
||||||
|
|
||||||
|
public Long getFileSize() { |
||||||
|
return file.getFileSize(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setFileSize(Long fileSize) { |
||||||
|
file.setFileSize(fileSize); |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getWidth() { |
||||||
|
return file.getWidth(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setWidth(Integer width) { |
||||||
|
file.setWidth(width); |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getHeight() { |
||||||
|
return file.getHeight(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setHeight(Integer height) { |
||||||
|
file.setHeight(height); |
||||||
|
} |
||||||
|
|
||||||
|
public String getCoverArtPath() { |
||||||
|
return file.getCoverArtPath(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setCoverArtPath(String coverArtPath) { |
||||||
|
file.setCoverArtPath(coverArtPath); |
||||||
|
} |
||||||
|
|
||||||
|
public String getParentPath() { |
||||||
|
return file.getParentPath(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setParentPath(String parentPath) { |
||||||
|
file.setParentPath(parentPath); |
||||||
|
} |
||||||
|
|
||||||
|
public File getParentFile() { |
||||||
|
return file.getParentFile(); |
||||||
|
} |
||||||
|
|
||||||
|
public int getPlayCount() { |
||||||
|
return file.getPlayCount(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setPlayCount(int playCount) { |
||||||
|
file.setPlayCount(playCount); |
||||||
|
} |
||||||
|
|
||||||
|
public Date getLastPlayed() { |
||||||
|
return file.getLastPlayed(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setLastPlayed(Date lastPlayed) { |
||||||
|
file.setLastPlayed(lastPlayed); |
||||||
|
} |
||||||
|
|
||||||
|
public String getComment() { |
||||||
|
return file.getComment(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setComment(String comment) { |
||||||
|
file.setComment(comment); |
||||||
|
} |
||||||
|
|
||||||
|
public Date getCreated() { |
||||||
|
return file.getCreated(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setCreated(Date created) { |
||||||
|
file.setCreated(created); |
||||||
|
} |
||||||
|
|
||||||
|
public Date getChanged() { |
||||||
|
return file.getChanged(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setChanged(Date changed) { |
||||||
|
file.setChanged(changed); |
||||||
|
} |
||||||
|
|
||||||
|
public Date getLastScanned() { |
||||||
|
return file.getLastScanned(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setLastScanned(Date lastScanned) { |
||||||
|
file.setLastScanned(lastScanned); |
||||||
|
} |
||||||
|
|
||||||
|
public Date getStarredDate() { |
||||||
|
return file.getStarredDate(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setStarredDate(Date starredDate) { |
||||||
|
file.setStarredDate(starredDate); |
||||||
|
} |
||||||
|
|
||||||
|
public Date getChildrenLastUpdated() { |
||||||
|
return file.getChildrenLastUpdated(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setChildrenLastUpdated(Date childrenLastUpdated) { |
||||||
|
file.setChildrenLastUpdated(childrenLastUpdated); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isPresent() { |
||||||
|
return file.isPresent(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setPresent(boolean present) { |
||||||
|
file.setPresent(present); |
||||||
|
} |
||||||
|
|
||||||
|
public int getVersion() { |
||||||
|
return file.getVersion(); |
||||||
|
} |
||||||
|
|
||||||
|
public File getCoverArtFile() { |
||||||
|
return file.getCoverArtFile(); |
||||||
|
} |
||||||
|
|
||||||
|
public static List<Integer> toIdList(List<MediaFile> from) { |
||||||
|
return MediaFile.toIdList(from); |
||||||
|
} |
||||||
|
|
||||||
|
public static Function<MediaFile, Integer> toId() { |
||||||
|
return MediaFile.toId(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,169 @@ |
|||||||
|
package org.libresonic.player.security; |
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils; |
||||||
|
import org.libresonic.player.service.JWTSecurityService; |
||||||
|
import org.libresonic.player.service.SecurityService; |
||||||
|
import org.libresonic.player.service.SettingsService; |
||||||
|
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.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; |
||||||
|
|
||||||
|
@Configuration |
||||||
|
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) |
||||||
|
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) |
||||||
|
public class GlobalSecurityConfig extends GlobalAuthenticationConfigurerAdapter { |
||||||
|
|
||||||
|
private static Logger logger = LoggerFactory.getLogger(GlobalSecurityConfig.class); |
||||||
|
|
||||||
|
static final String FAILURE_URL = "/login?error=1"; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private SecurityService securityService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private CsrfSecurityRequestMatcher csrfSecurityRequestMatcher; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
LoginFailureLogger loginFailureLogger; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
SettingsService settingsService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
LibresonicUserDetailsContextMapper libresonicUserDetailsContextMapper; |
||||||
|
|
||||||
|
|
||||||
|
@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(libresonicUserDetailsContextMapper); |
||||||
|
} |
||||||
|
auth.userDetailsService(securityService); |
||||||
|
String jwtKey = settingsService.getJWTKey(); |
||||||
|
if(StringUtils.isBlank(jwtKey)) { |
||||||
|
logger.warn("Generating new jwt key"); |
||||||
|
jwtKey = JWTSecurityService.generateKey(); |
||||||
|
settingsService.setJWTKey(jwtKey); |
||||||
|
settingsService.save(); |
||||||
|
} |
||||||
|
auth.authenticationProvider(new JWTAuthenticationProvider(jwtKey)); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@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.view", "/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.setLoginFailureLogger(loginFailureLogger); |
||||||
|
http = http.addFilterBefore(restAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); |
||||||
|
|
||||||
|
http |
||||||
|
.csrf() |
||||||
|
.requireCsrfProtectionMatcher(csrfSecurityRequestMatcher) |
||||||
|
.and().headers() |
||||||
|
.frameOptions() |
||||||
|
.sameOrigin() |
||||||
|
.and().authorizeRequests() |
||||||
|
.antMatchers("recover.view", "accessDenied.view", |
||||||
|
"/style/**", "/icons/**", "/flash/**", "/script/**", |
||||||
|
"/sonos/**", "/crossdomain.xml", "/login", "/error") |
||||||
|
.permitAll() |
||||||
|
.antMatchers("/personalSettings.view", "/passwordSettings.view", |
||||||
|
"/playerSettings.view", "/shareSettings.view", "/passwordSettings.view") |
||||||
|
.hasRole("SETTINGS") |
||||||
|
.antMatchers("/generalSettings.view", "/advancedSettings.view", "/userSettings.view", |
||||||
|
"/musicFolderSettings.view", "/databaseSettings.view") |
||||||
|
.hasRole("ADMIN") |
||||||
|
.antMatchers("/deletePlaylist.view", "/savePlaylist.view") |
||||||
|
.hasRole("PLAYLIST") |
||||||
|
.antMatchers("/download.view") |
||||||
|
.hasRole("DOWNLOAD") |
||||||
|
.antMatchers("/upload.view") |
||||||
|
.hasRole("UPLOAD") |
||||||
|
.antMatchers("/createShare.view") |
||||||
|
.hasRole("SHARE") |
||||||
|
.antMatchers("/changeCoverArt.view", "/editTags.view") |
||||||
|
.hasRole("COVERART") |
||||||
|
.antMatchers("/setMusicFileInfo.view") |
||||||
|
.hasRole("COMMENT") |
||||||
|
.antMatchers("/podcastReceiverAdmin.view") |
||||||
|
.hasRole("PODCAST") |
||||||
|
.antMatchers("/**") |
||||||
|
.hasRole("USER") |
||||||
|
.anyRequest().authenticated() |
||||||
|
.and().formLogin() |
||||||
|
.loginPage("/login") |
||||||
|
.permitAll() |
||||||
|
.defaultSuccessUrl("/index.view", 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("libresonic"); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,100 @@ |
|||||||
|
package org.libresonic.player.security; |
||||||
|
|
||||||
|
import com.auth0.jwt.interfaces.Claim; |
||||||
|
import com.auth0.jwt.interfaces.DecodedJWT; |
||||||
|
import com.google.common.collect.MapDifference; |
||||||
|
import com.google.common.collect.Maps; |
||||||
|
import org.apache.commons.lang3.StringUtils; |
||||||
|
import org.libresonic.player.service.JWTSecurityService; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
import org.springframework.security.authentication.AuthenticationProvider; |
||||||
|
import org.springframework.security.authentication.InsufficientAuthenticationException; |
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.core.AuthenticationException; |
||||||
|
import org.springframework.security.core.GrantedAuthority; |
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
||||||
|
import org.springframework.web.util.UriComponents; |
||||||
|
import org.springframework.web.util.UriComponentsBuilder; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Objects; |
||||||
|
|
||||||
|
public class JWTAuthenticationProvider implements AuthenticationProvider { |
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationProvider.class); |
||||||
|
|
||||||
|
private final String jwtKey; |
||||||
|
|
||||||
|
public JWTAuthenticationProvider(String jwtSignAndVerifyKey) { |
||||||
|
this.jwtKey = jwtSignAndVerifyKey; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Authentication authenticate(Authentication auth) throws AuthenticationException { |
||||||
|
JWTAuthenticationToken authentication = (JWTAuthenticationToken) auth; |
||||||
|
if(authentication.getCredentials() == null || !(authentication.getCredentials() instanceof String)) { |
||||||
|
logger.error("Credentials not present"); |
||||||
|
return null; |
||||||
|
} |
||||||
|
String rawToken = (String) auth.getCredentials(); |
||||||
|
DecodedJWT token = JWTSecurityService.verify(jwtKey, rawToken); |
||||||
|
Claim path = token.getClaim(JWTSecurityService.CLAIM_PATH); |
||||||
|
authentication.setAuthenticated(true); |
||||||
|
|
||||||
|
// TODO:AD This is super unfortunate, but not sure there is a better way when using JSP
|
||||||
|
if(StringUtils.contains(authentication.getRequestedPath(), "/WEB-INF/jsp/")) { |
||||||
|
logger.warn("BYPASSING AUTH FOR WEB-INF page"); |
||||||
|
} else |
||||||
|
|
||||||
|
if(!roughlyEqual(path.asString(), authentication.getRequestedPath())) { |
||||||
|
throw new InsufficientAuthenticationException("Credentials not valid for path " + authentication |
||||||
|
.getRequestedPath() + ". They are valid for " + path.asString()); |
||||||
|
} |
||||||
|
|
||||||
|
List<GrantedAuthority> authorities = new ArrayList<>(); |
||||||
|
authorities.add(new SimpleGrantedAuthority("IS_AUTHENTICATED_FULLY")); |
||||||
|
authorities.add(new SimpleGrantedAuthority("ROLE_TEMP")); |
||||||
|
return new JWTAuthenticationToken(authorities, rawToken, authentication.getRequestedPath()); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean roughlyEqual(String expectedRaw, String requestedPathRaw) { |
||||||
|
logger.debug("Comparing expected [{}] vs requested [{}]", expectedRaw, requestedPathRaw); |
||||||
|
if(StringUtils.isEmpty(expectedRaw)) { |
||||||
|
logger.debug("False: empty expected"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
try { |
||||||
|
UriComponents expected = UriComponentsBuilder.fromUriString(expectedRaw).build(); |
||||||
|
UriComponents requested = UriComponentsBuilder.fromUriString(requestedPathRaw).build(); |
||||||
|
|
||||||
|
if(!Objects.equals(expected.getPath(), requested.getPath())) { |
||||||
|
logger.debug("False: expected path [{}] does not match requested path [{}]", |
||||||
|
expected.getPath(), requested.getPath()); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
MapDifference<String, List<String>> difference = Maps.difference(expected.getQueryParams(), |
||||||
|
requested.getQueryParams()); |
||||||
|
|
||||||
|
if(difference.entriesDiffering().size() != 0 || |
||||||
|
difference.entriesOnlyOnLeft().size() != 0 || |
||||||
|
difference.entriesOnlyOnRight().size() != 1 || |
||||||
|
difference.entriesOnlyOnRight().get(JWTSecurityService.JWT_PARAM_NAME) == null) { |
||||||
|
logger.debug("False: expected query params [{}] do not match requested query params [{}]", expected.getQueryParams(), requested.getQueryParams()); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
} catch(Exception e) { |
||||||
|
logger.warn("Exception encountered while comparing paths", e); |
||||||
|
return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supports(Class<?> authentication) { |
||||||
|
return JWTAuthenticationToken.class.isAssignableFrom(authentication); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
package org.libresonic.player.security; |
||||||
|
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||||
|
import org.springframework.security.core.GrantedAuthority; |
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
public class JWTAuthenticationToken extends AbstractAuthenticationToken { |
||||||
|
private final String token; |
||||||
|
private String requestedPath; |
||||||
|
|
||||||
|
public JWTAuthenticationToken(Collection<? extends GrantedAuthority> authorities, String token, String requestedPath) { |
||||||
|
super(authorities); |
||||||
|
this.token = token; |
||||||
|
this.requestedPath = requestedPath; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object getCredentials() { |
||||||
|
return token; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object getPrincipal() { |
||||||
|
return "GERNIC_JWT_PRINICPLE"; |
||||||
|
} |
||||||
|
|
||||||
|
public String getRequestedPath() { |
||||||
|
return requestedPath; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,123 @@ |
|||||||
|
package org.libresonic.player.security; |
||||||
|
|
||||||
|
import org.libresonic.player.service.JWTSecurityService; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
import org.springframework.security.authentication.AuthenticationManager; |
||||||
|
import org.springframework.security.authentication.AuthenticationServiceException; |
||||||
|
import org.springframework.security.authentication.InternalAuthenticationServiceException; |
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.core.AuthenticationException; |
||||||
|
import org.springframework.security.core.authority.AuthorityUtils; |
||||||
|
import org.springframework.security.core.context.SecurityContextHolder; |
||||||
|
import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
||||||
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
import javax.servlet.*; |
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import javax.servlet.http.HttpServletResponse; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
public class JWTRequestParameterProcessingFilter implements Filter { |
||||||
|
private static final Logger logger = LoggerFactory.getLogger(JWTRequestParameterProcessingFilter.class); |
||||||
|
private final AuthenticationManager authenticationManager; |
||||||
|
private final AuthenticationFailureHandler failureHandler; |
||||||
|
|
||||||
|
protected JWTRequestParameterProcessingFilter(AuthenticationManager authenticationManager, String failureUrl) { |
||||||
|
this.authenticationManager = authenticationManager; failureHandler = new SimpleUrlAuthenticationFailureHandler(failureUrl); |
||||||
|
} |
||||||
|
|
||||||
|
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { |
||||||
|
Optional<JWTAuthenticationToken> token = findToken(request); |
||||||
|
if(token.isPresent()) { |
||||||
|
return authenticationManager.authenticate(token.get()); |
||||||
|
} |
||||||
|
throw new AuthenticationServiceException("Invalid auth method"); |
||||||
|
} |
||||||
|
|
||||||
|
private static Optional<JWTAuthenticationToken> findToken(HttpServletRequest request) { |
||||||
|
String token = request.getParameter(JWTSecurityService.JWT_PARAM_NAME); |
||||||
|
if(!StringUtils.isEmpty(token)) { |
||||||
|
return Optional.of(new JWTAuthenticationToken(AuthorityUtils.NO_AUTHORITIES, token, request.getRequestURI() + "?" + request.getQueryString())); |
||||||
|
} |
||||||
|
return Optional.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void init(FilterConfig filterConfig) throws ServletException { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, |
||||||
|
ServletException { |
||||||
|
HttpServletRequest request = (HttpServletRequest) req; |
||||||
|
HttpServletResponse response = (HttpServletResponse) resp; |
||||||
|
|
||||||
|
if(!findToken(request).isPresent()) { |
||||||
|
chain.doFilter(req, resp); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) { |
||||||
|
logger.debug("Request is to process authentication"); |
||||||
|
} |
||||||
|
|
||||||
|
Authentication authResult; |
||||||
|
|
||||||
|
try { |
||||||
|
authResult = attemptAuthentication(request, response); |
||||||
|
if (authResult == null) { |
||||||
|
// return immediately as subclass has indicated that it hasn't completed
|
||||||
|
// authentication
|
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
catch (InternalAuthenticationServiceException failed) { |
||||||
|
logger.error( |
||||||
|
"An internal error occurred while trying to authenticate the user.", |
||||||
|
failed); |
||||||
|
unsuccessfulAuthentication(request, response, failed); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
catch (AuthenticationException failed) { |
||||||
|
// Authentication failed
|
||||||
|
unsuccessfulAuthentication(request, response, failed); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) { |
||||||
|
logger.debug("Authentication success. Updating SecurityContextHolder to contain: " |
||||||
|
+ authResult); |
||||||
|
} |
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authResult); |
||||||
|
|
||||||
|
chain.doFilter(request, response); |
||||||
|
} |
||||||
|
|
||||||
|
protected void unsuccessfulAuthentication(HttpServletRequest request, |
||||||
|
HttpServletResponse response, AuthenticationException failed) |
||||||
|
throws IOException, ServletException { |
||||||
|
SecurityContextHolder.clearContext(); |
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) { |
||||||
|
logger.debug("Authentication request failed: " + failed.toString(), failed); |
||||||
|
logger.debug("Updated SecurityContextHolder to contain null Authentication"); |
||||||
|
logger.debug("Delegating to authentication failure handler " + failureHandler); |
||||||
|
} |
||||||
|
|
||||||
|
failureHandler.onAuthenticationFailure(request, response, failed); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void destroy() { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,112 +0,0 @@ |
|||||||
package org.libresonic.player.security; |
|
||||||
|
|
||||||
import org.libresonic.player.service.SecurityService; |
|
||||||
import org.libresonic.player.service.SettingsService; |
|
||||||
import org.springframework.beans.factory.annotation.Autowired; |
|
||||||
import org.springframework.context.annotation.Bean; |
|
||||||
import org.springframework.context.annotation.Configuration; |
|
||||||
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.configuration.WebSecurityConfigurerAdapter; |
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSecurity |
|
||||||
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { |
|
||||||
|
|
||||||
@Autowired |
|
||||||
private SecurityService securityService; |
|
||||||
|
|
||||||
@Autowired |
|
||||||
private CsrfSecurityRequestMatcher csrfSecurityRequestMatcher; |
|
||||||
|
|
||||||
@Autowired |
|
||||||
LoginFailureLogger loginFailureLogger; |
|
||||||
|
|
||||||
@Autowired |
|
||||||
SettingsService settingsService; |
|
||||||
|
|
||||||
@Autowired |
|
||||||
LibresonicUserDetailsContextMapper libresonicUserDetailsContextMapper; |
|
||||||
|
|
||||||
@Override |
|
||||||
@Bean(name = "authenticationManager") |
|
||||||
public AuthenticationManager authenticationManagerBean() throws Exception { |
|
||||||
return super.authenticationManagerBean(); |
|
||||||
} |
|
||||||
|
|
||||||
@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(libresonicUserDetailsContextMapper); |
|
||||||
} |
|
||||||
auth.userDetailsService(securityService); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
@Override |
|
||||||
protected void configure(HttpSecurity http) throws Exception { |
|
||||||
|
|
||||||
RESTRequestParameterProcessingFilter restAuthenticationFilter = new RESTRequestParameterProcessingFilter(); |
|
||||||
restAuthenticationFilter.setAuthenticationManager((AuthenticationManager) getApplicationContext().getBean("authenticationManager")); |
|
||||||
restAuthenticationFilter.setSecurityService(securityService); |
|
||||||
restAuthenticationFilter.setLoginFailureLogger(loginFailureLogger); |
|
||||||
http = http.addFilterBefore(restAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); |
|
||||||
|
|
||||||
http |
|
||||||
.csrf() |
|
||||||
.requireCsrfProtectionMatcher(csrfSecurityRequestMatcher) |
|
||||||
.and().headers() |
|
||||||
.frameOptions() |
|
||||||
.sameOrigin() |
|
||||||
.and().authorizeRequests() |
|
||||||
.antMatchers("recover.view", "accessDenied.view", |
|
||||||
"coverArt.view", "/hls/**", "/stream/**", "/ws/**", |
|
||||||
"/share/**", "/style/**", "/icons/**", |
|
||||||
"/flash/**", "/script/**", "/sonos/**", "/crossdomain.xml", "/login") |
|
||||||
.permitAll() |
|
||||||
.antMatchers("/personalSettings.view", "/passwordSettings.view", |
|
||||||
"/playerSettings.view", "/shareSettings.view","/passwordSettings.view") |
|
||||||
.hasRole("SETTINGS") |
|
||||||
.antMatchers("/generalSettings.view","/advancedSettings.view","/userSettings.view", |
|
||||||
"/musicFolderSettings.view", "/databaseSettings.view") |
|
||||||
.hasRole("ADMIN") |
|
||||||
.antMatchers("/deletePlaylist.view","/savePlaylist.view") |
|
||||||
.hasRole("PLAYLIST") |
|
||||||
.antMatchers("/download.view") |
|
||||||
.hasRole("DOWNLOAD") |
|
||||||
.antMatchers("/upload.view") |
|
||||||
.hasRole("UPLOAD") |
|
||||||
.antMatchers("/createShare.view") |
|
||||||
.hasRole("SHARE") |
|
||||||
.antMatchers("/changeCoverArt.view","/editTags.view") |
|
||||||
.hasRole("COVERART") |
|
||||||
.antMatchers("/setMusicFileInfo.view") |
|
||||||
.hasRole("COMMENT") |
|
||||||
.antMatchers("/podcastReceiverAdmin.view") |
|
||||||
.hasRole("PODCAST") |
|
||||||
.antMatchers("/**") |
|
||||||
.hasRole("USER") |
|
||||||
.anyRequest().authenticated() |
|
||||||
.and().formLogin() |
|
||||||
.loginPage("/login") |
|
||||||
.permitAll() |
|
||||||
.defaultSuccessUrl("/index.view", true) |
|
||||||
.failureUrl("/login?error=1") |
|
||||||
.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("libresonic"); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,80 @@ |
|||||||
|
package org.libresonic.player.service; |
||||||
|
|
||||||
|
import com.auth0.jwt.JWT; |
||||||
|
import com.auth0.jwt.JWTVerifier; |
||||||
|
import com.auth0.jwt.algorithms.Algorithm; |
||||||
|
import com.auth0.jwt.interfaces.DecodedJWT; |
||||||
|
import org.apache.commons.lang3.StringUtils; |
||||||
|
import org.apache.commons.lang3.time.DateUtils; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
import org.springframework.web.util.UriComponents; |
||||||
|
import org.springframework.web.util.UriComponentsBuilder; |
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException; |
||||||
|
import java.math.BigInteger; |
||||||
|
import java.security.SecureRandom; |
||||||
|
import java.util.Date; |
||||||
|
|
||||||
|
public class JWTSecurityService { |
||||||
|
private static final Logger logger = LoggerFactory.getLogger(JWTSecurityService.class); |
||||||
|
|
||||||
|
public static final String JWT_PARAM_NAME = "jwt"; |
||||||
|
public static final String CLAIM_PATH = "path"; |
||||||
|
// TODO make this configurable
|
||||||
|
public static final int DEFAULT_DAYS_VALID_FOR = 7; |
||||||
|
private static SecureRandom secureRandom = new SecureRandom(); |
||||||
|
|
||||||
|
private final SettingsService settingsService; |
||||||
|
|
||||||
|
public JWTSecurityService(SettingsService settingsService) { |
||||||
|
this.settingsService = settingsService; |
||||||
|
} |
||||||
|
|
||||||
|
public static String generateKey() { |
||||||
|
BigInteger randomInt = new BigInteger(130, secureRandom); |
||||||
|
return randomInt.toString(32); |
||||||
|
} |
||||||
|
|
||||||
|
public static Algorithm getAlgorithm(String jwtKey) { |
||||||
|
try { |
||||||
|
return Algorithm.HMAC256(jwtKey); |
||||||
|
} catch (UnsupportedEncodingException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static String createToken(String jwtKey, String path, Date expireDate) { |
||||||
|
UriComponents components = UriComponentsBuilder.fromUriString(path).build(); |
||||||
|
String query = components.getQuery(); |
||||||
|
String claim = components.getPath() + (!StringUtils.isBlank(query) ? "?" + components.getQuery() : ""); |
||||||
|
logger.debug("Creating token with claim " + claim); |
||||||
|
return JWT.create() |
||||||
|
.withClaim(CLAIM_PATH, claim) |
||||||
|
.withExpiresAt(expireDate) |
||||||
|
.sign(getAlgorithm(jwtKey)); |
||||||
|
} |
||||||
|
|
||||||
|
public UriComponentsBuilder addJWTToken(UriComponentsBuilder builder) { |
||||||
|
return addJWTToken(builder, DateUtils.addDays(new Date(), DEFAULT_DAYS_VALID_FOR)); |
||||||
|
} |
||||||
|
|
||||||
|
public UriComponentsBuilder addJWTToken(UriComponentsBuilder builder, Date expires) { |
||||||
|
String token = JWTSecurityService.createToken( |
||||||
|
settingsService.getJWTKey(), |
||||||
|
builder.toUriString(), |
||||||
|
expires); |
||||||
|
builder.queryParam(JWTSecurityService.JWT_PARAM_NAME, token); |
||||||
|
return builder; |
||||||
|
} |
||||||
|
|
||||||
|
public static DecodedJWT verify(String jwtKey, String token) { |
||||||
|
Algorithm algorithm = JWTSecurityService.getAlgorithm(jwtKey); |
||||||
|
JWTVerifier verifier = JWT.require(algorithm).build(); |
||||||
|
return verifier.verify(token); |
||||||
|
} |
||||||
|
|
||||||
|
public DecodedJWT verify(String credentials) { |
||||||
|
return verify(settingsService.getJWTKey(), credentials); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
package org.libresonic.player.service; |
||||||
|
|
||||||
|
import com.auth0.jwt.JWT; |
||||||
|
import com.auth0.jwt.JWTVerifier; |
||||||
|
import com.auth0.jwt.algorithms.Algorithm; |
||||||
|
import com.auth0.jwt.interfaces.Claim; |
||||||
|
import com.auth0.jwt.interfaces.DecodedJWT; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.junit.runners.Parameterized; |
||||||
|
import org.springframework.web.util.UriComponentsBuilder; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
|
||||||
|
@RunWith(Parameterized.class) |
||||||
|
public class JWTSecurityServiceTest { |
||||||
|
|
||||||
|
private final String key = "someKey"; |
||||||
|
private final JWTSecurityService service = new JWTSecurityService(settingsWithKey(key)); |
||||||
|
private final String uriString; |
||||||
|
private final Algorithm algorithm = JWTSecurityService.getAlgorithm(key); |
||||||
|
private final JWTVerifier verifier = JWT.require(algorithm).build(); |
||||||
|
private final String expectedClaimString; |
||||||
|
|
||||||
|
@Parameterized.Parameters |
||||||
|
public static Collection<Object[]> data() { |
||||||
|
return Arrays.asList(new Object[][] { |
||||||
|
{ "http://localhost:8080/libresonic/stream?id=4", "/libresonic/stream?id=4" }, |
||||||
|
{ "/libresonic/stream?id=4", "/libresonic/stream?id=4" }, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public JWTSecurityServiceTest(String uriString, String expectedClaimString) { |
||||||
|
this.uriString = uriString; |
||||||
|
this.expectedClaimString = expectedClaimString; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Test |
||||||
|
public void addJWTToken() throws Exception { |
||||||
|
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriString); |
||||||
|
String actualUri = service.addJWTToken(builder).build().toUriString(); |
||||||
|
String jwtToken = UriComponentsBuilder.fromUriString(actualUri).build().getQueryParams().getFirst( |
||||||
|
JWTSecurityService.JWT_PARAM_NAME); |
||||||
|
DecodedJWT verify = verifier.verify(jwtToken); |
||||||
|
Claim claim = verify.getClaim(JWTSecurityService.CLAIM_PATH); |
||||||
|
assertEquals(expectedClaimString, claim.asString()); |
||||||
|
} |
||||||
|
|
||||||
|
private SettingsService settingsWithKey(String jwtKey) { |
||||||
|
return new SettingsService() { |
||||||
|
@Override |
||||||
|
public String getJWTKey() { |
||||||
|
return jwtKey; |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue