src/main/java/net/oauth/jsontoken/discovery/JsonServerInfo.java
public class JsonServerInfo implements ServerInfo {

  @SerializedName("verification_keys")
  private final Map<String, String> verificationKeys = Maps.newHashMap();

  /**
   * Parses a JSON-formatted server info document and returns it as a {@link JsonServerInfo} object.
   *
   * @param json the contents of the JSON-formatted server info document.
   */
  public static JsonServerInfo getDocument(String json) {
    return new Gson().fromJson(json, JsonServerInfo.class);
  }

  /*
   * (non-Javadoc)
   * @see net.oauth.jsontoken.discovery.ServerInfo#getVerificationKey(java.lang.String)
   */
  @Override
  public PublicKey getVerificationKey(String keyId) {
    String magicKey = verificationKeys.get(keyId);
    if (magicKey == null) {
      return null;
    } else {
      return new MagicRsaPublicKey(magicKey).getKey();
    }
  }
}
src/main/java/net/oauth/jsontoken/crypto/HmacSHA256Verifier.java
public class HmacSHA256Verifier implements Verifier {

  private final HmacSHA256Signer signer;

  /**
   * Public constructor.
   *
   * @param verificationKey the HMAC verification key to be used for signature verification.
   * @throws InvalidKeyException if the verificationKey cannot be used as an HMAC key.
   */
  public HmacSHA256Verifier(byte[] verificationKey) throws InvalidKeyException {
    signer = new HmacSHA256Signer("verifier", null, verificationKey);
  }

  /*
   * (non-Javadoc)
   * @see net.oauth.jsontoken.crypto.Verifier#verifySignature(byte[], byte[])
   */
  @Override
  public void verifySignature(byte[] source, byte[] signature) throws SignatureException {
    byte[] comparison = signer.sign(source);
    if (!compareBytes(signature, comparison)) {
      throw new SignatureException("signature did not verify");
    }
  }

  /**
   * Performs a byte-by-byte comparison of {@code first} and {@code second} parameters. This method
   * will "NOT" short-circuit the comparison once it has detected a byte difference in order to
   * defend against a "timing attack".
   *
   * @param first the first byte array used in the comparison
   * @param second the second byte array used in the comparison
   * @return {@code true} if the {@code first} and {@code second} byte arrays are equal otherwise
   *     {@code false}
   */
  private boolean compareBytes(byte[] first, byte[] second) {
    if (first == null || second == null) {
      return (first == second);
    } else if (first.length != second.length) {
      return false;
    } else {
      byte result = 0;
      for (int i = 0; i < first.length; i++) {
        result |= first[i] ^ second[i];
      }
      return (result == 0);
    }
  }
}
src/main/java/net/oauth/jsontoken/discovery/VerifierProvider.java
public interface VerifierProvider {

  /**
   * Returns the {@code List<Verifier>} that represents a certain verification key, given the key's
   * id and its issuer.
   *
   * @param issuer the id of the issuer that's using the key.
   * @param keyId the id of the key, if keyId mismatches, return a list of possible verification
   *     keys.
   * @return a {@code List<Verifier>} object that represents the verification key.
   */
  List<Verifier> findVerifier(String issuer, String keyId);
}
src/main/java/net/oauth/jsontoken/exceptions/InvalidJsonTokenException.java
public final class InvalidJsonTokenException extends Exception {
  private final ErrorCode errorCode;

  public InvalidJsonTokenException(ErrorCode errorCode) {
    this.errorCode = errorCode;
  }

  public InvalidJsonTokenException(ErrorCode errorCode, String message) {
    super(message);
    this.errorCode = errorCode;
  }

  public InvalidJsonTokenException(ErrorCode errorCode, Throwable cause) {
    super(cause);
    this.errorCode = errorCode;
  }

  public ErrorCode getErrorCode() {
    return errorCode;
  }
}
src/main/java/net/oauth/jsontoken/discovery/DefaultPublicKeyLocator.java
public class DefaultPublicKeyLocator implements VerifierProvider {

