com.rt.storage.auth.oauth2.TokenVerifier Maven / Gradle / Ivy
package com.rt.storage.auth.oauth2;
import com.rt.storage.api.client.http.GenericUrl;
import com.rt.storage.api.client.http.HttpRequest;
import com.rt.storage.api.client.http.HttpResponse;
import com.rt.storage.api.client.http.HttpTransport;
import com.rt.storage.api.client.json.GenericJson;
import com.rt.storage.api.client.json.webtoken.JsonWebSignature;
import com.rt.storage.api.client.util.Base64;
import com.rt.storage.api.client.util.Clock;
import com.rt.storage.api.client.util.Key;
import com.rt.storage.auth.http.HttpTransportFactory;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.UncheckedExecutionException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* Handle verification of igned JWT tokens.
*
* @author Jeff Ching
* @since 0.21.0
*/
@Beta
public class TokenVerifier {
private static final String IAP_CERT_URL = "https://www.gstatic.com/iap/verify/public_key-jwk";
private static final String FEDERATED_SIGNON_CERT_URL =
"https://www.googleapis.com/oauth2/v3/certs";
private static final Set SUPPORTED_ALGORITHMS = ImmutableSet.of("RS256", "ES256");
private final String audience;
private final String certificatesLocation;
private final String issuer;
private final PublicKey publicKey;
private final Clock clock;
private final LoadingCache> publicKeyCache;
private TokenVerifier(Builder builder) {
this.audience = builder.audience;
this.certificatesLocation = builder.certificatesLocation;
this.issuer = builder.issuer;
this.publicKey = builder.publicKey;
this.clock = builder.clock;
this.publicKeyCache =
CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build(new PublicKeyLoader(builder.httpTransportFactory));
}
public static Builder newBuilder() {
return new Builder()
.setClock(Clock.SYSTEM)
.setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY);
}
/**
* Verify an encoded JWT token.
*
* @param token encoded JWT token
* @return the parsed JsonWebSignature instance for additional validation if necessary
* @throws VerificationException thrown if any verification fails
*/
public JsonWebSignature verify(String token) throws VerificationException {
JsonWebSignature jsonWebSignature;
try {
jsonWebSignature = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, token);
} catch (IOException e) {
throw new VerificationException("Error parsing JsonWebSignature token", e);
}
// Verify the expected audience if an audience is provided in the verifyOptions
if (audience != null && !audience.equals(jsonWebSignature.getPayload().getAudience())) {
throw new VerificationException("Expected audience does not match");
}
// Verify the expected issuer if an issuer is provided in the verifyOptions
if (issuer != null && !issuer.equals(jsonWebSignature.getPayload().getIssuer())) {
throw new VerificationException("Expected issuer does not match");
}
Long expiresAt = jsonWebSignature.getPayload().getExpirationTimeSeconds();
if (expiresAt != null && expiresAt <= clock.currentTimeMillis() / 1000) {
throw new VerificationException("Token is expired");
}
// Short-circuit signature types
if (!SUPPORTED_ALGORITHMS.contains(jsonWebSignature.getHeader().getAlgorithm())) {
throw new VerificationException(
"Unexpected signing algorithm: expected either RS256 or ES256");
}
PublicKey publicKeyToUse = publicKey;
if (publicKeyToUse == null) {
try {
String certificateLocation = getCertificateLocation(jsonWebSignature);
publicKeyToUse =
publicKeyCache.get(certificateLocation).get(jsonWebSignature.getHeader().getKeyId());
} catch (ExecutionException | UncheckedExecutionException e) {
throw new VerificationException("Error fetching PublicKey from certificate location", e);
}
}
if (publicKeyToUse == null) {
throw new VerificationException(
"Could not find PublicKey for provided keyId: "
+ jsonWebSignature.getHeader().getKeyId());
}
try {
if (jsonWebSignature.verifySignature(publicKeyToUse)) {
return jsonWebSignature;
}
throw new VerificationException("Invalid signature");
} catch (GeneralSecurityException e) {
throw new VerificationException("Error validating token", e);
}
}
private String getCertificateLocation(JsonWebSignature jsonWebSignature)
throws VerificationException {
if (certificatesLocation != null) return certificatesLocation;
switch (jsonWebSignature.getHeader().getAlgorithm()) {
case "RS256":
return FEDERATED_SIGNON_CERT_URL;
case "ES256":
return IAP_CERT_URL;
}
throw new VerificationException("Unknown algorithm");
}
public static class Builder {
private String audience;
private String certificatesLocation;
private String issuer;
private PublicKey publicKey;
private Clock clock;
private HttpTransportFactory httpTransportFactory;
/**
* Set a target audience to verify.
*
* @param audience the audience claim to verify
* @return the builder
*/
public Builder setAudience(String audience) {
this.audience = audience;
return this;
}
/**
* Override the location URL that contains published public keys. Defaults to well-known Google
* locations.
*
* @param certificatesLocation URL to published public keys
* @return the builder
*/
public Builder setCertificatesLocation(String certificatesLocation) {
this.certificatesLocation = certificatesLocation;
return this;
}
/**
* Set the issuer to verify.
*
* @param issuer the issuer claim to verify
* @return the builder
*/
public Builder setIssuer(String issuer) {
this.issuer = issuer;
return this;
}
/**
* Set the PublicKey for verifying the signature. This will ignore the key id from the JWT token
* header.
*
* @param publicKey the public key to validate the signature
* @return the builder
*/
public Builder setPublicKey(PublicKey publicKey) {
this.publicKey = publicKey;
return this;
}
/**
* Set the clock for checking token expiry. Used for testing.
*
* @param clock the clock to use. Defaults to the system clock
* @return the builder
*/
public Builder setClock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Set the HttpTransportFactory used for requesting public keys from the certificate URL. Used
* mostly for testing.
*
* @param httpTransportFactory the HttpTransportFactory used to build certificate URL requests
* @return the builder
*/
public Builder setHttpTransportFactory(HttpTransportFactory httpTransportFactory) {
this.httpTransportFactory = httpTransportFactory;
return this;
}
/**
* Build the custom TokenVerifier for verifying tokens.
*
* @return the customized TokenVerifier
*/
public TokenVerifier build() {
return new TokenVerifier(this);
}
}
/** Custom CacheLoader for mapping certificate urls to the contained public keys. */
static class PublicKeyLoader extends CacheLoader> {
private final HttpTransportFactory httpTransportFactory;
/**
* Data class used for deserializing a JSON Web Key Set (JWKS) from an external HTTP request.
*/
public static class JsonWebKeySet extends GenericJson {
@Key public List keys;
}
/** Data class used for deserializing a single JSON Web Key. */
public static class JsonWebKey {
@Key public String alg;
@Key public String crv;
@Key public String kid;
@Key public String kty;
@Key public String use;
@Key public String x;
@Key public String y;
@Key public String e;
@Key public String n;
}
PublicKeyLoader(HttpTransportFactory httpTransportFactory) {
super();
this.httpTransportFactory = httpTransportFactory;
}
@Override
public Map load(String certificateUrl) throws Exception {
HttpTransport httpTransport = httpTransportFactory.create();
JsonWebKeySet jwks;
try {
HttpRequest request =
httpTransport
.createRequestFactory()
.buildGetRequest(new GenericUrl(certificateUrl))
.setParser(OAuth2Utils.JSON_FACTORY.createJsonObjectParser());
HttpResponse response = request.execute();
jwks = response.parseAs(JsonWebKeySet.class);
} catch (IOException io) {
return ImmutableMap.of();
}
ImmutableMap.Builder keyCacheBuilder = new ImmutableMap.Builder<>();
if (jwks.keys == null) {
// Fall back to x509 formatted specification
for (String keyId : jwks.keySet()) {
String publicKeyPem = (String) jwks.get(keyId);
keyCacheBuilder.put(keyId, buildPublicKey(publicKeyPem));
}
} else {
for (JsonWebKey key : jwks.keys) {
try {
keyCacheBuilder.put(key.kid, buildPublicKey(key));
} catch (NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidParameterSpecException ignored) {
ignored.printStackTrace();
}
}
}
return keyCacheBuilder.build();
}
private PublicKey buildPublicKey(JsonWebKey key)
throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException {
if ("ES256".equals(key.alg)) {
return buildEs256PublicKey(key);
} else if ("RS256".equals((key.alg))) {
return buildRs256PublicKey(key);
} else {
return null;
}
}
private PublicKey buildPublicKey(String publicPem)
throws CertificateException, UnsupportedEncodingException {
return CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(publicPem.getBytes("UTF-8")))
.getPublicKey();
}
private PublicKey buildRs256PublicKey(JsonWebKey key)
throws NoSuchAlgorithmException, InvalidKeySpecException {
Preconditions.checkArgument("RSA".equals(key.kty));
Preconditions.checkNotNull(key.e);
Preconditions.checkNotNull(key.n);
BigInteger modulus = new BigInteger(1, Base64.decodeBase64(key.n));
BigInteger exponent = new BigInteger(1, Base64.decodeBase64(key.e));
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
private PublicKey buildEs256PublicKey(JsonWebKey key)
throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException {
Preconditions.checkArgument("EC".equals(key.kty));
Preconditions.checkArgument("P-256".equals(key.crv));
BigInteger x = new BigInteger(1, Base64.decodeBase64(key.x));
BigInteger y = new BigInteger(1, Base64.decodeBase64(key.y));
ECPoint pubPoint = new ECPoint(x, y);
AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
parameters.init(new ECGenParameterSpec("secp256r1"));
ECParameterSpec ecParameters = parameters.getParameterSpec(ECParameterSpec.class);
ECPublicKeySpec pubSpec = new ECPublicKeySpec(pubPoint, ecParameters);
KeyFactory kf = KeyFactory.getInstance("EC");
return kf.generatePublic(pubSpec);
}
}
/** Custom exception for wrapping all verification errors. */
public static class VerificationException extends Exception {
public VerificationException(String message) {
super(message);
}
public VerificationException(String message, Throwable cause) {
super(message, cause);
}
}
}