Lock Down unsecured urls
Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ import java.util.concurrent.Semaphore;
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/coverArt")
|
||||
@RequestMapping(value = {"/coverArt", "/ext/coverArt"})
|
||||
public class CoverArtController implements LastModified {
|
||||
|
||||
public static final String ALBUM_COVERART_PREFIX = "al-";
|
||||
|
||||
+53
-26
@@ -19,30 +19,28 @@
|
||||
*/
|
||||
package org.libresonic.player.controller;
|
||||
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.libresonic.player.Logger;
|
||||
import org.libresonic.player.domain.MediaFile;
|
||||
import org.libresonic.player.domain.MusicFolder;
|
||||
import org.libresonic.player.domain.Player;
|
||||
import org.libresonic.player.domain.Share;
|
||||
import org.libresonic.player.service.MediaFileService;
|
||||
import org.libresonic.player.service.PlayerService;
|
||||
import org.libresonic.player.service.SettingsService;
|
||||
import org.libresonic.player.service.ShareService;
|
||||
import org.libresonic.player.domain.*;
|
||||
import org.libresonic.player.security.JWTAuthenticationToken;
|
||||
import org.libresonic.player.service.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
import static org.libresonic.player.controller.ExternalPlayerController.SHARE_PATH;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Controller for the page used to play shared music (Twitter, Facebook etc).
|
||||
@@ -50,12 +48,10 @@ import static org.libresonic.player.controller.ExternalPlayerController.SHARE_PA
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping(SHARE_PATH + "**")
|
||||
@RequestMapping(value = {"/ext/share/**"})
|
||||
public class ExternalPlayerController {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ExternalPlayerController.class);
|
||||
|
||||
public static final String SHARE_PATH = "/share/";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExternalPlayerController.class);
|
||||
|
||||
@Autowired
|
||||
private SettingsService settingsService;
|
||||
@@ -65,15 +61,16 @@ public class ExternalPlayerController {
|
||||
private ShareService shareService;
|
||||
@Autowired
|
||||
private MediaFileService mediaFileService;
|
||||
@Autowired
|
||||
private JWTSecurityService jwtSecurityService;
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET)
|
||||
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
|
||||
String pathRelativeToBase = new UrlPathHelper().getPathWithinApplication(request);
|
||||
|
||||
String shareName = StringUtils.removeStart(pathRelativeToBase, SHARE_PATH);
|
||||
String shareName = ControllerUtils.extractMatched(request);
|
||||
LOG.debug("Share name is {}", shareName);
|
||||
|
||||
if(StringUtils.isBlank(shareName)) {
|
||||
LOG.warn("Could not find share with shareName " + shareName);
|
||||
@@ -97,24 +94,32 @@ public class ExternalPlayerController {
|
||||
Player player = playerService.getGuestPlayer(request);
|
||||
|
||||
map.put("share", share);
|
||||
map.put("songs", getSongs(share, player.getUsername()));
|
||||
map.put("player", player.getId());
|
||||
map.put("songs", getSongs(request, share, player));
|
||||
|
||||
return new ModelAndView("externalPlayer", "model", map);
|
||||
}
|
||||
|
||||
private List<MediaFile> getSongs(Share share, String username) throws IOException {
|
||||
List<MediaFile> result = new ArrayList<MediaFile>();
|
||||
private List<MediaFileWithUrlInfo> getSongs(HttpServletRequest request, Share share, Player player) throws IOException {
|
||||
Date expires = null;
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if(authentication instanceof JWTAuthenticationToken) {
|
||||
DecodedJWT token = jwtSecurityService.verify((String) authentication.getCredentials());
|
||||
expires = token.getExpiresAt();
|
||||
}
|
||||
Date finalExpires = expires;
|
||||
|
||||
List<MusicFolder> musicFolders = settingsService.getMusicFoldersForUser(username);
|
||||
List<MediaFileWithUrlInfo> result = new ArrayList<>();
|
||||
|
||||
List<MusicFolder> musicFolders = settingsService.getMusicFoldersForUser(player.getUsername());
|
||||
|
||||
if (share != null) {
|
||||
for (MediaFile file : shareService.getSharedFiles(share.getId(), musicFolders)) {
|
||||
if (file.getFile().exists()) {
|
||||
if (file.isDirectory()) {
|
||||
result.addAll(mediaFileService.getChildrenOf(file, true, false, true));
|
||||
List<MediaFile> childrenOf = mediaFileService.getChildrenOf(file, true, false, true);
|
||||
result.addAll(childrenOf.stream().map(mf -> addUrlInfo(request, player, mf, finalExpires)).collect(Collectors.toList()));
|
||||
} else {
|
||||
result.add(file);
|
||||
result.add(addUrlInfo(request, player, file, finalExpires));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,4 +127,26 @@ public class ExternalPlayerController {
|
||||
return result;
|
||||
}
|
||||
|
||||
public MediaFileWithUrlInfo addUrlInfo(HttpServletRequest request, Player player, MediaFile mediaFile, Date expires) {
|
||||
String prefix = "/ext";
|
||||
String streamUrl = jwtSecurityService.addJWTToken(
|
||||
UriComponentsBuilder
|
||||
.fromHttpUrl(NetworkService.getBaseUrl(request) + prefix + "/stream")
|
||||
.queryParam("id", mediaFile.getId())
|
||||
.queryParam("player", player.getId())
|
||||
.queryParam("maxBitRate", "1200"),
|
||||
expires)
|
||||
.build()
|
||||
.toUriString();
|
||||
|
||||
String coverArtUrl = jwtSecurityService.addJWTToken(
|
||||
UriComponentsBuilder
|
||||
.fromHttpUrl(NetworkService.getBaseUrl(request) + prefix + "/coverArt.view")
|
||||
.queryParam("id", mediaFile.getId())
|
||||
.queryParam("size", "500"),
|
||||
expires)
|
||||
.build()
|
||||
.toUriString();
|
||||
return new MediaFileWithUrlInfo(mediaFile, coverArtUrl, streamUrl);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ package org.libresonic.player.controller;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.libresonic.player.domain.MediaFile;
|
||||
import org.libresonic.player.domain.Player;
|
||||
import org.libresonic.player.service.JWTSecurityService;
|
||||
import org.libresonic.player.service.MediaFileService;
|
||||
import org.libresonic.player.service.PlayerService;
|
||||
import org.libresonic.player.service.SecurityService;
|
||||
@@ -33,6 +34,7 @@ import org.springframework.web.bind.ServletRequestUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -50,7 +52,7 @@ import java.util.regex.Pattern;
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
@Controller(value = "hlsController")
|
||||
@RequestMapping("/hls/**")
|
||||
@RequestMapping(value = {"/hls/**", "/ext/hls/**"})
|
||||
public class HLSController {
|
||||
|
||||
private static final int SEGMENT_DURATION = 10;
|
||||
@@ -62,6 +64,8 @@ public class HLSController {
|
||||
private MediaFileService mediaFileService;
|
||||
@Autowired
|
||||
private SecurityService securityService;
|
||||
@Autowired
|
||||
private JWTSecurityService jwtSecurityService;
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET)
|
||||
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
@@ -142,7 +146,12 @@ public class HLSController {
|
||||
for (Pair<Integer, Dimension> bitRate : bitRates) {
|
||||
Integer kbps = bitRate.getFirst();
|
||||
writer.println("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + kbps * 1000L);
|
||||
writer.print(contextPath + "hls/hls.m3u8?id=" + id + "&player=" + player.getId() + "&bitRate=" + kbps);
|
||||
UriComponentsBuilder url = (UriComponentsBuilder.fromUriString(contextPath + "ext/hls/hls.m3u8")
|
||||
.queryParam("id", id)
|
||||
.queryParam("player", player.getId())
|
||||
.queryParam("bitRate", kbps));
|
||||
jwtSecurityService.addJWTToken(url);
|
||||
writer.print(url.toUriString());
|
||||
Dimension dimension = bitRate.getSecond();
|
||||
if (dimension != null) {
|
||||
writer.print("@" + dimension.width + "x" + dimension.height);
|
||||
@@ -173,17 +182,22 @@ public class HLSController {
|
||||
}
|
||||
|
||||
private String createStreamUrl(HttpServletRequest request, Player player, int id, int offset, int duration, Pair<Integer, Dimension> bitRate) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(getContextPath(request)).append("stream/stream.ts?id=").append(id).append("&hls=true&timeOffset=").append(offset)
|
||||
.append("&player=").append(player.getId()).append("&duration=").append(duration);
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(getContextPath(request) + "ext/stream/stream.ts");
|
||||
builder.queryParam("id", id);
|
||||
builder.queryParam("hls", "true");
|
||||
builder.queryParam("timeOffset", offset);
|
||||
builder.queryParam("player", player.getId());
|
||||
builder.queryParam("duration", duration);
|
||||
if (bitRate != null) {
|
||||
builder.append("&maxBitRate=").append(bitRate.getFirst());
|
||||
builder.queryParam("maxBitRate", bitRate.getFirst());
|
||||
Dimension dimension = bitRate.getSecond();
|
||||
if (dimension != null) {
|
||||
builder.append("&size=").append(dimension.width).append("x").append(dimension.height);
|
||||
builder.queryParam("size", dimension.width);
|
||||
builder.queryParam("x", dimension.height);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
jwtSecurityService.addJWTToken(builder);
|
||||
return builder.toUriString();
|
||||
}
|
||||
|
||||
private String getContextPath(HttpServletRequest request) {
|
||||
|
||||
+10
-3
@@ -63,7 +63,6 @@ public class ShareSettingsController {
|
||||
public String doGet(HttpServletRequest request, Model model) throws Exception {
|
||||
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("shareBaseUrl", shareService.getShareBaseUrl(request));
|
||||
map.put("shareInfos", getShareInfos(request));
|
||||
map.put("user", securityService.getCurrentUser(request));
|
||||
|
||||
@@ -121,7 +120,9 @@ public class ShareSettingsController {
|
||||
List<MediaFile> files = shareService.getSharedFiles(share.getId(), musicFolders);
|
||||
if (!files.isEmpty()) {
|
||||
MediaFile file = files.get(0);
|
||||
result.add(new ShareInfo(share, file.isDirectory() ? file : mediaFileService.getParentOf(file)));
|
||||
result.add(new ShareInfo(shareService.getShareUrl(request, share), share, file.isDirectory() ? file :
|
||||
mediaFileService
|
||||
.getParentOf(file)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -144,10 +145,12 @@ public class ShareSettingsController {
|
||||
}
|
||||
|
||||
public static class ShareInfo {
|
||||
private final String shareUrl;
|
||||
private final Share share;
|
||||
private final MediaFile dir;
|
||||
|
||||
public ShareInfo(Share share, MediaFile dir) {
|
||||
public ShareInfo(String shareUrl, Share share, MediaFile dir) {
|
||||
this.shareUrl = shareUrl;
|
||||
this.share = share;
|
||||
this.dir = dir;
|
||||
}
|
||||
@@ -156,6 +159,10 @@ public class ShareSettingsController {
|
||||
return share;
|
||||
}
|
||||
|
||||
public String getShareUrl() {
|
||||
return shareUrl;
|
||||
}
|
||||
|
||||
public MediaFile getDir() {
|
||||
return dir;
|
||||
}
|
||||
|
||||
+7
-3
@@ -25,12 +25,15 @@ import org.libresonic.player.domain.*;
|
||||
import org.libresonic.player.io.PlayQueueInputStream;
|
||||
import org.libresonic.player.io.RangeOutputStream;
|
||||
import org.libresonic.player.io.ShoutCastOutputStream;
|
||||
import org.libresonic.player.security.JWTAuthenticationToken;
|
||||
import org.libresonic.player.service.*;
|
||||
import org.libresonic.player.service.sonos.SonosHelper;
|
||||
import org.libresonic.player.util.HttpRange;
|
||||
import org.libresonic.player.util.StringUtil;
|
||||
import org.libresonic.player.util.Util;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.ServletRequestBindingException;
|
||||
import org.springframework.web.bind.ServletRequestUtils;
|
||||
@@ -55,7 +58,7 @@ import java.util.regex.Pattern;
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/stream/**")
|
||||
@RequestMapping(value = {"/stream/**", "/ext/stream/**"})
|
||||
public class StreamController {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(StreamController.class);
|
||||
@@ -86,10 +89,11 @@ public class StreamController {
|
||||
PlayQueueInputStream in = null;
|
||||
Player player = playerService.getPlayer(request, response, false, true);
|
||||
User user = securityService.getUserByName(player.getUsername());
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
try {
|
||||
|
||||
if (!user.isStreamRole()) {
|
||||
if (!(authentication instanceof JWTAuthenticationToken) && !user.isStreamRole()) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Streaming is forbidden for user " + user.getUsername());
|
||||
return null;
|
||||
}
|
||||
@@ -128,7 +132,7 @@ public class StreamController {
|
||||
|
||||
if (isSingleFile) {
|
||||
|
||||
if (!securityService.isFolderAccessAllowed(file, user.getUsername())) {
|
||||
if (!(authentication instanceof JWTAuthenticationToken) && !securityService.isFolderAccessAllowed(file, user.getUsername())) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN,
|
||||
"Access to file " + file.getId() + " is forbidden for user " + user.getUsername());
|
||||
return null;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+169
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+100
@@ -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);
|
||||
}
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
}
|
||||
+123
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,6 @@ package org.libresonic.player.service;
|
||||
public class ServiceLocator {
|
||||
|
||||
private static SettingsService settingsService;
|
||||
private static VersionService versionService;
|
||||
|
||||
private ServiceLocator() {
|
||||
}
|
||||
|
||||
public static SettingsService getSettingsService() {
|
||||
return settingsService;
|
||||
@@ -40,13 +36,5 @@ public class ServiceLocator {
|
||||
public static void setSettingsService(SettingsService settingsService) {
|
||||
ServiceLocator.settingsService = settingsService;
|
||||
}
|
||||
|
||||
public static VersionService getVersionService() {
|
||||
return versionService;
|
||||
}
|
||||
|
||||
public static void setVersionService(VersionService versionService) {
|
||||
ServiceLocator.versionService = versionService;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ public class SettingsService {
|
||||
private static final String KEY_SONOS_ENABLED = "SonosEnabled";
|
||||
private static final String KEY_SONOS_SERVICE_NAME = "SonosServiceName";
|
||||
private static final String KEY_SONOS_SERVICE_ID = "SonosServiceId";
|
||||
private static final String KEY_JWT_KEY = "JWTKey";
|
||||
|
||||
private static final String KEY_SMTP_SERVER = "SmtpServer";
|
||||
private static final String KEY_SMTP_ENCRYPTION = "SmtpEncryption";
|
||||
@@ -117,6 +118,7 @@ public class SettingsService {
|
||||
private static final String KEY_DATABASE_USERTABLE_QUOTE = "DatabaseUsertableQuote";
|
||||
|
||||
// Default values.
|
||||
private static final String DEFAULT_JWT_KEY = null;
|
||||
private static final String DEFAULT_INDEX_STRING = "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ)";
|
||||
private static final String DEFAULT_IGNORED_ARTICLES = "The El La Los Las Le Les";
|
||||
private static final String DEFAULT_SHORTCUTS = "New Incoming Podcast";
|
||||
@@ -1338,6 +1340,14 @@ public class SettingsService {
|
||||
setString(KEY_DATABASE_USERTABLE_QUOTE, usertableQuote);
|
||||
}
|
||||
|
||||
public String getJWTKey() {
|
||||
return getString(KEY_JWT_KEY, DEFAULT_JWT_KEY);
|
||||
}
|
||||
|
||||
public void setJWTKey(String jwtKey) {
|
||||
setString(KEY_JWT_KEY, jwtKey);
|
||||
}
|
||||
|
||||
public void setConfigurationService(ApacheCommonsConfigurationService configurationService) {
|
||||
this.configurationService = configurationService;
|
||||
}
|
||||
|
||||
@@ -23,17 +23,12 @@ import org.apache.commons.lang.ObjectUtils;
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
import org.libresonic.player.Logger;
|
||||
import org.libresonic.player.dao.ShareDao;
|
||||
import org.libresonic.player.domain.MediaFile;
|
||||
import org.libresonic.player.domain.MusicFolder;
|
||||
import org.libresonic.player.domain.Share;
|
||||
import org.libresonic.player.domain.User;
|
||||
import org.libresonic.player.domain.*;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Provides services for sharing media.
|
||||
@@ -47,8 +42,8 @@ public class ShareService {
|
||||
|
||||
private ShareDao shareDao;
|
||||
private SecurityService securityService;
|
||||
private SettingsService settingsService;
|
||||
private MediaFileService mediaFileService;
|
||||
private JWTSecurityService jwtSecurityService;
|
||||
|
||||
public List<Share> getAllShares() {
|
||||
return shareDao.getAllShares();
|
||||
@@ -115,12 +110,9 @@ public class ShareService {
|
||||
shareDao.deleteShare(id);
|
||||
}
|
||||
|
||||
public String getShareBaseUrl(HttpServletRequest request) {
|
||||
return NetworkService.getBaseUrl(request) + "/share/";
|
||||
}
|
||||
|
||||
public String getShareUrl(HttpServletRequest request, Share share) {
|
||||
return getShareBaseUrl(request) + share.getName();
|
||||
String shareUrl = NetworkService.getBaseUrl(request) + "/ext/share/" + share.getName();
|
||||
return jwtSecurityService.addJWTToken(UriComponentsBuilder.fromUriString(shareUrl), share.getExpires()).build().toUriString();
|
||||
}
|
||||
|
||||
public void setSecurityService(SecurityService securityService) {
|
||||
@@ -131,11 +123,11 @@ public class ShareService {
|
||||
this.shareDao = shareDao;
|
||||
}
|
||||
|
||||
public void setSettingsService(SettingsService settingsService) {
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
public void setMediaFileService(MediaFileService mediaFileService) {
|
||||
this.mediaFileService = mediaFileService;
|
||||
}
|
||||
|
||||
public void setJwtSecurityService(JWTSecurityService jwtSecurityService) {
|
||||
this.jwtSecurityService = jwtSecurityService;
|
||||
}
|
||||
}
|
||||
@@ -68,10 +68,6 @@ public class VersionService {
|
||||
*/
|
||||
private static final String VERSION_URL = "http://libresonic.org/release/version.txt";
|
||||
|
||||
public void init() {
|
||||
ServiceLocator.setVersionService(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version number for the locally installed Libresonic version.
|
||||
*
|
||||
|
||||
+7
-1
@@ -33,6 +33,7 @@ import org.libresonic.player.domain.*;
|
||||
import org.libresonic.player.service.MediaFileService;
|
||||
import org.libresonic.player.service.PlaylistService;
|
||||
import org.libresonic.player.util.Util;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@@ -260,7 +261,12 @@ public class FolderBasedContentDirectory extends LibresonicContentDirectory {
|
||||
}
|
||||
|
||||
private URI getAlbumArtUrl(MediaFile album) throws URISyntaxException {
|
||||
return new URI(getBaseUrl() + "coverArt.view?id=" + album.getId() + "&size=" + CoverArtScheme.LARGE.getSize());
|
||||
return jwtSecurityService.addJWTToken(UriComponentsBuilder.fromUriString(getBaseUrl() + "/ext/coverArt.view")
|
||||
.queryParam("id", album.getId())
|
||||
.queryParam("size", CoverArtScheme.LARGE.getSize()))
|
||||
.build()
|
||||
.encode()
|
||||
.toUri();
|
||||
}
|
||||
|
||||
public void setMediaFileService(MediaFileService mediaFileService) {
|
||||
|
||||
+17
-2
@@ -30,11 +30,13 @@ import org.fourthline.cling.support.model.Res;
|
||||
import org.fourthline.cling.support.model.SortCriterion;
|
||||
import org.libresonic.player.domain.MediaFile;
|
||||
import org.libresonic.player.domain.Player;
|
||||
import org.libresonic.player.service.JWTSecurityService;
|
||||
import org.libresonic.player.service.PlayerService;
|
||||
import org.libresonic.player.service.SettingsService;
|
||||
import org.libresonic.player.service.TranscodingService;
|
||||
import org.libresonic.player.util.StringUtil;
|
||||
import org.seamless.util.MimeType;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
@@ -47,14 +49,23 @@ public abstract class LibresonicContentDirectory extends AbstractContentDirector
|
||||
protected SettingsService settingsService;
|
||||
private PlayerService playerService;
|
||||
private TranscodingService transcodingService;
|
||||
protected JWTSecurityService jwtSecurityService;
|
||||
|
||||
protected Res createResourceForSong(MediaFile song) {
|
||||
Player player = playerService.getGuestPlayer(null);
|
||||
String url = getBaseUrl() + "stream?id=" + song.getId() + "&player=" + player.getId();
|
||||
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(getBaseUrl() + "/ext/stream")
|
||||
.queryParam("id", song.getId())
|
||||
.queryParam("player", player.getId());
|
||||
|
||||
if (song.isVideo()) {
|
||||
url += "&format=" + TranscodingService.FORMAT_RAW;
|
||||
builder.queryParam("format", TranscodingService.FORMAT_RAW);
|
||||
}
|
||||
|
||||
jwtSecurityService.addJWTToken(builder);
|
||||
|
||||
String url = builder.toUriString();
|
||||
|
||||
String suffix = song.isVideo() ? FilenameUtils.getExtension(song.getPath()) : transcodingService.getSuffix(player, song, null);
|
||||
String mimeTypeString = StringUtil.getMimeType(suffix);
|
||||
MimeType mimeType = mimeTypeString == null ? null : MimeType.valueOf(mimeTypeString);
|
||||
@@ -123,4 +134,8 @@ public abstract class LibresonicContentDirectory extends AbstractContentDirector
|
||||
public void setSettingsService(SettingsService settingsService) {
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
public void setJwtSecurityService(JWTSecurityService jwtSecurityService) {
|
||||
this.jwtSecurityService = jwtSecurityService;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
<property name="playlistDao" ref="playlistDao"/>
|
||||
</bean>
|
||||
|
||||
<bean id="versionService" class="org.libresonic.player.service.VersionService" init-method="init"/>
|
||||
<bean id="versionService" class="org.libresonic.player.service.VersionService" />
|
||||
|
||||
<bean id="statusService" class="org.libresonic.player.service.StatusService">
|
||||
<property name="mediaFileService" ref="mediaFileService"/>
|
||||
@@ -166,9 +166,9 @@
|
||||
|
||||
<bean id="shareService" class="org.libresonic.player.service.ShareService">
|
||||
<property name="shareDao" ref="shareDao"/>
|
||||
<property name="settingsService" ref="settingsService"/>
|
||||
<property name="securityService" ref="securityService"/>
|
||||
<property name="mediaFileService" ref="mediaFileService"/>
|
||||
<property name="jwtSecurityService" ref="jwtSecurityService" />
|
||||
</bean>
|
||||
|
||||
<bean id="podcastService" class="org.libresonic.player.service.PodcastService" init-method="init">
|
||||
@@ -198,6 +198,7 @@
|
||||
<property name="playerService" ref="playerService"/>
|
||||
<property name="transcodingService" ref="transcodingService"/>
|
||||
<property name="mediaFileService" ref="mediaFileService"/>
|
||||
<property name="jwtSecurityService" ref="jwtSecurityService" />
|
||||
</bean>
|
||||
|
||||
<bean id="upnpService" class="org.libresonic.player.service.UPnPService" init-method="init">
|
||||
@@ -299,4 +300,8 @@
|
||||
<property name="settingsService" ref="settingsService"/>
|
||||
</bean>
|
||||
|
||||
<bean id="jwtSecurityService" class="org.libresonic.player.service.JWTSecurityService">
|
||||
<constructor-arg ref="settingsService" />
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
|
||||
@@ -10,12 +10,8 @@
|
||||
<meta name="og:type" content="album"/>
|
||||
|
||||
<c:if test="${not empty model.songs}">
|
||||
<sub:url value="/coverArt.view" var="coverArtUrl">
|
||||
<sub:param name="id" value="${model.songs[0].id}"/>
|
||||
<sub:param name="size" value="500"/>
|
||||
</sub:url>
|
||||
<meta name="og:title" content="${fn:escapeXml(model.songs[0].artist)} — ${fn:escapeXml(model.songs[0].albumName)}"/>
|
||||
<meta name="og:image" content="${coverArtUrl}"/>
|
||||
<meta name="og:image" content="${model.songs[0].coverArtUrl}"/>
|
||||
</c:if>
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -45,21 +41,12 @@
|
||||
var list = new Array();
|
||||
|
||||
<c:forEach items="${model.songs}" var="song" varStatus="loopStatus">
|
||||
<%--@elvariable id="song" type="org.libresonic.player.domain.MediaFile"--%>
|
||||
<sub:url value="/stream" var="streamUrl">
|
||||
<sub:param name="id" value="${song.id}"/>
|
||||
<sub:param name="player" value="${model.player}"/>
|
||||
<sub:param name="maxBitRate" value="1200"/>
|
||||
</sub:url>
|
||||
<sub:url value="/coverArt.view" var="coverUrl">
|
||||
<sub:param name="id" value="${song.id}"/>
|
||||
<sub:param name="size" value="500"/>
|
||||
</sub:url>
|
||||
<%--@elvariable id="song" type="org.libresonic.player.domain.MediaFileWithUrlInfo"--%>
|
||||
|
||||
// TODO: Use video provider for aac, m4a
|
||||
list[${loopStatus.count - 1}] = {
|
||||
file: "${streamUrl}",
|
||||
image: "${coverUrl}",
|
||||
file: "${song.streamUrl}",
|
||||
image: "${song.coverArtUrl}",
|
||||
title: "${fn:escapeXml(song.title)}",
|
||||
provider: "${song.video ? "video" : "sound"}",
|
||||
description: "${fn:escapeXml(song.artist)}"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</c:url>
|
||||
|
||||
<tr>
|
||||
<td style="padding-left:1em"><a href="${model.shareBaseUrl}${share.name}" target="_blank">${share.name}</a></td>
|
||||
<td style="padding-left:1em"><a href="${shareInfo.shareUrl}" target="_blank">${share.name}</a></td>
|
||||
<td style="padding-left:1em">${fn:escapeXml(share.username)}</td>
|
||||
<td style="padding-left:1em"><input type="text" name="description[${share.id}]" size="40" value="${share.description}"/></td>
|
||||
<td style="padding-left:1em"><fmt:formatDate value="${share.lastVisited}" type="date" dateStyle="medium"/></td>
|
||||
|
||||
+62
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user