Skip to content

Commit 289f139

Browse files
author
Timur Sadykov
authored
fix!: IdTokenVerifier now throws IOException if any issue obtaining public keys. Adding retries to public key fetch to cover transient network issues. (#934)
* fix!: IdtokenVerifier now throws IOException if any issue obtaining public keys. Adding retries to public key fetch to cover transient network issues. * fix: docs nit fixes
1 parent a3ea3ab commit 289f139

File tree

2 files changed

+51
-23
lines changed

2 files changed

+51
-23
lines changed

google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/IdTokenVerifier.java

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package com.google.api.client.auth.openidconnect;
1616

1717
import com.google.api.client.http.GenericUrl;
18+
import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler;
19+
import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler.BackOffRequired;
1820
import com.google.api.client.http.HttpRequest;
1921
import com.google.api.client.http.HttpResponse;
2022
import com.google.api.client.http.HttpTransport;
@@ -25,6 +27,7 @@
2527
import com.google.api.client.util.Base64;
2628
import com.google.api.client.util.Beta;
2729
import com.google.api.client.util.Clock;
30+
import com.google.api.client.util.ExponentialBackOff;
2831
import com.google.api.client.util.Key;
2932
import com.google.api.client.util.Preconditions;
3033
import com.google.common.annotations.VisibleForTesting;
@@ -231,8 +234,10 @@ public final Collection<String> getAudience() {
231234
*
232235
* @param idToken ID token
233236
* @return {@code true} if verified successfully or {@code false} if failed
237+
* @throws IOException if verification fails to run. For example, if it fails to get public keys
238+
* for signature validation.
234239
*/
235-
public boolean verify(IdToken idToken) {
240+
public boolean verify(IdToken idToken) throws IOException {
236241
boolean payloadValid = verifyPayload(idToken);
237242

238243
if (!payloadValid) {
@@ -242,11 +247,7 @@ public boolean verify(IdToken idToken) {
242247
try {
243248
return verifySignature(idToken);
244249
} catch (VerificationException ex) {
245-
LOGGER.log(
246-
Level.SEVERE,
247-
"id token signature verification failed. "
248-
+ "Please see docs for IdTokenVerifier for default settings and configuration options",
249-
ex);
250+
LOGGER.log(Level.INFO, "Id token signature verification failed. ", ex);
250251
return false;
251252
}
252253
}
@@ -281,7 +282,7 @@ protected boolean verifyPayload(IdToken idToken) {
281282
}
282283

283284
@VisibleForTesting
284-
boolean verifySignature(IdToken idToken) throws VerificationException {
285+
boolean verifySignature(IdToken idToken) throws IOException, VerificationException {
285286
if (Boolean.parseBoolean(environment.getVariable(SKIP_SIGNATURE_ENV_VAR))) {
286287
return true;
287288
}
@@ -297,12 +298,12 @@ boolean verifySignature(IdToken idToken) throws VerificationException {
297298
String certificateLocation = getCertificateLocation(idToken.getHeader());
298299
publicKeyToUse = publicKeyCache.get(certificateLocation).get(idToken.getHeader().getKeyId());
299300
} catch (ExecutionException | UncheckedExecutionException e) {
300-
throw new VerificationException(
301+
throw new IOException(
301302
"Error fetching public key from certificate location " + certificatesLocation, e);
302303
}
303304

304305
if (publicKeyToUse == null) {
305-
throw new VerificationException(
306+
throw new IOException(
306307
"Could not find public key for provided keyId: " + idToken.getHeader().getKeyId());
307308
}
308309

@@ -508,6 +509,10 @@ public Builder setHttpTransportFactory(HttpTransportFactory httpTransportFactory
508509

509510
/** Custom CacheLoader for mapping certificate urls to the contained public keys. */
510511
static class PublicKeyLoader extends CacheLoader<String, Map<String, PublicKey>> {
512+
private static final int DEFAULT_NUMBER_OF_RETRIES = 2;
513+
private static final int INITIAL_RETRY_INTERVAL_MILLIS = 1000;
514+
private static final double RETRY_RANDOMIZATION_FACTOR = 0.1;
515+
private static final double RETRY_MULTIPLIER = 2;
511516
private final HttpTransportFactory httpTransportFactory;
512517

513518
/**
@@ -553,6 +558,19 @@ public Map<String, PublicKey> load(String certificateUrl) throws Exception {
553558
.createRequestFactory()
554559
.buildGetRequest(new GenericUrl(certificateUrl))
555560
.setParser(GsonFactory.getDefaultInstance().createJsonObjectParser());
561+
request.setNumberOfRetries(DEFAULT_NUMBER_OF_RETRIES);
562+
563+
ExponentialBackOff backoff =
564+
new ExponentialBackOff.Builder()
565+
.setInitialIntervalMillis(INITIAL_RETRY_INTERVAL_MILLIS)
566+
.setRandomizationFactor(RETRY_RANDOMIZATION_FACTOR)
567+
.setMultiplier(RETRY_MULTIPLIER)
568+
.build();
569+
570+
request.setUnsuccessfulResponseHandler(
571+
new HttpBackOffUnsuccessfulResponseHandler(backoff)
572+
.setBackOffRequired(BackOffRequired.ALWAYS));
573+
556574
HttpResponse response = request.execute();
557575
jwks = response.parseAs(JsonWebKeySet.class);
558576
} catch (IOException io) {
@@ -589,7 +607,7 @@ public Map<String, PublicKey> load(String certificateUrl) throws Exception {
589607
"No valid public key returned by the keystore: " + certificateUrl);
590608
}
591609

592-
return keyCacheBuilder.build();
610+
return keyCache;
593611
}
594612

595613
private PublicKey buildPublicKey(JsonWebKey key)

google-oauth-client/src/test/java/com/google/api/client/auth/openidconnect/IdTokenVerifierTest.java

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ public class IdTokenVerifierTest extends TestCase {
6868
"https://www.googleapis.com/oauth2/v1/certs";
6969

7070
private static final String SERVICE_ACCOUNT_RS256_TOKEN =
71-
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjJlZjc3YjM4YTFiMDM3MDQ4NzA0MzkxNmFjYmYyN2Q3NGVkZDA4YjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tL2F1ZGllbmNlIiwiZXhwIjoxNTg3NjMwNTQzLCJpYXQiOjE1ODc2MjY5NDMsImlzcyI6InNvbWUgaXNzdWVyIiwic3ViIjoic29tZSBzdWJqZWN0In0.gGOQW0qQgs4jGUmCsgRV83RqsJLaEy89-ZOG6p1u0Y26FyY06b6Odgd7xXLsSTiiSnch62dl0Lfi9D0x2ByxvsGOCbovmBl2ZZ0zHr1wpc4N0XS9lMUq5RJQbonDibxXG4nC2zroDfvD0h7i-L8KMXeJb9pYwW7LkmrM_YwYfJnWnZ4bpcsDjojmPeUBlACg7tjjOgBFbyQZvUtaERJwSRlaWibvNjof7eCVfZChE0PwBpZc_cGqSqKXv544L4ttqdCnmONjqrTATXwC4gYxruevkjHfYI5ojcQmXoWDJJ0-_jzfyPE4MFFdCFgzLgnfIOwe5ve0MtquKuv2O0pgvg";
71+
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3MjdiNmI0OTQwMmI5Y2Y5NWJlNGU4ZmQzOGFhN2U3YzExNjQ0YjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Nsb3VkdGFza3MuZ29vZ2xlYXBpcy5jb20vdjIvcHJvamVjdHMvZ2Nsb3VkLWRldmVsL2xvY2F0aW9ucyIsImF6cCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNjYwODgwNjczLCJpYXQiOjE2NjA4NzcwNzMsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInN1YiI6IjExMjgxMDY3Mjk2MzcyODM2NjQwNiJ9.Q2tG-hN6UHecbzaCIlg58K9msp58nLZWs03CBGO_D6F3cI4LKQEUzsbcztZqmNGWd0ld4zkrKzIP9cQosa_xold4hEzSX_ORRHYQLimLYaQmP3rKqWPMsbIupPdpnGqBDzAYjc7Pw9pQBzuZJj8e3FEG6a5tblDfMcgeklXZIkwzN7ypWCbFDoDP2STSYJYZ-LQIB0-Zlex7dm2KhyB8QSkMQK60YvpXz4L1OtwG7spk3yUCWxul6hYF76klST0iS6DH03YdaDpt4gRXkTUKyTRfB10h-WhCAKKRzmT6d_IT9ApIyqPhimkgkBHhLNyjK8lgAJdk9CLriSEOgVpsow";
72+
private static final String SERVICE_ACCOUNT_RS256_TOKEN_BAD_SIGNATURE =
73+
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3MjdiNmI0OTQwMmI5Y2Y5NWJlNGU4ZmQzOGFhN2U3YzExNjQ0YjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Nsb3VkdGFza3MuZ29vZ2xlYXBpcy5jb20vdjIvcHJvamVjdHMvZ2Nsb3VkLWRldmVsL2xvY2F0aW9ucyIsImF6cCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6InN0aW0tdGVzdEBzdGVsbGFyLWRheS0yNTQyMjIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNjYwODgwNjczLCJpYXQiOjE2NjA4NzcwNzMsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInN1YiI6IjExMjgxMDY3Mjk2MzcyODM2NjQwNiJ9.Q2tG-hN6UHecbzaCIlg58K9msp58nLZWs03CBGO_D6F3cI4LKQEUzsbcztZqmNGWd0ld4zkrKzIP9cQosa_xold4hEzSX_ORRHYQLimLYaQmP3rKqWPMsbIupPdpnGqBDzAYjc7Pw9pQBzuZJj8e3FEG6a5tblDfMcgeklXZIkwzN7ypWCbFDoDP2STSYJYZ-LQIB0-Zlex7dm2KhyB8QSkMQK60YvpXz4L1OtwG7spk3yUCWxul6hYF76klST0iS6DH03YdaDpt4gRXkTUKyTRfB10h-WhCAKKRzmT6d_IT9ApIyqPhimkgkBHhLNyjK8lgAJdk9CLriSEOgVpruy";
7274
private static final String SERVICE_ACCOUNT_CERT_URL =
73-
"https://www.googleapis.com/robot/v1/metadata/x509/integration-tests%40chingor-test.iam.gserviceaccount.com";
75+
"https://www.googleapis.com/oauth2/v3/certs";
7476

7577
private static final List<String> ALL_TOKENS =
7678
Arrays.asList(ES256_TOKEN, FEDERATED_SIGNON_RS256_TOKEN, SERVICE_ACCOUNT_RS256_TOKEN);
@@ -184,7 +186,7 @@ public void testBuilderSetNullIssuers() throws Exception {
184186
assertNull(verifier.getIssuer());
185187
}
186188

187-
public void testMissingAudience() throws VerificationException {
189+
public void testMissingAudience() throws IOException {
188190
IdToken idToken = newIdToken(ISSUER, null);
189191

190192
MockClock clock = new MockClock();
@@ -198,7 +200,7 @@ public void testMissingAudience() throws VerificationException {
198200
assertFalse(verifier.verify(idToken));
199201
}
200202

201-
public void testVerifyEs256TokenPublicKeyMismatch() throws Exception {
203+
public void testPublicKeyStoreIntermittentError() throws Exception {
202204
// Mock HTTP requests
203205
MockLowLevelHttpRequest failedRequest =
204206
new MockLowLevelHttpRequest() {
@@ -245,7 +247,7 @@ public LowLevelHttpResponse execute() throws IOException {
245247
};
246248

247249
HttpTransportFactory httpTransportFactory =
248-
mockTransport(failedRequest, badRequest, emptyRequest, goodRequest);
250+
mockTransport(failedRequest, badRequest, badRequest, badRequest, emptyRequest, goodRequest);
249251
IdTokenVerifier tokenVerifier =
250252
new IdTokenVerifier.Builder()
251253
.setClock(FIXED_CLOCK)
@@ -255,28 +257,28 @@ public LowLevelHttpResponse execute() throws IOException {
255257
try {
256258
tokenVerifier.verifySignature(IdToken.parse(JSON_FACTORY, ES256_TOKEN));
257259
fail("Should have failed verification");
258-
} catch (VerificationException ex) {
260+
} catch (IOException ex) {
259261
assertTrue(ex.getMessage().contains("Error fetching public key"));
260262
}
261263

262264
try {
263265
tokenVerifier.verifySignature(IdToken.parse(JSON_FACTORY, ES256_TOKEN));
264266
fail("Should have failed verification");
265-
} catch (VerificationException ex) {
267+
} catch (IOException ex) {
266268
assertTrue(ex.getMessage().contains("Error fetching public key"));
267269
}
268270

269271
try {
270272
tokenVerifier.verifySignature(IdToken.parse(JSON_FACTORY, ES256_TOKEN));
271273
fail("Should have failed verification");
272-
} catch (VerificationException ex) {
274+
} catch (IOException ex) {
273275
assertTrue(ex.getCause().getMessage().contains("No valid public key returned"));
274276
}
275277

276278
Assert.assertTrue(tokenVerifier.verifySignature(IdToken.parse(JSON_FACTORY, ES256_TOKEN)));
277279
}
278280

279-
public void testVerifyEs256Token() throws VerificationException, IOException {
281+
public void testVerifyEs256Token() throws IOException {
280282
HttpTransportFactory httpTransportFactory =
281283
mockTransport(
282284
"https://www.gstatic.com/iap/verify/public_key-jwk",
@@ -289,7 +291,7 @@ public void testVerifyEs256Token() throws VerificationException, IOException {
289291
assertTrue(tokenVerifier.verify(IdToken.parse(JSON_FACTORY, ES256_TOKEN)));
290292
}
291293

292-
public void testVerifyRs256Token() throws VerificationException, IOException {
294+
public void testVerifyRs256Token() throws IOException {
293295
HttpTransportFactory httpTransportFactory =
294296
mockTransport(
295297
"https://www.googleapis.com/oauth2/v3/certs",
@@ -304,7 +306,7 @@ public void testVerifyRs256Token() throws VerificationException, IOException {
304306
}
305307

306308
public void testVerifyRs256TokenWithLegacyCertificateUrlFormat()
307-
throws VerificationException, IOException {
309+
throws IOException, VerificationException {
308310
HttpTransportFactory httpTransportFactory =
309311
mockTransport(
310312
LEGACY_FEDERATED_SIGNON_CERT_URL, readResourceAsString("legacy_federated_keys.json"));
@@ -318,15 +320,23 @@ public void testVerifyRs256TokenWithLegacyCertificateUrlFormat()
318320
assertTrue(tokenVerifier.verify(IdToken.parse(JSON_FACTORY, FEDERATED_SIGNON_RS256_TOKEN)));
319321
}
320322

321-
public void testVerifyServiceAccountRs256Token() throws VerificationException, IOException {
322-
MockClock clock = new MockClock(1587626643000L);
323+
public void testVerifyServiceAccountRs256Token() throws IOException {
324+
MockClock clock = new MockClock(1660880973000L);
323325
IdTokenVerifier tokenVerifier =
324326
new IdTokenVerifier.Builder()
325327
.setClock(clock)
326328
.setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL)
327329
.setHttpTransportFactory(new DefaultHttpTransportFactory())
328330
.build();
329331
assertTrue(tokenVerifier.verify(IdToken.parse(JSON_FACTORY, SERVICE_ACCOUNT_RS256_TOKEN)));
332+
333+
// a token with a bad signature that is expected to fail in verify, but work in verifyPayload
334+
assertFalse(
335+
tokenVerifier.verify(
336+
IdToken.parse(JSON_FACTORY, SERVICE_ACCOUNT_RS256_TOKEN_BAD_SIGNATURE)));
337+
assertTrue(
338+
tokenVerifier.verifyPayload(
339+
IdToken.parse(JSON_FACTORY, SERVICE_ACCOUNT_RS256_TOKEN_BAD_SIGNATURE)));
330340
}
331341

332342
static String readResourceAsString(String resourceName) throws IOException {

0 commit comments

Comments
 (0)