getAudience() {
return audience;
}
/**
* Verifies that the given ID token is valid using the cached public keys.
*
* It verifies:
*
*
* - The issuer is one of {@link #getIssuers()} by calling {@link
* IdToken#verifyIssuer(String)}.
*
- The audience is one of {@link #getAudience()} by calling {@link
* IdToken#verifyAudience(Collection)}.
*
- The current time against the issued at and expiration time, using the {@link #getClock()}
* and allowing for a time skew specified in {@link #getAcceptableTimeSkewSeconds()} , by
* calling {@link IdToken#verifyTime(long, long)}.
*
- This method verifies token signature per current OpenID Connect Spec:
* https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. By default,
* method gets a certificate from well-known location. A request to certificate location is
* performed using {@link com.google.api.client.http.javanet.NetHttpTransport} Both
* certificate location and transport implementation can be overridden via {@link Builder}
* not recommended: this check can be disabled with OAUTH_CLIENT_SKIP_SIGNATURE environment
* variable set to true. Use {@link #verifyPayload(IdToken)} instead.
*
*
* Deprecated. This method returns false if network requests to get certificates fail. Use {@link
* IdTokenVerifier.verfyOrThrow(IdToken)} instead to differentiate between potentially retryable
* network errors and false verification results.
*
* @param idToken ID token
* @return {@code true} if verified successfully or {@code false} if failed
*/
@Deprecated
public boolean verify(IdToken idToken) {
try {
return verifyOrThrow(idToken);
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
return false;
}
}
/**
* Verifies that the given ID token is valid using the cached public keys.
*
* It verifies:
*
*
* - The issuer is one of {@link #getIssuers()} by calling {@link
* IdToken#verifyIssuer(String)}.
*
- The audience is one of {@link #getAudience()} by calling {@link
* IdToken#verifyAudience(Collection)}.
*
- The current time against the issued at and expiration time, using the {@link #getClock()}
* and allowing for a time skew specified in {@link #getAcceptableTimeSkewSeconds()} , by
* calling {@link IdToken#verifyTime(long, long)}.
*
- This method verifies token signature per current OpenID Connect Spec:
* https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. By default,
* method gets a certificate from well-known location. A request to certificate location is
* performed using {@link com.google.api.client.http.javanet.NetHttpTransport} Both
* certificate location and transport implementation can be overridden via {@link Builder}
* not recommended: this check can be disabled with OAUTH_CLIENT_SKIP_SIGNATURE environment
* variable set to true.
*
*
* Overriding is allowed, but it must call the super implementation.
*
* @param idToken ID token
* @return {@code true} if verified successfully or {@code false} if payload validation failed
* @throws IOException if verification fails to run. For example, if it fails to get public keys
* for signature verification.
*/
public boolean verifyOrThrow(IdToken idToken) throws IOException {
boolean payloadValid = verifyPayload(idToken);
if (!payloadValid) {
return false;
}
try {
return verifySignature(idToken);
} catch (VerificationException ex) {
LOGGER.log(Level.INFO, "Id token signature verification failed. ", ex);
return false;
}
}
/**
* Verifies the payload of the given ID token
*
*
It verifies:
*
*
* - The issuer is one of {@link #getIssuers()} by calling {@link
* IdToken#verifyIssuer(String)}.
*
- The audience is one of {@link #getAudience()} by calling {@link
* IdToken#verifyAudience(Collection)}.
*
- The current time against the issued at and expiration time, using the {@link #getClock()}
* and allowing for a time skew specified in {@link #getAcceptableTimeSkewSeconds()} , by
* calling {@link IdToken#verifyTime(long, long)}.
*
*
* Overriding is allowed, but it must call the super implementation.
*
* @param idToken ID token
* @return {@code true} if verified successfully or {@code false} if failed
*/
protected boolean verifyPayload(IdToken idToken) {
boolean tokenPayloadValid =
(issuers == null || idToken.verifyIssuer(issuers))
&& (audience == null || idToken.verifyAudience(audience))
&& idToken.verifyTime(clock.currentTimeMillis(), acceptableTimeSkewSeconds);
return tokenPayloadValid;
}
@VisibleForTesting
boolean verifySignature(IdToken idToken) throws IOException, VerificationException {
if (Boolean.parseBoolean(environment.getVariable(SKIP_SIGNATURE_ENV_VAR))) {
return true;
}
// Short-circuit signature types
if (!SUPPORTED_ALGORITHMS.contains(idToken.getHeader().getAlgorithm())) {
throw new VerificationException(
String.format(NOT_SUPPORTED_ALGORITHM, idToken.getHeader().getAlgorithm()));
}
PublicKey publicKeyToUse = null;
try {
String certificateLocation = getCertificateLocation(idToken.getHeader());
publicKeyToUse = publicKeyCache.get(certificateLocation).get(idToken.getHeader().getKeyId());
} catch (ExecutionException | UncheckedExecutionException e) {
throw new IOException(
"Error fetching public key from certificate location " + certificatesLocation, e);
}
if (publicKeyToUse == null) {
throw new IOException(
"Could not find public key for provided keyId: " + idToken.getHeader().getKeyId());
}
try {
if (idToken.verifySignature(publicKeyToUse)) {
return true;
}
throw new VerificationException("Invalid signature");
} catch (GeneralSecurityException e) {
throw new VerificationException("Error validating token", e);
}
}
private String getCertificateLocation(Header header) throws VerificationException {
if (certificatesLocation != null) return certificatesLocation;
switch (header.getAlgorithm()) {
case "RS256":
return FEDERATED_SIGNON_CERT_URL;
case "ES256":
return IAP_CERT_URL;
}
throw new VerificationException(String.format(NOT_SUPPORTED_ALGORITHM, header.getAlgorithm()));
}
/**
* Builder for {@link IdTokenVerifier}.
*
*
Implementation is not thread-safe.
*
* @since 1.16
*/
public static class Builder {
/** Clock. */
Clock clock = Clock.SYSTEM;
String certificatesLocation;
/** wrapper for environment variables */
Environment environment;
/** Seconds of time skew to accept when verifying time. */
long acceptableTimeSkewSeconds = DEFAULT_TIME_SKEW_SECONDS;
/** Collection of equivalent expected issuers or {@code null} to suppress the issuer check. */
Collection issuers;
/** List of trusted audience client IDs or {@code null} to suppress the audience check. */
Collection audience;
HttpTransportFactory httpTransportFactory;
/** Builds a new instance of {@link IdTokenVerifier}. */
public IdTokenVerifier build() {
return new IdTokenVerifier(this);
}
/** Returns the clock. */
public final Clock getClock() {
return clock;
}
/**
* Sets the clock.
*
* Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Builder setClock(Clock clock) {
this.clock = Preconditions.checkNotNull(clock);
return this;
}
/**
* Returns the first of equivalent expected issuers or {@code null} if issuer check suppressed.
*/
public final String getIssuer() {
if (issuers == null) {
return null;
} else {
return issuers.iterator().next();
}
}
/**
* Sets the expected issuer or {@code null} to suppress the issuer check.
*
*
Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Builder setIssuer(String issuer) {
if (issuer == null) {
return setIssuers(null);
} else {
return setIssuers(Collections.singleton(issuer));
}
}
/**
* Overrides 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;
}
/**
* Returns the equivalent expected issuers or {@code null} if issuer check suppressed.
*
* @since 1.21.0
*/
public final Collection getIssuers() {
return issuers;
}
/**
* Sets the list of equivalent expected issuers or {@code null} to suppress the issuer check.
* Typically only a single issuer should be used, but multiple may be specified to support an
* issuer transitioning to a new string. The collection must not be empty.
*
* Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*
* @since 1.21.0
*/
public Builder setIssuers(Collection issuers) {
Preconditions.checkArgument(
issuers == null || !issuers.isEmpty(), "Issuers must not be empty");
this.issuers = issuers;
return this;
}
/**
* Returns the list of trusted audience client IDs or {@code null} to suppress the audience
* check.
*/
public final Collection getAudience() {
return audience;
}
/**
* Sets the list of trusted audience client IDs or {@code null} to suppress the audience check.
*
* Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Builder setAudience(Collection audience) {
this.audience = audience;
return this;
}
/** Returns the seconds of time skew to accept when verifying time. */
public final long getAcceptableTimeSkewSeconds() {
return acceptableTimeSkewSeconds;
}
/**
* Sets the seconds of time skew to accept when verifying time (default is {@link
* #DEFAULT_TIME_SKEW_SECONDS}).
*
* It must be greater or equal to zero.
*
*
Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Builder setAcceptableTimeSkewSeconds(long acceptableTimeSkewSeconds) {
Preconditions.checkArgument(acceptableTimeSkewSeconds >= 0);
this.acceptableTimeSkewSeconds = acceptableTimeSkewSeconds;
return this;
}
/** Returns an instance of the {@link Environment} */
final Environment getEnvironment() {
return environment;
}
/** Sets the environment. Used mostly for testing */
Builder setEnvironment(Environment environment) {
this.environment = environment;
return this;
}
/**
* Sets 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;
}
}
/** Custom CacheLoader for mapping certificate urls to the contained public keys. */
static class PublicKeyLoader extends CacheLoader> {
private static final int DEFAULT_NUMBER_OF_RETRIES = 2;
private static final int INITIAL_RETRY_INTERVAL_MILLIS = 1000;
private static final double RETRY_RANDOMIZATION_FACTOR = 0.1;
private static final double RETRY_MULTIPLIER = 2;
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(GsonFactory.getDefaultInstance().createJsonObjectParser());
request.setNumberOfRetries(DEFAULT_NUMBER_OF_RETRIES);
ExponentialBackOff backoff =
new ExponentialBackOff.Builder()
.setInitialIntervalMillis(INITIAL_RETRY_INTERVAL_MILLIS)
.setRandomizationFactor(RETRY_RANDOMIZATION_FACTOR)
.setMultiplier(RETRY_MULTIPLIER)
.build();
request.setUnsuccessfulResponseHandler(
new HttpBackOffUnsuccessfulResponseHandler(backoff)
.setBackOffRequired(BackOffRequired.ALWAYS));
HttpResponse response = request.execute();
jwks = response.parseAs(JsonWebKeySet.class);
} catch (IOException io) {
LOGGER.log(
Level.WARNING,
"Failed to get a certificate from certificate location " + certificateUrl,
io);
throw io;
}
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) {
LOGGER.log(Level.WARNING, "Failed to put a key into the cache", ignored);
}
}
}
ImmutableMap keyCache = keyCacheBuilder.build();
if (keyCache.isEmpty()) {
throw new VerificationException(
"No valid public key returned by the keystore: " + certificateUrl);
}
return keyCache;
}
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 {
com.google.common.base.Preconditions.checkArgument("RSA".equals(key.kty));
com.google.common.base.Preconditions.checkNotNull(key.e);
com.google.common.base.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 {
com.google.common.base.Preconditions.checkArgument("EC".equals(key.kty));
com.google.common.base.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. */
static class VerificationException extends Exception {
public VerificationException(String message) {
super(message);
}
public VerificationException(String message, Throwable cause) {
super(message, cause);
}
}
static class DefaultHttpTransportFactory implements HttpTransportFactory {
public HttpTransport create() {
return HTTP_TRANSPORT;
}
}
}