  private final ServerDescriptorProvider descriptorProvider;
  private final ServerInfoResolver descriptorResolver;

  /**
   * Public constructor.
   *
   * @param descriptorProvider A {@link ServerDescriptorProvider} that maps issuer ids to server
   *     descriptors (URLs).
   * @param resolver A {@link ServerInfoResolver}, i.e., an object that can fetch and parse a server
   *     info document, given a server descriptor.
   */
  public DefaultPublicKeyLocator(
      ServerDescriptorProvider descriptorProvider, ServerInfoResolver resolver) {
    this.descriptorProvider = descriptorProvider;
    this.descriptorResolver = resolver;
  }

  /*
   * (non-Javadoc)
   * @see net.oauth.jsontoken.discovery.VerifierProvider#findVerifier(java.lang.String, java.lang.String)
   */
  @Override
  public List<Verifier> findVerifier(String issuer, String keyId) {
    URI serverDescriptor = descriptorProvider.getServerDescriptor(issuer);
    Verifier rsaVerifier =
        new RsaSHA256Verifier(
            descriptorResolver.resolve(serverDescriptor).getVerificationKey(keyId));
    return Lists.newArrayList(rsaVerifier);
  }
}
src/main/java/net/oauth/jsontoken/discovery/VerifierProviders.java
public class VerifierProviders {

  private final Map<SignatureAlgorithm, VerifierProvider> map = Maps.newHashMap();

  /** Sets a new {@link VerifierProvider} for the given {@link SignatureAlgorithm}. */
  public void setVerifierProvider(SignatureAlgorithm alg, VerifierProvider provider) {
    map.put(alg, provider);
  }

  /** Returns the {@link VerifierProvider} for the given {@link SignatureAlgorithm}. */
  @Nullable
  public VerifierProvider getVerifierProvider(SignatureAlgorithm alg) {
    return map.get(alg);
  }
}
src/main/java/net/oauth/jsontoken/exceptions/ErrorCode.java
public enum ErrorCode {

  /** The header is missing required parameters. */
  BAD_HEADER,

  /** Signature failed verification. */
  BAD_SIGNATURE,

  /** IAT is after EXP or IAT is in the future */
  BAD_TIME_RANGE,

  /** IAT and EXP are both in the past. */
  EXPIRED_TOKEN,

  /**
   * The token is in an illegal state because of incorrect use of the library. If this error code
   * appears, immediately rethrow as an {@link IllegalStateException}.
   */
  ILLEGAL_STATE,

  /** Token string is corrupted and/or does not contain three components. */
  MALFORMED_TOKEN_STRING,

  /** There are no verifiers available for a given issuer and keyId. */
  NO_VERIFIER,

  /** Generic catch-all for exceptions with scenarios that are not pre-defined. */
  UNKNOWN,

  /** The signature algorithm is not supported or is unknown. */
  UNSUPPORTED_ALGORITHM
}
src/main/java/net/oauth/jsontoken/crypto/SignatureAlgorithm.java
public enum SignatureAlgorithm {
  HS256("SHA256"),
  HS1("SHA1"),
  RS256("SHA256"),
  RS1("SHA1");

  private final String hashAlg;

  private SignatureAlgorithm(String hashAlg) {
    this.hashAlg = hashAlg;
  }

  /** What the signature algorithm is named in the "alg" parameter in a JSON Token's envelope. */
  public String getNameForJson() {
    return name();
  }

  /**
   * Returns the hash algorithm that should be used when hashing data. When large pieces of data are
   * to be included in a JSON Token's payload, it sometimes might make sense to include the hash of
   * the data instead. If an issuer wants to do that, they should use this hash algorithm to hash
   * the data.
   */
  public String getHashAlgorithm() {
    return hashAlg;
  }

  /** Given the name of the algorithm in the envelope, returns the corresponding enum instance. */
  public static SignatureAlgorithm getFromJsonName(String name) {
    return SignatureAlgorithm.valueOf(name);
  }
}
src/main/java/net/oauth/jsontoken/discovery/IdentityServerDescriptorProvider.java
public class IdentityServerDescriptorProvider implements ServerDescriptorProvider {

