parent
f13ed83d77
commit
f7bd43136b
@ -1,9 +1,25 @@ |
||||
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 |
||||
* in the MultiController. |
||||
*/ |
||||
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