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);
}
}