  /*
   * (non-Javadoc)
   * @see net.oauth.jsontoken.discovery.ServerDescriptorProvider#getServerDescriptor(java.lang.String)
   */
  @Override
  public URI getServerDescriptor(String issuer) {
    return URI.create(issuer);
  }
}
src/main/java/net/oauth/jsontoken/crypto/Verifier.java
public interface Verifier {

  /**
   * Verifies a signature on an array of bytes.
   *
   * @param source The bytes that were signed.
   * @param signature The signature on the bytes.
   * @throws SignatureException If the signature doesn't match, or if some other error occurred.
   */
  void verifySignature(byte[] source, byte[] signature) throws SignatureException;
}


Question: import static org.junit.Assert.assertThrows;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.regex.Pattern;
import junit.framework.TestCase;
import net.oauth.jsontoken.crypto.HmacSHA256Verifier;
import net.oauth.jsontoken.crypto.SignatureAlgorithm;
import net.oauth.jsontoken.crypto.Verifier;
import net.oauth.jsontoken.discovery.DefaultPublicKeyLocator;
import net.oauth.jsontoken.discovery.IdentityServerDescriptorProvider;
import net.oauth.jsontoken.discovery.JsonServerInfo;
import net.oauth.jsontoken.discovery.VerifierProvider;
import net.oauth.jsontoken.discovery.VerifierProviders;
import net.oauth.jsontoken.exceptions.ErrorCode;
import net.oauth.jsontoken.exceptions.InvalidJsonTokenException;
import org.apache.commons.codec.binary.Base64;
import org.junit.function.ThrowingRunnable;
/*
 * Copyright 2010 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.oauth.jsontoken;



public abstract class JsonTokenTestBase extends TestCase {

  protected static final byte[] SYMMETRIC_KEY = "kjdhasdkjhaskdjhaskdjhaskdjh".getBytes();

  protected static final String PRIVATE_KEY =
      "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6nMEXFuxTnM5+yM4Afngybf5Z"
          + "89JxlchBA3Ni//Gm1/25MetzfId2Jg8NkthmRDzH6sFaoNS7n6Z6JyNJFszb2PXKBkZdem219F5k"
          + "jawoHrfA1Lu8fBmGQYG/aG70aPft2eEZbY+XqW5WUlMk7vFW7BDikwBXyv/5rrFasBfPWd13xozQ"
          + "9612IErWGlGMgxmB64jcTbGWMzDgzE/scSmyeQ0vQQMW8J+Nnb/yDpY7loXrVrAgZx8IBv1f9Fv3"
          + "p7tirTD/vFgzxE2rIAauM/aU8zBHEyXL1NSNq0I62OAF4DLiDlcEFOvYjqoiCPQIh0NXnQy8Dcs5"
          + "xHCj0e1b3X/LAgMBAAECggEBAJ9G5iQQA7xF7ZYXTITtbSgV6+/ZBTi/kEG1cUoBjL9MQZpNOlrC"
          + "4lf8mgKK4LtA6OP1wfzZo1lVJeHDAAIFPOs0nm1Ft89XjMauAdcveM5xkYM7z9VL0vlddiHqQDHK"
          + "WjsgKVnrwpC/I5b4A1FVxJXdPXg14igM8zioW2Y9QMVPxeUmRJxeGfvlotRlD1At1KNKg7Q2bPoi"
          + "1IlRzdae6ky18x/o6FRbTo2WGRehqIAjqmwqNib3u4k/1QfEbKGShVjMtraxdlFBM7kXb/pTfhhU"
          + "xlsf4xraVy2LWBLen+BAOYScd0P7vD+5oET+e4YVqILoz/WQqI9BYmTHkzj+LLECgYEA9bVjRrXq"
          + "5NtO94w0T2BI9yGnZNRFbCcSocUlc6lgX7lFa6N5JvaoWF5p9CmUPPm7lxGOeSzvLKB4qv3uP/Px"
          + "RQzWvAT/isKnSJ2FuKcFYGA527uJ5BlOJAtTKViYhQdYlE2g9KsjLkxJ27aF49jrkhKWqueIdJpF"
          + "VfF9w+KYvVkCgYEAwm205fCRH3WEBzii2TrHqm/nVRWZ7Kxis4JppwxUslLKp33bzbHn9uOKFGfN"
          + "rtXpSq9hvAcnJlJAEyVFtVNFcazE/+GbUfnrKaC3UeomjYxBk45Lcutt441gOO2SFcra7GHiNgVv"
          + "fELNMo/Rr7tk8djcUcYXuDk4Kz/T2AttzcMCgYBg/Z8YtIrqmB+N3Exx4OIsm55GUPyueqYCMZ5d"
          + "D8k5QBtFKByU4t0FNQ/CD/+yKiqAsa956eDnztiTNvWrTRI6XZ0OTzLIhZofMf8tKtEWgCWWtWrz"
          + "HYIY/FdxhMWADaxLrnEQ49VZW0f0cRJdJK2o1amgARF+Zb9k85TflD0S0QKBgBYFlQrCT72vcs/a"
          + "k19lb/4XBK23b6LF97v7VnosRF+aTwffkoje0LY/GYGsLDjUU8M40Coa6U1G3akNfLLIBsKUXg/Z"
          + "ft0vIHqrkHf/vHQl4buTz2npzp2Kgs6P4g8D1f4WLCgQP4tkiZdjgM2VvR5DgNjmRgOAv6LubNE4"
          + "oiw/AoGAXKfOSrbgx8JQUE7Lt6mhGvP9oTj3uiV16GgxjOtkpP3TfjsdRcmivAuekKMMKufQvGxh"
          + "nX9eCYvLqJqZZwPy/002H7So3Yd1/d9ORkKetDKGjXHPDYyEPQQ+ss9OGm53XlViklXb+i9wsdDz"
          + "R7tAFexSjyVKnWSDBh52t6lBtHo=";

  private static final String SERVER_INFO_DOCUMENT =
      "{ \"verification_keys\": {"
          +
          // this is the public key that goes with the above private key
          "\"key1\":\"RSA.ALqcwRcW7FOczn7IzgB-eDJt_lnz0nGVyEEDc2L_8abX_bkx63N8h3YmDw2S2GZEPMfqwVqg1LufpnonI0kWzNvY9coGRl16bbX0XmSNrCget8DUu7x8GYZBgb9obvRo9-3Z4Rltj5epblZSUyTu8VbsEOKTAFfK__musVqwF89Z3XfGjND3rXYgStYaUYyDGYHriNxNsZYzMODMT-xxKbJ5DS9BAxbwn42dv_IOljuWhetWsCBnHwgG_V_0W_enu2KtMP-8WDPETasgBq4z9pTzMEcTJcvU1I2rQjrY4AXgMuIOVwQU69iOqiII9AiHQ1edDLwNyznEcKPR7Vvdf8s.AQAB\"},"
        + " "
          +
          // some other information that might be in the server info document.
          "\"foo\": \"bar\"}";

  protected VerifierProviders locators;
  protected VerifierProviders locatorsFromRuby;
  protected RSAPrivateKey privateKey;

  protected static final String TOKEN_STRING =
      "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtleTIifQ.eyJpc3MiOiJnb29nbGUuY29tIiwiYmFyIjoxNSwiZm9vIjoic29tZSB2YWx1ZSIsImF1ZCI6Imh0dHA6Ly93d3cuZ29vZ2xlLmNvbSIsImlhdCI6MTI3NjY2OTcyMiwiZXhwIjoxMjc2NjY5NzIzfQ.Xugb4nb5kLV3NTpOLaz9er5PhAI5mFehHst_33EUFHs";
  protected static final String TOKEN_STRING_BAD_SIG =
      "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtleTIifQ.eyJpc3MiOiJnb29nbGUuY29tIiwiYmFyIjoxNSwiZm9vIjoic29tZSB2YWx1ZSIsImF1ZCI6Imh0dHA6Ly93d3cuZ29vZ2xlLmNvbSIsImlhdCI6MTI3NjY2OTcyMiwiZXhwIjoxMjc2NjY5NzIzfQ.Wugb4nb5kLV3NTpOLaz9er5PhAI5mFehHst_33EUFHs";
  protected static final String TOKEN_STRING_2PARTS =
      "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtleTIifQ.eyJpc3MiOiJnb29nbGUuY29tIiwiYmFyIjoxNSwiZm9vIjoic29tZSB2YWx1ZSIsImF1ZCI6Imh0dHA6Ly93d3cuZ29vZ2xlLmNvbSIsImlhdCI6MTI3NjY2OTcyMiwiZXhwIjoxMjc2NjY5NzIzfQ";
  protected static final String TOKEN_STRING_UNSUPPORTED_SIGNATURE_ALGORITHM =
      "eyJhbGciOiJIUzUxMiIsImtpZCI6ImtleTIifQ.eyJpc3MiOiJnb29nbGUuY29tIiwiYmFyIjoxNSwiZm9vIjoic29tZSB2YWx1ZSIsImF1ZCI6Imh0dHA6Ly93d3cuZ29vZ2xlLmNvbSIsImlhdCI6MTI3NjY2OTcyMiwiZXhwIjoxMjc2NjY5NzIzfQ.44qsiZg1Hnf95N-2wNqd1htgDlE7X0BSUMMkboMcZ5QLKbmVATozMuzdoE0MAhU-IdWUuICFbzu_wcDEXDTLug";
  protected static final String TOKEN_STRING_CORRUPT_HEADER =
      "fyJhbGciOiJIUzI1NiIsImtpZCI6ImtleTIifQ.eyJpc3MiOiJnb29nbGUuY29tIiwiYmFyIjoxNSwiZm9vIjoic29tZSB2YWx1ZSIsImF1ZCI6Imh0dHA6Ly93d3cuZ29vZ2xlLmNvbSIsImlhdCI6MTI3NjY2OTcyMiwiZXhwIjoxMjc2NjY5NzIzfQ.Xugb4nb5kLV3NTpOLaz9er5PhAI5mFehHst_33EUFHs";
  protected static final String TOKEN_STRING_HEADER_MISSING_ALG =
      "eyJ3cm9uZ1BhcmFtIjoiSFMyNTYifQ.eyJpc3MiOiJnb29nbGUuY29tIiwiYmFyIjoxNSwiZm9vIjoic29tZSB2YWx1ZSIsImF1ZCI6Imh0dHA6Ly93d3cuZ29vZ2xlLmNvbSIsImlhdCI6MTI3NjY2OTcyMiwiZXhwIjoxMjc2NjY5NzIzfQ.mBtVwsgT2uZJqu2zyUzbCXF4tfo8jSSlOeRI0Tv222o";
  protected static final String TOKEN_FROM_RUBY =
      "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8";

  protected static final Duration SKEW = Duration.ofMinutes(1);
  protected FakeClock clock = new FakeClock(SKEW);

  /**
   * Convert encoded tokens into a more human-readable form without verifying. Useful for logging.
   */
  protected static String decodeTokenForHumans(String encodedToken) {
    String[] pieces = encodedToken.split(Pattern.quote("."));
    if (pieces.length != 3) {
      return "invalid token (3 segments expected): " + encodedToken;
    }
    for (int i = 0; i < 3; i++) {
      pieces[i] = new String(Base64.decodeBase64(pieces[i].getBytes()));
    }
    return pieces[0] + "." + pieces[1] + "." + pieces[2];
  }

  @Override
  protected void setUp() throws Exception {
    final Verifier hmacVerifier = new HmacSHA256Verifier(SYMMETRIC_KEY);

    VerifierProvider hmacLocator = (signerId, keyId) -> Lists.newArrayList(hmacVerifier);

    VerifierProvider rsaLocator =
Answer:         new DefaultPublicKeyLocator(