getAudience() {
* 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.
+ * variable set to true. Use {@link #verifyPayload(IdToken)} instead.
*
*
- * Overriding is allowed, but it must call the super implementation.
+ * 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) {
@@ -242,11 +283,7 @@ public boolean verify(IdToken idToken) {
try {
return verifySignature(idToken);
} catch (VerificationException ex) {
- LOGGER.log(
- Level.SEVERE,
- "id token signature verification failed. "
- + "Please see docs for IdTokenVerifier for default settings and configuration options",
- ex);
+ LOGGER.log(Level.INFO, "Id token signature verification failed. ", ex);
return false;
}
}
@@ -281,7 +318,7 @@ protected boolean verifyPayload(IdToken idToken) {
}
@VisibleForTesting
- boolean verifySignature(IdToken idToken) throws VerificationException {
+ boolean verifySignature(IdToken idToken) throws IOException, VerificationException {
if (Boolean.parseBoolean(environment.getVariable(SKIP_SIGNATURE_ENV_VAR))) {
return true;
}
@@ -297,12 +334,12 @@ boolean verifySignature(IdToken idToken) throws VerificationException {
String certificateLocation = getCertificateLocation(idToken.getHeader());
publicKeyToUse = publicKeyCache.get(certificateLocation).get(idToken.getHeader().getKeyId());
} catch (ExecutionException | UncheckedExecutionException e) {
- throw new VerificationException(
+ throw new IOException(
"Error fetching public key from certificate location " + certificatesLocation, e);
}
if (publicKeyToUse == null) {
- throw new VerificationException(
+ throw new IOException(
"Could not find public key for provided keyId: " + idToken.getHeader().getKeyId());
}
@@ -330,14 +367,12 @@ private String getCertificateLocation(Header header) throws VerificationExceptio
}
/**
- * {@link Beta}
* Builder for {@link IdTokenVerifier}.
*
*
Implementation is not thread-safe.
*
* @since 1.16
*/
- @Beta
public static class Builder {
/** Clock. */
@@ -508,6 +543,10 @@ public Builder setHttpTransportFactory(HttpTransportFactory httpTransportFactory
/** 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;
/**
@@ -553,6 +592,19 @@ public Map load(String certificateUrl) throws Exception {
.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) {
@@ -589,7 +641,7 @@ public Map load(String certificateUrl) throws Exception {
"No valid public key returned by the keystore: " + certificateUrl);
}
- return keyCacheBuilder.build();
+ return keyCache;
}
private PublicKey buildPublicKey(JsonWebKey key)
diff --git a/google-oauth-client/src/test/java/com/google/api/client/auth/openidconnect/IdTokenVerifierTest.java b/google-oauth-client/src/test/java/com/google/api/client/auth/openidconnect/IdTokenVerifierTest.java
index 75f644061..556797eeb 100644
--- a/google-oauth-client/src/test/java/com/google/api/client/auth/openidconnect/IdTokenVerifierTest.java
+++ b/google-oauth-client/src/test/java/com/google/api/client/auth/openidconnect/IdTokenVerifierTest.java
@@ -68,9 +68,11 @@ public class IdTokenVerifierTest extends TestCase {
"https://www.googleapis.com/oauth2/v1/certs";
private static final String SERVICE_ACCOUNT_RS256_TOKEN =
- "eyJhbGciOiJSUzI1NiIsImtpZCI6IjJlZjc3YjM4YTFiMDM3MDQ4NzA0MzkxNmFjYmYyN2Q3NGVkZDA4YjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tL2F1ZGllbmNlIiwiZXhwIjoxNTg3NjMwNTQzLCJpYXQiOjE1ODc2MjY5NDMsImlzcyI6InNvbWUgaXNzdWVyIiwic3ViIjoic29tZSBzdWJqZWN0In0.gGOQW0qQgs4jGUmCsgRV83RqsJLaEy89-ZOG6p1u0Y26FyY06b6Odgd7xXLsSTiiSnch62dl0Lfi9D0x2ByxvsGOCbovmBl2ZZ0zHr1wpc4N0XS9lMUq5RJQbonDibxXG4nC2zroDfvD0h7i-L8KMXeJb9pYwW7LkmrM_YwYfJnWnZ4bpcsDjojmPeUBlACg7tjjOgBFbyQZvUtaERJwSRlaWibvNjof7eCVfZChE0PwBpZc_cGqSqKXv544L4ttqdCnmONjqrTATXwC4gYxruevkjHfYI5ojcQmXoWDJJ0-_jzfyPE4MFFdCFgzLgnfIOwe5ve0MtquKuv2O0pgvg";
+ "eyJhbGciOiJSUzI1NiIsImtpZCI6IjYwODNkZDU5ODE2NzNmNjYxZmRlOWRhZTY0NmI2ZjAzODBhMDE0NWMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiIzMjU1NTk0MDU1OS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImF6cCI6ImludGVncmF0aW9uLXRlc3RzQGNoaW5nb3ItdGVzdC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsIjoiaW50ZWdyYXRpb24tdGVzdHNAY2hpbmdvci10ZXN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTY4NjAwMjIyMywiaWF0IjoxNjg1OTk4NjIzLCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMDQwMjkyOTI4NTMwOTk5NzgyOTMifQ.0sJwjljZk38dJck4swTZTkqg6A_cBJE3JKpwH3CBVuU5MdUyKxuQz7GTs9akUn9426dtdsZvY_CPrbE-erkUKq-Es5aayNFNa4MdSKfaHbXGs5wopBQ-rnSjE3kAOPdD527a-NvbujBQ-069qupbms9p003Dgj2ph4AeAxR5vukn7hjGQNCnsouaVUzEqa6dB1JTQb895YPF6UOcjVDZf6S00Dot2vKFRvY2jWQ2AxnGjyZnzjyOg8lnXQWeLTFj_oqJc7xYmcxN1QCkXgcJfoThTRzvFokB7Qryi0m14rjPzOgQAQSGNniSnEGY5A5ZwKTdYIwZxCrDcZmrKfz7vQ";
+ private static final String SERVICE_ACCOUNT_RS256_TOKEN_BAD_SIGNATURE =
+ "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3MjdiNmI0OTQwMmI5Y2Y5NWJlNGU4ZmQzOGFhN2U3YzExNjQ0YjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Nsb3VkdGFza3MuZ29vZ2xlYXBpcy5jb20vdjIvcHJvamVjdHMvZ2Nsb3VkLWRldmVsL2xvY2F0aW9ucyIsImF6cCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNjYwODgwNjczLCJpYXQiOjE2NjA4NzcwNzMsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInN1YiI6IjExMjgxMDY3Mjk2MzcyODM2NjQwNiJ9.Q2tG-hN6UHecbzaCIlg58K9msp58nLZWs03CBGO_D6F3cI4LKQEUzsbcztZqmNGWd0ld4zkrKzIP9cQosa_xold4hEzSX_ORRHYQLimLYaQmP3rKqWPMsbIupPdpnGqBDzAYjc7Pw9pQBzuZJj8e3FEG6a5tblDfMcgeklXZIkwzN7ypWCbFDoDP2STSYJYZ-LQIB0-Zlex7dm2KhyB8QSkMQK60YvpXz4L1OtwG7spk3yUCWxul6hYF76klST0iS6DH03YdaDpt4gRXkTUKyTRfB10h-WhCAKKRzmT6d_IT9ApIyqPhimkgkBHhLNyjK8lgAJdk9CLriSEOgVpruy";
private static final String SERVICE_ACCOUNT_CERT_URL =
- "https://www.googleapis.com/robot/v1/metadata/x509/integration-tests%40chingor-test.iam.gserviceaccount.com";
+ "https://www.googleapis.com/oauth2/v3/certs";
private static final List ALL_TOKENS =
Arrays.asList(ES256_TOKEN, FEDERATED_SIGNON_RS256_TOKEN, SERVICE_ACCOUNT_RS256_TOKEN);
@@ -184,7 +186,7 @@ public void testBuilderSetNullIssuers() throws Exception {
assertNull(verifier.getIssuer());
}
- public void testMissingAudience() throws VerificationException {
+ public void testMissingAudience() throws IOException {
IdToken idToken = newIdToken(ISSUER, null);
MockClock clock = new MockClock();
@@ -198,7 +200,7 @@ public void testMissingAudience() throws VerificationException {
assertFalse(verifier.verify(idToken));
}
- public void testVerifyEs256TokenPublicKeyMismatch() throws Exception {
+ public void testPublicKeyStoreIntermittentError() throws Exception {
// Mock HTTP requests
MockLowLevelHttpRequest failedRequest =
new MockLowLevelHttpRequest() {
@@ -245,7 +247,7 @@ public LowLevelHttpResponse execute() throws IOException {
};
HttpTransportFactory httpTransportFactory =
- mockTransport(failedRequest, badRequest, emptyRequest, goodRequest);
+ mockTransport(failedRequest, badRequest, badRequest, badRequest, emptyRequest, goodRequest);
IdTokenVerifier tokenVerifier =
new IdTokenVerifier.Builder()
.setClock(FIXED_CLOCK)
@@ -255,28 +257,28 @@ public LowLevelHttpResponse execute() throws IOException {
try {
tokenVerifier.verifySignature(IdToken.parse(JSON_FACTORY, ES256_TOKEN));
fail("Should have failed verification");
- } catch (VerificationException ex) {
+ } catch (IOException ex) {
assertTrue(ex.getMessage().contains("Error fetching public key"));
}
try {
tokenVerifier.verifySignature(IdToken.parse(JSON_FACTORY, ES256_TOKEN));
fail("Should have failed verification");
- } catch (VerificationException ex) {
+ } catch (IOException ex) {
assertTrue(ex.getMessage().contains("Error fetching public key"));
}
try {
tokenVerifier.verifySignature(IdToken.parse(JSON_FACTORY, ES256_TOKEN));
fail("Should have failed verification");
- } catch (VerificationException ex) {
+ } catch (IOException ex) {
assertTrue(ex.getCause().getMessage().contains("No valid public key returned"));
}
Assert.assertTrue(tokenVerifier.verifySignature(IdToken.parse(JSON_FACTORY, ES256_TOKEN)));
}
- public void testVerifyEs256Token() throws VerificationException, IOException {
+ public void testVerifyEs256Token() throws IOException {
HttpTransportFactory httpTransportFactory =
mockTransport(
"https://www.gstatic.com/iap/verify/public_key-jwk",
@@ -289,7 +291,7 @@ public void testVerifyEs256Token() throws VerificationException, IOException {
assertTrue(tokenVerifier.verify(IdToken.parse(JSON_FACTORY, ES256_TOKEN)));
}
- public void testVerifyRs256Token() throws VerificationException, IOException {
+ public void testVerifyRs256Token() throws IOException {
HttpTransportFactory httpTransportFactory =
mockTransport(
"https://www.googleapis.com/oauth2/v3/certs",
@@ -304,7 +306,7 @@ public void testVerifyRs256Token() throws VerificationException, IOException {
}
public void testVerifyRs256TokenWithLegacyCertificateUrlFormat()
- throws VerificationException, IOException {
+ throws IOException, VerificationException {
HttpTransportFactory httpTransportFactory =
mockTransport(
LEGACY_FEDERATED_SIGNON_CERT_URL, readResourceAsString("legacy_federated_keys.json"));
@@ -318,15 +320,30 @@ public void testVerifyRs256TokenWithLegacyCertificateUrlFormat()
assertTrue(tokenVerifier.verify(IdToken.parse(JSON_FACTORY, FEDERATED_SIGNON_RS256_TOKEN)));
}
- public void testVerifyServiceAccountRs256Token() throws VerificationException, IOException {
- MockClock clock = new MockClock(1587626643000L);
- IdTokenVerifier tokenVerifier =
- new IdTokenVerifier.Builder()
- .setClock(clock)
- .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL)
- .setHttpTransportFactory(new DefaultHttpTransportFactory())
- .build();
+ private IdTokenVerifier generateTokenVerifier(long mockClockTime) throws IOException {
+ MockClock clock = new MockClock(mockClockTime);
+ HttpTransportFactory transportFactory =
+ mockTransport(SERVICE_ACCOUNT_CERT_URL, readResourceAsString("certs.json"));
+ return new IdTokenVerifier.Builder()
+ .setClock(clock)
+ .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+ }
+
+ public void testVerifyServiceAccountRs256Token() throws IOException {
+ // use newly used signature
+ IdTokenVerifier tokenVerifier = generateTokenVerifier(1686002000000L);
assertTrue(tokenVerifier.verify(IdToken.parse(JSON_FACTORY, SERVICE_ACCOUNT_RS256_TOKEN)));
+
+ // a token with a bad signature that is expected to fail in verify, but work in verifyPayload
+ assertFalse(
+ tokenVerifier.verify(
+ IdToken.parse(JSON_FACTORY, SERVICE_ACCOUNT_RS256_TOKEN_BAD_SIGNATURE)));
+ tokenVerifier = generateTokenVerifier(1660880973000L);
+ assertTrue(
+ tokenVerifier.verifyPayload(
+ IdToken.parse(JSON_FACTORY, SERVICE_ACCOUNT_RS256_TOKEN_BAD_SIGNATURE)));
}
static String readResourceAsString(String resourceName) throws IOException {
diff --git a/google-oauth-client/src/test/resources/certs.json b/google-oauth-client/src/test/resources/certs.json
new file mode 100644
index 000000000..74c9f8ef6
--- /dev/null
+++ b/google-oauth-client/src/test/resources/certs.json
@@ -0,0 +1,20 @@
+{
+ "keys": [
+ {
+ "e": "AQAB",
+ "n": "1qrQCTst3RF04aMC9Ye_kGbsE0sftL4FOtB_WrzBDOFdrfVwLfflQuPX5kJ-0iYv9r2mjD5YIDy8b-iJKwevb69ISeoOrmL3tj6MStJesbbRRLVyFIm_6L7alHhZVyqHQtMKX7IaNndrfebnLReGntuNk76XCFxBBnRaIzAWnzr3WN4UPBt84A0KF74pei17dlqHZJ2HB2CsYbE9Ort8m7Vf6hwxYzFtCvMCnZil0fCtk2OQ73l6egcvYO65DkAJibFsC9xAgZaF-9GYRlSjMPd0SMQ8yU9i3W7beT00Xw6C0FYA9JAYaGaOvbT87l_6ZkAksOMuvIPD_jNVfTCPLQ",
+ "use": "sig",
+ "kty": "RSA",
+ "alg": "RS256",
+ "kid": "6083dd5981673f661fde9dae646b6f0380a0145c"
+ },
+ {
+ "use": "sig",
+ "alg": "RS256",
+ "kid": "85ba9313fd7a7d4afa84884abcc8403004363180",
+ "e": "AQAB",
+ "kty": "RSA",
+ "n": "pP-rCe4jkKX6mq8yP1GcBZcxJzmxKWicHHor1S3Q49u6Oe-bQsk5NsK5mdR7Y7liGV9n0ikXSM42dYKQdxbhKA-7--fFon5isJoHr4fIwL2CCwVm5QWlK37q6PiH2_F1M0hRorHfkCb4nI56ZvfygvuOH4LIS82OzIgmsYbeEfwDRpeMSxWKwlpa3pX3GZ6jG7FgzJGBvmBkagpgsa2JZdyU4gEGMOkHdSzi5Ii-6RGfFLhhI1OMxC9P2JaU5yjMN2pikfFIq_dbpm75yNUGpWJNVywtrlNvvJfA74UMN_lVCAaSR0A03BUMg6ljB65gFllpKF224uWBA8tpjngwKQ"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 18e21f9d7..46c4fd8a5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.google.oauth-client
google-oauth-client-parent
- 1.34.1
+ 1.35.0
pom
Parent for the Google OAuth Client Library for Java
Google OAuth Client Library for Java
@@ -194,7 +194,7 @@