content: package org.airsonic.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.airsonic.player.service.JWTSecurityService;
  import org.apache.commons.lang3.StringUtils;
  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 LOG = 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)) {
              LOG.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/")) {
              LOG.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) {
          LOG.debug("Comparing expected [{}] vs requested [{}]", expectedRaw, requestedPathRaw);
          if (StringUtils.isEmpty(expectedRaw)) {
              LOG.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())) {
                  LOG.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().isEmpty() ||
                      !difference.entriesOnlyOnLeft().isEmpty() ||
                      difference.entriesOnlyOnRight().size() != 1 ||
                      difference.entriesOnlyOnRight().get(JWTSecurityService.JWT_PARAM_NAME) == null) {
                  LOG.debug("False: expected query params [{}] do not match requested query params [{}]", expected.getQueryParams(), requested.getQueryParams());
                  return false;
              }
  
          } catch (Exception e) {
              LOG.warn("Exception encountered while comparing paths", e);
              return false;
          }
          return true;
      }
  
      @Override
      public boolean supports(Class<?> authentication) {
          return JWTAuthenticationToken.class.isAssignableFrom(authentication);
      }
  }
  