From 22419d60579ef4c1a8a256a90e6ca7bc58f09aa1 Mon Sep 17 00:00:00 2001 From: Timur Sadykov Date: Wed, 13 Apr 2022 11:21:55 -0700 Subject: [PATCH] fix: add signature verification to IdTokenVerifier (#861) * previously missing signature validation ported from google-auth-library-java * test cases Co-authored-by: Tomo Suzuki --- .gitignore | 7 + google-oauth-client/pom.xml | 1 - .../auth/openidconnect/Environment.java | 38 ++ .../openidconnect/HttpTransportFactory.java | 50 +++ .../auth/openidconnect/IdTokenVerifier.java | 351 +++++++++++++++++- .../openidconnect/IdTokenVerifierTest.java | 224 ++++++++++- .../resources/aws_security_credentials.json | 9 + .../src/test/resources/client_secret.json | 16 + .../src/test/resources/federated_keys.json | 20 + .../src/test/resources/iap_keys.json | 49 +++ .../test/resources/legacy_federated_keys.json | 4 + .../test/resources/service_account_keys.json | 4 + 12 files changed, 753 insertions(+), 20 deletions(-) create mode 100644 google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/Environment.java create mode 100644 google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/HttpTransportFactory.java create mode 100644 google-oauth-client/src/test/resources/aws_security_credentials.json create mode 100644 google-oauth-client/src/test/resources/client_secret.json create mode 100644 google-oauth-client/src/test/resources/federated_keys.json create mode 100644 google-oauth-client/src/test/resources/iap_keys.json create mode 100644 google-oauth-client/src/test/resources/legacy_federated_keys.json create mode 100644 google-oauth-client/src/test/resources/service_account_keys.json diff --git a/.gitignore b/.gitignore index 967c2ef30..bbc39b84f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,11 @@ bin/ .idea/ *.iml .checkstyle +.factorypath + +# MacOS +.DS_Store + +# VS Code +.vscode/ diff --git a/google-oauth-client/pom.xml b/google-oauth-client/pom.xml index 5f9fd8fa6..1e4868f5c 100644 --- a/google-oauth-client/pom.xml +++ b/google-oauth-client/pom.xml @@ -82,7 +82,6 @@ com.google.http-client google-http-client-gson - test junit diff --git a/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/Environment.java b/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/Environment.java new file mode 100644 index 000000000..9b9e17816 --- /dev/null +++ b/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/Environment.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.client.auth.openidconnect; + +class Environment { + public String getVariable(String name) { + return System.getenv(name); + } +} diff --git a/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/HttpTransportFactory.java b/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/HttpTransportFactory.java new file mode 100644 index 000000000..4ec046d60 --- /dev/null +++ b/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/HttpTransportFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.client.auth.openidconnect; + +import com.google.api.client.http.HttpTransport; + +/** + * A base interface for all {@link HttpTransport} factories. + * + *

Implementation must provide a public no-arg constructor. Loading of a factory implementation + * is done via {@link java.util.ServiceLoader}. + */ +public interface HttpTransportFactory { + + /** + * Creates a {@code HttpTransport} instance. + * + * @return The HttpTransport instance. + */ + HttpTransport create(); +} diff --git a/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/IdTokenVerifier.java b/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/IdTokenVerifier.java index 6f97418b2..9f3c446ad 100644 --- a/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/IdTokenVerifier.java +++ b/google-oauth-client/src/main/java/com/google/api/client/auth/openidconnect/IdTokenVerifier.java @@ -14,11 +14,53 @@ package com.google.api.client.auth.openidconnect; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.json.webtoken.JsonWebSignature.Header; +import com.google.api.client.util.Base64; import com.google.api.client.util.Beta; import com.google.api.client.util.Clock; +import com.google.api.client.util.Key; import com.google.api.client.util.Preconditions; +import com.google.common.annotations.VisibleForTesting; +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.Collection; import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; /** * {@link Beta}
@@ -39,6 +81,25 @@ * if (!verifier.verify(idToken)) {...} * * + * The verifier validates 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} Either or both certificate location and + * transport implementation can be overridden via {@link Builder} + * + *

+ * IdTokenVerifier verifier = new IdTokenVerifier.Builder()
+ * .setIssuer("issuer.example.com")
+ * .setAudience(Arrays.asList("myClientId"))
+ * .setHttpTransportFactory(customHttpTransportFactory)
+ * .build();
+ * ...
+ * if (!verifier.verify(idToken)) {...}
+ * 
+ * + * not recommended: this check can be disabled with OAUTH_CLIENT_SKIP_SIGNATURE environment variable + * set to true. + * *

Note that {@link #verify(IdToken)} only implements a subset of the verification steps, mostly * just the MUST steps. Please read SUPPORTED_ALGORITHMS = ImmutableSet.of("RS256", "ES256"); + private static final String NOT_SUPPORTED_ALGORITHM = + "Unexpected signing algorithm %s: expected either RS256 or ES256"; + + static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); + static final String SKIP_SIGNATURE_ENV_VAR = "OAUTH_CLIENT_SKIP_SIGNATURE"; /** Default value for seconds of time skew to accept when verifying time (5 minutes). */ public static final long DEFAULT_TIME_SKEW_SECONDS = 300; /** Clock to use for expiration checks. */ private final Clock clock; + private final String certificatesLocation; + private final Environment environment; + private final LoadingCache> publicKeyCache; + /** Seconds of time skew to accept when verifying time. */ private final long acceptableTimeSkewSeconds; @@ -76,11 +150,21 @@ public IdTokenVerifier() { /** @param builder builder */ protected IdTokenVerifier(Builder builder) { + this.certificatesLocation = builder.certificatesLocation; clock = builder.clock; acceptableTimeSkewSeconds = builder.acceptableTimeSkewSeconds; issuers = builder.issuers == null ? null : Collections.unmodifiableCollection(builder.issuers); audience = builder.audience == null ? null : Collections.unmodifiableCollection(builder.audience); + HttpTransportFactory transport = + builder.httpTransportFactory == null + ? new DefaultHttpTransportFactory() + : builder.httpTransportFactory; + this.publicKeyCache = + CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(new PublicKeyLoader(transport)); + this.environment = builder.environment == null ? new Environment() : builder.environment; } /** Returns the clock. */ @@ -134,6 +218,13 @@ public final Collection getAudience() { *

  • 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. @@ -142,9 +233,74 @@ public final Collection getAudience() { * @return {@code true} if verified successfully or {@code false} if failed */ public boolean verify(IdToken idToken) { - return (issuers == null || idToken.verifyIssuer(issuers)) - && (audience == null || idToken.verifyAudience(audience)) - && idToken.verifyTime(clock.currentTimeMillis(), acceptableTimeSkewSeconds); + boolean tokenFieldsValid = + (issuers == null || idToken.verifyIssuer(issuers)) + && (audience == null || idToken.verifyAudience(audience)) + && idToken.verifyTime(clock.currentTimeMillis(), acceptableTimeSkewSeconds); + + if (!tokenFieldsValid) { + return false; + } + + 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); + return false; + } + } + + @VisibleForTesting + boolean verifySignature(IdToken idToken) throws 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 VerificationException( + "Error fetching PublicKey from certificate location " + certificatesLocation, e); + } + + if (publicKeyToUse == null) { + throw new VerificationException( + "Could not find PublicKey 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())); } /** @@ -161,6 +317,11 @@ 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; @@ -170,6 +331,8 @@ public static class Builder { /** 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); @@ -216,6 +379,18 @@ public Builder setIssuer(String issuer) { } } + /** + * 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; + } + /** * Returns the equivalent expected issuers or {@code null} if issuer check suppressed. * @@ -280,5 +455,173 @@ public Builder setAcceptableTimeSkewSeconds(long acceptableTimeSkewSeconds) { 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 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()); + 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); + 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) { + LOGGER.log(Level.WARNING, "Failed to put a key into the cache", ignored); + } + } + } + + 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 { + 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; + } } } 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 9db0e982a..cf32e8ee8 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 @@ -15,12 +15,29 @@ package com.google.api.client.auth.openidconnect; import com.google.api.client.auth.openidconnect.IdToken.Payload; +import com.google.api.client.auth.openidconnect.IdTokenVerifier.VerificationException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature.Header; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.client.util.Clock; import com.google.api.client.util.Lists; +import com.google.common.io.CharStreams; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import junit.framework.TestCase; /** @@ -39,6 +56,25 @@ public class IdTokenVerifierTest extends TestCase { private static final String ISSUER2 = ISSUER + "2"; private static final String ISSUER3 = ISSUER + "3"; + private static final String ES256_TOKEN = + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ.yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA"; + + private static final String FEDERATED_SIGNON_RS256_TOKEN = + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImY5ZDk3YjRjYWU5MGJjZDc2YWViMjAwMjZmNmI3NzBjYWMyMjE3ODMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tL3BhdGgiLCJhenAiOiJpbnRlZ3JhdGlvbi10ZXN0c0BjaGluZ29yLXRlc3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6ImludGVncmF0aW9uLXRlc3RzQGNoaW5nb3ItdGVzdC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1ODc2Mjk4ODgsImlhdCI6MTU4NzYyNjI4OCwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTA0MDI5MjkyODUzMDk5OTc4MjkzIn0.Pj4KsJh7riU7ZIbPMcHcHWhasWEcbVjGP4yx_5E0iOpeDalTdri97E-o0dSSkuVX2FeBIgGUg_TNNgJ3YY97T737jT5DUYwdv6M51dDlLmmNqlu_P6toGCSRC8-Beu5gGmqS2Y82TmpHH9Vhoh5PsK7_rVHk8U6VrrVVKKTWm_IzTFhqX1oYKPdvfyaNLsXPbCt_NFE0C3DNmFkgVhRJu7LtzQQN-ghaqd3Ga3i6KH222OEI_PU4BUTvEiNOqRGoMlT_YOsyFN3XwqQ6jQGWhhkArL1z3CG2BVQjHTKpgVsRyy_H6WTZiju2Q-XWobgH-UPSZbyymV8-cFT9XKEtZQ"; + private static final String LEGACY_FEDERATED_SIGNON_CERT_URL = + "https://www.googleapis.com/oauth2/v1/certs"; + + private static final String SERVICE_ACCOUNT_RS256_TOKEN = + "eyJhbGciOiJSUzI1NiIsImtpZCI6IjJlZjc3YjM4YTFiMDM3MDQ4NzA0MzkxNmFjYmYyN2Q3NGVkZDA4YjEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tL2F1ZGllbmNlIiwiZXhwIjoxNTg3NjMwNTQzLCJpYXQiOjE1ODc2MjY5NDMsImlzcyI6InNvbWUgaXNzdWVyIiwic3ViIjoic29tZSBzdWJqZWN0In0.gGOQW0qQgs4jGUmCsgRV83RqsJLaEy89-ZOG6p1u0Y26FyY06b6Odgd7xXLsSTiiSnch62dl0Lfi9D0x2ByxvsGOCbovmBl2ZZ0zHr1wpc4N0XS9lMUq5RJQbonDibxXG4nC2zroDfvD0h7i-L8KMXeJb9pYwW7LkmrM_YwYfJnWnZ4bpcsDjojmPeUBlACg7tjjOgBFbyQZvUtaERJwSRlaWibvNjof7eCVfZChE0PwBpZc_cGqSqKXv544L4ttqdCnmONjqrTATXwC4gYxruevkjHfYI5ojcQmXoWDJJ0-_jzfyPE4MFFdCFgzLgnfIOwe5ve0MtquKuv2O0pgvg"; + private static final String SERVICE_ACCOUNT_CERT_URL = + "https://www.googleapis.com/robot/v1/metadata/x509/integration-tests%40chingor-test.iam.gserviceaccount.com"; + + private static final List ALL_TOKENS = + Arrays.asList(ES256_TOKEN, FEDERATED_SIGNON_RS256_TOKEN, SERVICE_ACCOUNT_RS256_TOKEN); + + static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); + static final MockClock FIXED_CLOCK = new MockClock(1584047020000L); + private static IdToken newIdToken(String issuer, String audience) { Payload payload = new Payload(); payload.setIssuer(issuer); @@ -54,8 +90,8 @@ public void testBuilder() throws Exception { assertEquals(Clock.SYSTEM, builder.getClock()); assertEquals(ISSUER, builder.getIssuer()); assertEquals(Collections.singleton(ISSUER), builder.getIssuers()); - assertTrue(TRUSTED_CLIENT_IDS.equals(builder.getAudience())); - Clock clock = new MyClock(); + assertEquals(TRUSTED_CLIENT_IDS, builder.getAudience()); + Clock clock = new MockClock(); builder.setClock(clock); assertEquals(clock, builder.getClock()); IdTokenVerifier verifier = builder.build(); @@ -65,25 +101,22 @@ public void testBuilder() throws Exception { assertEquals(TRUSTED_CLIENT_IDS, Lists.newArrayList(verifier.getAudience())); } - static class MyClock implements Clock { - - long timeMillis; - - public long currentTimeMillis() { - return timeMillis; - } - } - public void testVerify() throws Exception { - MyClock clock = new MyClock(); + MockClock clock = new MockClock(); + MockEnvironment testEnvironment = new MockEnvironment(); + testEnvironment.setVariable(IdTokenVerifier.SKIP_SIGNATURE_ENV_VAR, "true"); IdTokenVerifier verifier = new IdTokenVerifier.Builder() .setIssuers(Arrays.asList(ISSUER, ISSUER3)) .setAudience(Arrays.asList(CLIENT_ID)) .setClock(clock) + .setEnvironment(testEnvironment) .build(); + // verifier flexible doesn't check issuer and audience - IdTokenVerifier verifierFlexible = new IdTokenVerifier.Builder().setClock(clock).build(); + IdTokenVerifier verifierFlexible = + new IdTokenVerifier.Builder().setClock(clock).setEnvironment(testEnvironment).build(); + // issuer clock.timeMillis = 1500000L; IdToken idToken = newIdToken(ISSUER, CLIENT_ID); @@ -138,10 +171,10 @@ public void testBuilderSetNullIssuers() throws Exception { assertNull(verifier.getIssuer()); } - public void testMissingAudience() { + public void testMissingAudience() throws VerificationException { IdToken idToken = newIdToken(ISSUER, null); - MyClock clock = new MyClock(); + MockClock clock = new MockClock(); clock.timeMillis = 1500000L; IdTokenVerifier verifier = new IdTokenVerifier.Builder() @@ -151,4 +184,165 @@ public void testMissingAudience() { .build(); assertFalse(verifier.verify(idToken)); } + + public void testVerifyEs256TokenPublicKeyMismatch() throws Exception { + // Mock HTTP requests + HttpTransportFactory httpTransportFactory = + new HttpTransportFactory() { + @Override + public HttpTransport create() { + return new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) + throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + response.setContentType("application/json"); + response.setContent(""); + return response; + } + }; + } + }; + } + }; + IdTokenVerifier tokenVerifier = + new IdTokenVerifier.Builder() + .setClock(FIXED_CLOCK) + .setHttpTransportFactory(httpTransportFactory) + .build(); + + try { + tokenVerifier.verifySignature(IdToken.parse(JSON_FACTORY, ES256_TOKEN)); + fail("Should have failed verification"); + } catch (VerificationException ex) { + assertTrue(ex.getMessage().contains("Error fetching PublicKey")); + } + } + + public void testVerifyEs256Token() throws VerificationException, IOException { + HttpTransportFactory httpTransportFactory = + mockTransport( + "https://www.gstatic.com/iap/verify/public_key-jwk", + readResourceAsString("iap_keys.json")); + IdTokenVerifier tokenVerifier = + new IdTokenVerifier.Builder() + .setClock(FIXED_CLOCK) + .setHttpTransportFactory(httpTransportFactory) + .build(); + assertTrue(tokenVerifier.verify(IdToken.parse(JSON_FACTORY, ES256_TOKEN))); + } + + public void testVerifyRs256Token() throws VerificationException, IOException { + HttpTransportFactory httpTransportFactory = + mockTransport( + "https://www.googleapis.com/oauth2/v3/certs", + readResourceAsString("federated_keys.json")); + MockClock clock = new MockClock(1587625988000L); + IdTokenVerifier tokenVerifier = + new IdTokenVerifier.Builder() + .setClock(clock) + .setHttpTransportFactory(httpTransportFactory) + .build(); + assertTrue(tokenVerifier.verify(IdToken.parse(JSON_FACTORY, FEDERATED_SIGNON_RS256_TOKEN))); + } + + public void testVerifyRs256TokenWithLegacyCertificateUrlFormat() + throws VerificationException, IOException { + HttpTransportFactory httpTransportFactory = + mockTransport( + LEGACY_FEDERATED_SIGNON_CERT_URL, readResourceAsString("legacy_federated_keys.json")); + MockClock clock = new MockClock(1587626288000L); + IdTokenVerifier tokenVerifier = + new IdTokenVerifier.Builder() + .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) + .setClock(clock) + .setHttpTransportFactory(httpTransportFactory) + .build(); + 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(); + assertTrue(tokenVerifier.verify(IdToken.parse(JSON_FACTORY, SERVICE_ACCOUNT_RS256_TOKEN))); + } + + static String readResourceAsString(String resourceName) throws IOException { + InputStream inputStream = + IdTokenVerifierTest.class.getClassLoader().getResourceAsStream(resourceName); + try (final Reader reader = new InputStreamReader(inputStream)) { + return CharStreams.toString(reader); + } + } + + static HttpTransportFactory mockTransport(String url, String certificates) { + final String certificatesContent = certificates; + final String certificatesUrl = url; + return new HttpTransportFactory() { + @Override + public HttpTransport create() { + return new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + assertEquals(certificatesUrl, url); + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + response.setContentType("application/json"); + response.setContent(certificatesContent); + return response; + } + }; + } + }; + } + }; + } + + /** A mock implementation of {@link Clock} to set clock for testing */ + static class MockClock implements Clock { + public MockClock() {} + + public MockClock(long timeMillis) { + this.timeMillis = timeMillis; + } + + long timeMillis; + + public long currentTimeMillis() { + return timeMillis; + } + } + + /** A default http transport factory for testing */ + static class DefaultHttpTransportFactory implements HttpTransportFactory { + public HttpTransport create() { + return new NetHttpTransport(); + } + } + + /** A mock implementation of {@link Environment} to set environment variables for testing */ + class MockEnvironment extends Environment { + private final Map variables = new HashMap<>(); + + @Override + public String getVariable(String name) { + return variables.get(name); + } + + public void setVariable(String name, String value) { + variables.put(name, value); + } + } } diff --git a/google-oauth-client/src/test/resources/aws_security_credentials.json b/google-oauth-client/src/test/resources/aws_security_credentials.json new file mode 100644 index 000000000..76e7688a3 --- /dev/null +++ b/google-oauth-client/src/test/resources/aws_security_credentials.json @@ -0,0 +1,9 @@ +{ + "Code" : "Success", + "LastUpdated" : "2020-08-11T19:33:07Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "ASIARD4OQDT6A77FR3CL", + "SecretAccessKey" : "Y8AfSaucF37G4PpvfguKZ3/l7Id4uocLXxX0+VTx", + "Token" : "IQoJb3JpZ2luX2VjEIz//////////wEaCXVzLWVhc3QtMiJGMEQCIH7MHX/Oy/OB8OlLQa9GrqU1B914+iMikqWQW7vPCKlgAiA/Lsv8Jcafn14owfxXn95FURZNKaaphj0ykpmS+Ki+CSq0AwhlEAAaDDA3NzA3MTM5MTk5NiIMx9sAeP1ovlMTMKLjKpEDwuJQg41/QUKx0laTZYjPlQvjwSqS3OB9P1KAXPWSLkliVMMqaHqelvMF/WO/glv3KwuTfQsavRNs3v5pcSEm4SPO3l7mCs7KrQUHwGP0neZhIKxEXy+Ls//1C/Bqt53NL+LSbaGv6RPHaX82laz2qElphg95aVLdYgIFY6JWV5fzyjgnhz0DQmy62/Vi8pNcM2/VnxeCQ8CC8dRDSt52ry2v+nc77vstuI9xV5k8mPtnaPoJDRANh0bjwY5Sdwkbp+mGRUJBAQRlNgHUJusefXQgVKBCiyJY4w3Csd8Bgj9IyDV+Azuy1jQqfFZWgP68LSz5bURyIjlWDQunO82stZ0BgplKKAa/KJHBPCp8Qi6i99uy7qh76FQAqgVTsnDuU6fGpHDcsDSGoCls2HgZjZFPeOj8mmRhFk1Xqvkbjuz8V1cJk54d3gIJvQt8gD2D6yJQZecnuGWd5K2e2HohvCc8Fc9kBl1300nUJPV+k4tr/A5R/0QfEKOZL1/k5lf1g9CREnrM8LVkGxCgdYMxLQow1uTL+QU67AHRRSp5PhhGX4Rek+01vdYSnJCMaPhSEgcLqDlQkhk6MPsyT91QMXcWmyO+cAZwUPwnRamFepuP4K8k2KVXs/LIJHLELwAZ0ekyaS7CptgOqS7uaSTFG3U+vzFZLEnGvWQ7y9IPNQZ+Dffgh4p3vF4J68y9049sI6Sr5d5wbKkcbm8hdCDHZcv4lnqohquPirLiFQ3q7B17V9krMPu3mz1cg4Ekgcrn/E09NTsxAqD8NcZ7C7ECom9r+X3zkDOxaajW6hu3Az8hGlyylDaMiFfRbBJpTIlxp7jfa7CxikNgNtEKLH9iCzvuSg2vhA==", + "Expiration" : "2020-08-11T07:35:49Z" +} \ No newline at end of file diff --git a/google-oauth-client/src/test/resources/client_secret.json b/google-oauth-client/src/test/resources/client_secret.json new file mode 100644 index 000000000..66fae0a6e --- /dev/null +++ b/google-oauth-client/src/test/resources/client_secret.json @@ -0,0 +1,16 @@ +{ + "web": { + "client_id":"ya29.1.AADtN_UtlxN3PuGAxrN2XQnZTVRvDyVWnYq4I6dws", + "auth_uri":"https://accounts.google.com/o/oauth2/auth", + "token_uri":"https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs", + "client_secret":"jakuaL9YyieakhECKL2SwZcu", + "redirect_uris":[ + "http://example.appspot.com/oauth2callback", + "http://localhost:8080/oauth2callback" + ], + "javascript_origins":[ + "https://www.example.com" + ] + } +} diff --git a/google-oauth-client/src/test/resources/federated_keys.json b/google-oauth-client/src/test/resources/federated_keys.json new file mode 100644 index 000000000..9986a8e0f --- /dev/null +++ b/google-oauth-client/src/test/resources/federated_keys.json @@ -0,0 +1,20 @@ +{ + "keys": [ + { + "kid": "f9d97b4cae90bcd76aeb20026f6b770cac221783", + "e": "AQAB", + "kty": "RSA", + "alg": "RS256", + "n": "ya_7gVJrvqFp5xfYPOco8gBLY38kQDlTlT6ueHtUtbTkRVE1X5tFmPqChnX7wWd2fK7MS4-nclYaGLL7IvJtN9tjrD0h_3_HvnrRZTaVyS-yfWqCQDRq_0VW1LBEygwYRqbO2T0lOocTY-5qUosDvJfe-o-lQYMH7qtDAyiq9XprVzKYTfS545BTECXi0he9ikJl5Q_RAP1BZoaip8F0xX5Y_60G90VyXFWuy16nm5ASW8fwqzdn1lL_ogiO1LirgBFFEXz_t4PwmjWzfQwkoKv4Ab_l9u2FdAoKtFH2CwKaGB8hatIK3bOAJJgRebeU3w6Ah3gxRfi8HWPHbAGjtw", + "use": "sig" + }, + { + "kid": "28b741e8de984a47159f19e6d7783e9d4fa810db", + "e": "AQAB", + "kty": "RSA", + "alg": "RS256", + "n": "zc4ELn-9nLzCZb4PdXGVhtUtzwmQI8HZH8tOIEg9omx6CW-PZ5xtVQ5O5EBG2AA5_K-aOWvVEWyfeHe8WwZltM1cXu6QNdXbpVVYeZ0th9hm7ZflNz7h1PMM9lNXLJjokax5gxGskc8CsjhkwurEot1TD2zbGIQsOYoebQTvJ2AYxIjk77BU20nLplurge8jrK-V1G3zJlp0xIKqxjsfIFYm1Mp-HQhJzdMbjNEScs0dDT4rPxdA-wOVGix0wrPdIE1gM4GxZ7AlSZ7IcjuYMZIe6d6oAeKG0FG0avbtipAQglxTHM3UOge6PmThr_mmiI82oLqGutul-XYgy1S2NQ", + "use": "sig" + } + ] +} \ No newline at end of file diff --git a/google-oauth-client/src/test/resources/iap_keys.json b/google-oauth-client/src/test/resources/iap_keys.json new file mode 100644 index 000000000..2ba2bfa01 --- /dev/null +++ b/google-oauth-client/src/test/resources/iap_keys.json @@ -0,0 +1,49 @@ +{ + "keys" : [ + { + "alg" : "ES256", + "crv" : "P-256", + "kid" : "2nMJtw", + "kty" : "EC", + "use" : "sig", + "x" : "9e1x7YRZg53A5zIJ0p2ZQ9yTrgPLGIf4ntOk-4O2R28", + "y" : "q8iDm7nsnpz1xPdrWBtTZSowzciS3O7bMYtFFJ8saYo" + }, + { + "alg" : "ES256", + "crv" : "P-256", + "kid" : "LYyP2g", + "kty" : "EC", + "use" : "sig", + "x" : "SlXFFkJ3JxMsXyXNrqzE3ozl_0913PmNbccLLWfeQFU", + "y" : "GLSahrZfBErmMUcHP0MGaeVnJdBwquhrhQ8eP05NfCI" + }, + { + "alg" : "ES256", + "crv" : "P-256", + "kid" : "mpf0DA", + "kty" : "EC", + "use" : "sig", + "x" : "fHEdeT3a6KaC1kbwov73ZwB_SiUHEyKQwUUtMCEn0aI", + "y" : "QWOjwPhInNuPlqjxLQyhveXpWqOFcQPhZ3t-koMNbZI" + }, + { + "alg" : "ES256", + "crv" : "P-256", + "kid" : "b9vTLA", + "kty" : "EC", + "use" : "sig", + "x" : "qCByTAvci-jRAD7uQSEhTdOs8iA714IbcY2L--YzynI", + "y" : "WQY0uCoQyPSozWKGQ0anmFeOH5JNXiZa9i6SNqOcm7w" + }, + { + "alg" : "ES256", + "crv" : "P-256", + "kid" : "0oeLcQ", + "kty" : "EC", + "use" : "sig", + "x" : "MdhRXGEoGJLtBjQEIjnYLPkeci9rXnca2TffkI0Kac0", + "y" : "9BoREHfX7g5OK8ELpA_4RcOnFCGSjfR4SGZpBo7juEY" + } + ] +} \ No newline at end of file diff --git a/google-oauth-client/src/test/resources/legacy_federated_keys.json b/google-oauth-client/src/test/resources/legacy_federated_keys.json new file mode 100644 index 000000000..3a5748399 --- /dev/null +++ b/google-oauth-client/src/test/resources/legacy_federated_keys.json @@ -0,0 +1,4 @@ +{ + "f9d97b4cae90bcd76aeb20026f6b770cac221783": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIILRTfnfU3e2gwDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0yMDA0MTQwNDI5MzBaFw0yMDA0MzAxNjQ0MzBaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJr/uBUmu+oWnnF9g85yjyAEtjfyRAOVOV\nPq54e1S1tORFUTVfm0WY+oKGdfvBZ3Z8rsxLj6dyVhoYsvsi8m0322OsPSH/f8e+\netFlNpXJL7J9aoJANGr/RVbUsETKDBhGps7ZPSU6hxNj7mpSiwO8l976j6VBgwfu\nq0MDKKr1emtXMphN9LnjkFMQJeLSF72KQmXlD9EA/UFmhqKnwXTFflj/rQb3RXJc\nVa7LXqebkBJbx/CrN2fWUv+iCI7UuKuAEUURfP+3g/CaNbN9DCSgq/gBv+X27YV0\nCgq0UfYLApoYHyFq0grds4AkmBF5t5TfDoCHeDFF+LwdY8dsAaO3AgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQA1Wrx3XsIAAYOaycAkV2mZW1j+Vqxx\nSAeUyuhLoaJ7jntd7LqTuTr+qRnR/fH/CjTbPzngvCyVE6hjClh159YRpf4TJ4aL\nMJ97qDxc/f/pM/7yklIaHHOwqYU10plIyw+m0dnQutPqy1o/aDUytDznNmM6L3v+\ncot2bxyd2PtjGfa1hPNNnEnrZfS2Gc0qqR64RUWbsdLVVQB8MKcaNUqjk9o/1O4p\nNNk2D2VcofdaLPpwSmtzV8wEd4vfzI17qFSPi6gbTfydvxkejk0kdSyWUPw+1YC4\nv2o2rzwXub9hcP2zXyZvTGKPMAkZ8VKuzWuvfuSsTtgcPJ20GpIkin/j\n-----END CERTIFICATE-----\n", + "28b741e8de984a47159f19e6d7783e9d4fa810db": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIcog+uwMaMb8wDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0yMDA0MjIwNDI5MzBaFw0yMDA1MDgxNjQ0MzBaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNzgQuf72cvMJlvg91cZWG1S3PCZAjwdkf\ny04gSD2ibHoJb49nnG1VDk7kQEbYADn8r5o5a9URbJ94d7xbBmW0zVxe7pA11dul\nVVh5nS2H2Gbtl+U3PuHU8wz2U1csmOiRrHmDEayRzwKyOGTC6sSi3VMPbNsYhCw5\nih5tBO8nYBjEiOTvsFTbScumW6uB7yOsr5XUbfMmWnTEgqrGOx8gVibUyn4dCEnN\n0xuM0RJyzR0NPis/F0D7A5UaLHTCs90gTWAzgbFnsCVJnshyO5gxkh7p3qgB4obQ\nUbRq9u2KkBCCXFMczdQ6B7o+ZOGv+aaIjzaguoa626X5diDLVLY1AgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQBEfCN7qgI2GJJAC99PDbafqC1EMBlv\nBT/7UiQdTuDV04+cQH9IpzROW7IZc/ILcqpF6KXUmj6j0sWO+hxKFY66TJKPcypK\n/ZMI58epwwVgGZyYU0BbAIZ9uvOgDfuveMildlMDMg1cJNp7WjBrEJ2DcGfC56wJ\nuKvqB1upxnfh+Ceg3ApU50k6Ld6+dbDDR0Vzt/wGZlZZ5Uj6AwDFe+5p9zEpWg61\nHeny/tSBfgZ19vP2h3ye9ZTK1OFRMNufj8iSzmlkbSqWuy82XVSBRKy5QslqXsYe\nU3gM3EVvXHA/Of3sROFpvznCXNr+Kn03wTv0ny6rnSgHQUzj7p9fydXY\n-----END CERTIFICATE-----\n" +} \ No newline at end of file diff --git a/google-oauth-client/src/test/resources/service_account_keys.json b/google-oauth-client/src/test/resources/service_account_keys.json new file mode 100644 index 000000000..361bb2e4d --- /dev/null +++ b/google-oauth-client/src/test/resources/service_account_keys.json @@ -0,0 +1,4 @@ +{ + "a8611b6a9c0a0a8b940d0f915c326fd1605c8ac6": "-----BEGIN CERTIFICATE-----\nMIIDPDCCAiSgAwIBAgIIFJsPvyc/ZSUwDQYJKoZIhvcNAQEFBQAwQTE/MD0GA1UE\nAxM2aW50ZWdyYXRpb24tdGVzdHMuY2hpbmdvci10ZXN0LmlhbS5nc2VydmljZWFj\nY291bnQuY29tMB4XDTIwMDQwMjIyMjIxN1oXDTIyMDUwMTEzNTYxNVowQTE/MD0G\nA1UEAxM2aW50ZWdyYXRpb24tdGVzdHMuY2hpbmdvci10ZXN0LmlhbS5nc2Vydmlj\nZWFjY291bnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6Yys\nP5LIa1rRxQY93FXIJDzq6Tai4VuetffJbltRtYbdwC5Vyl99O2zoVdRlg+iYXK5B\nb6kidjmWOf0kNimQ5FwYvu+xsm6w8vjL/XShkHEKiURszyCua8wvLeGVCiGBg/XU\nDOgYMjzRIH5fTuj3PTZk4sMj02ZCpCQEMQ6ogpLXjaLp3ZXtFhkuHyCxVYbTRr+k\nGU86JAg4XwD6AdC349v+8FEQD7YtJezUAAKEgXh9e5UeL5CpOo3Vsdv/yEVo00jh\nYuWzLM6Oxt55WAhiD29vKrm7VQPSr1XwwqpdyFL2BlmqyTlb3amwvc9qv2kojGvM\nSUqgS83dc0jFqtMvEQIDAQABozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQE\nAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEA\nm3XUMKOtXdpw0oRjykLHdYzTHHFjQTMmhWD172rsxSzwpkFoErAC7bnmEvpcRU7D\nr4M+pE5VuDJ64J3lEpAh7W0zMXezPtGyWr39hVxL3vp3nh4CbCzzkUCfFvBOFFhm\nOI9qnjtMtaozoGi5zLs5jEaFmgR3wfij9KQjNGZJxAg0ZkwcSNb76qOCG1/vG5au\n4UuoIaq8WqSxMqBF/g+NrAE2PZhjNGnUwFPTre3SyR0otYDzJfmpL/tp5VDie8hM\nL5UZU/CmZk46+T9VbvnZ5mkPAjGiPumiptO5iliBOHPtPdn8VrP+aSQM1btHA094\n1HwfbFp7pZHBUn9COAP/1Q==\n-----END CERTIFICATE-----\n", + "2ef77b38a1b0370487043916acbf27d74edd08b1": "-----BEGIN CERTIFICATE-----\nMIIC+jCCAeKgAwIBAgIIIwRR4+AftjswDQYJKoZIhvcNAQEFBQAwIDEeMBwGA1UE\nAxMVMTA0MDI5MjkyODUzMDk5OTc4MjkzMB4XDTIwMDMwNDA1NTIyMloXDTMwMDMw\nMjA1NTIyMlowIDEeMBwGA1UEAxMVMTA0MDI5MjkyODUzMDk5OTc4MjkzMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm4jAbNdDEDkG/36wP07lFKMTAnWy\nhtV/Vp4QFSIE456moU/HEmBwJX2wocPgvoxPat7FxUv7mwdgHq7+sczis4DrDIIY\n8XfZ+D98+X+rOfkS1WLXpO76REZE4JCUfkB3NKVMP0kfoCFPf2pafz1NJRrZczUw\nbSi/q1+KYHmbk8YS+Q7Iq7gW9dvQtWrsRH8dQIrToJfGH+rbSQyKUFN7skFOflw4\n/OSuT0wvD6z57JcRFtAD3zgeUuCPNRIbkPQC3vCLwWGLKSYWLJ3eM9PPW9bk+czf\nSxJOie7zRMToh4BchLO6ZQgshoEaBHbwdOTu8455skqlRJMU9SKwA6eqVQIDAQAB\nozgwNjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK\nBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOCAQEAXvt8M2GFK+5UHG0GclIqse8j\n+EgqXvDTkbeTxFP+onbA/RwKM+fzdYpDwrH1dQ6jJervmceewUTTMegfFzhF33GC\nxvjQfhs+yVOXQiBHosd93CgR19dMUYs/r1wuUpwqBGdW2S81ns3yreY72BHrikrl\nHNLD3aSJ6hq5CZ01EFpjTW10ndBdPhJRSWD2g8VI1lpd716HEmrXfPHX73KVkk5/\nWfvrMA1UK/Ag+TWQerKG3iQFUAPIUiyepdaG4uFWTBY9nzLPiC1cx3bVPVZ+5yul\nJN15hmAMd3qPgSbbeQ6JC72zXCfW3buBE2n9cGtRbZF1URJZ3NbvwRS5BD425g==\n-----END CERTIFICATE-----\n" +} \ No newline at end of file