From 282bc14699bef426393db45265397570d65541bc Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Fri, 24 Apr 2020 08:57:51 -0700 Subject: [PATCH 01/18] feat: add TokenVerifier class that can verify RS256/ES256 tokens --- .../com/google/auth/oauth2/TokenVerifier.java | 362 ++++++++++++++++++ .../google/auth/oauth2/TokenVerifierTest.java | 124 ++++++ 2 files changed, 486 insertions(+) create mode 100644 oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java new file mode 100644 index 000000000..e97ea77c2 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -0,0 +1,362 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.auth.oauth2; + +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.json.GenericJson; +import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.api.client.util.Base64; +import com.google.api.client.util.Clock; +import com.google.api.client.util.Key; +import com.google.auth.http.HttpTransportFactory; +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 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.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +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.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +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 final String audience; + private final String certificatesLocation; + private final String issuer; + private final PublicKey publicKey; + private final Clock clock; + private final LoadingCache> publicKeyCache; + + public TokenVerifier() { + this(newBuilder()); + } + + 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()); + } + + public static Builder newBuilder() { + return new Builder().setClock(Clock.SYSTEM); + } + + public boolean 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"); + } + + switch (jsonWebSignature.getHeader().getAlgorithm()) { + case "RS256": + return verifyRs256(jsonWebSignature); + case "ES256": + return verifyEs256(jsonWebSignature); + default: + throw new VerificationException( + "Unexpected signing algorithm: expected either RS256 or ES256"); + } + } + + public static class Builder { + private String audience; + private String certificatesLocation; + private String issuer; + private PublicKey publicKey; + private Clock clock; + + public Builder setAudience(String audience) { + this.audience = audience; + return this; + } + + public Builder setCertificatesLocation(String certificatesLocation) { + this.certificatesLocation = certificatesLocation; + return this; + } + + public Builder setIssuer(String issuer) { + this.issuer = issuer; + return this; + } + + public Builder setPublicKey(PublicKey publicKey) { + this.publicKey = publicKey; + return this; + } + + public Builder setClock(Clock clock) { + this.clock = clock; + return this; + } + + public TokenVerifier build() { + return new TokenVerifier(this); + } + } + + static class PublicKeyLoader extends CacheLoader> { + public static class JsonWebKeySet extends GenericJson { + @Key public List keys; + } + + 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; + } + + @Override + public Map load(String certificateUrl) throws Exception { + HttpTransportFactory httpTransportFactory = OAuth2Utils.HTTP_TRANSPORT_FACTORY; + 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); + } + } + + public static class VerificationException extends Exception { + public VerificationException(String message) { + super(message); + } + + public VerificationException(String message, Throwable cause) { + super(message, cause); + } + } + + private boolean verifyEs256(JsonWebSignature jsonWebSignature) throws VerificationException { + String certsUrl = certificatesLocation == null ? IAP_CERT_URL : certificatesLocation; + PublicKey verifyPublicKey = publicKey; + if (verifyPublicKey == null) { + try { + verifyPublicKey = publicKeyCache.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); + } catch (ExecutionException e) { + throw new VerificationException("Error fetching PublicKey for ES256 token", e); + } + } + if (verifyPublicKey == null) { + throw new VerificationException( + "Could not find publicKey for provided keyId: " + + jsonWebSignature.getHeader().getKeyId()); + } + try { + Signature signatureAlgorithm = Signature.getInstance("SHA256withECDSA"); + signatureAlgorithm.initVerify(verifyPublicKey); + signatureAlgorithm.update(jsonWebSignature.getSignedContentBytes()); + byte[] derBytes = convertDerBytes(jsonWebSignature.getSignatureBytes()); + return signatureAlgorithm.verify(derBytes); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new VerificationException("Error validating ES256 token", e); + } + } + + private boolean verifyRs256(JsonWebSignature jsonWebSignature) throws VerificationException { + String certsUrl = + certificatesLocation == null ? FEDERATED_SIGNON_CERT_URL : certificatesLocation; + PublicKey verifyPublicKey = publicKey; + if (verifyPublicKey == null) { + try { + verifyPublicKey = publicKeyCache.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); + } catch (ExecutionException e) { + throw new VerificationException("Error fetching PublicKey for ES256 token", e); + } + } + if (verifyPublicKey == null) { + throw new VerificationException( + "Could not find publicKey for provided keyId: " + + jsonWebSignature.getHeader().getKeyId()); + } + try { + return jsonWebSignature.verifySignature(verifyPublicKey); + } catch (GeneralSecurityException e) { + throw new VerificationException("Error validating RS256 token", e); + } + } + + private static byte DER_TAG_SIGNATURE_OBJECT = 0x30; + private static byte DER_TAG_ASN1_INTEGER = 0x02; + + private static byte[] convertDerBytes(byte[] signature) { + // expect the signature to be 64 bytes long + Preconditions.checkState(signature.length == 64); + + byte[] int1 = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32)).toByteArray(); + byte[] int2 = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64)).toByteArray(); + byte[] der = new byte[6 + int1.length + int2.length]; + + // Mark that this is a signature object + der[0] = DER_TAG_SIGNATURE_OBJECT; + der[1] = (byte) (der.length - 2); + + // Start ASN1 integer and write the first 32 bits + der[2] = DER_TAG_ASN1_INTEGER; + der[3] = (byte) int1.length; + System.arraycopy(int1, 0, der, 4, int1.length); + + // Start ASN1 integer and write the second 32 bits + int offset = int1.length + 4; + der[offset] = DER_TAG_ASN1_INTEGER; + der[offset + 1] = (byte) int2.length; + System.arraycopy(int2, 0, der, offset + 2, int2.length); + + return der; + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java new file mode 100644 index 000000000..9fa2e137c --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.auth.oauth2; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.util.Clock; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +public class TokenVerifierTest { + 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); + + // Fixed to 2020-02-26 08:00:00 to allow expiration tests to pass + private static final Clock FIXED_CLOCK = + new Clock() { + @Override + public long currentTimeMillis() { + return 1582704000000L; + } + }; + + @Test + public void verifyExpiredToken() { + for (String token : ALL_TOKENS) { + TokenVerifier tokenVerifier = new TokenVerifier(); + try { + tokenVerifier.verify(token); + fail("Should have thrown a VerificationException"); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("expired")); + } + } + } + + @Test + public void verifyExpectedAudience() { + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder().setAudience("expected audience").build(); + for (String token : ALL_TOKENS) { + try { + tokenVerifier.verify(token); + fail("Should have thrown a VerificationException"); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("audience does not match")); + } + } + } + + @Test + public void verifyExpectedIssuer() { + TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setIssuer("expected issuer").build(); + for (String token : ALL_TOKENS) { + try { + tokenVerifier.verify(token); + fail("Should have thrown a VerificationException"); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("issuer does not match")); + } + } + } + + @Test + public void verifyEs256Token() throws TokenVerifier.VerificationException { + TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); + assertTrue(tokenVerifier.verify(ES256_TOKEN)); + } + + @Test + public void verifyRs256Token() throws TokenVerifier.VerificationException { + TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); + assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); + } + + @Test + public void verifyRs256TokenWithLegacyCertificateUrlFormat() + throws TokenVerifier.VerificationException { + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder() + .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) + .setClock(FIXED_CLOCK) + .build(); + assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); + } + + @Test + public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationException { + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder() + .setClock(FIXED_CLOCK) + .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) + .build(); + assertTrue(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); + } +} From cc549192b93930bbc9ce3c23f44c64c9c35ef215 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Fri, 24 Apr 2020 10:34:51 -0700 Subject: [PATCH 02/18] test: inject HttpTransportFactory for testing --- .../auth/oauth2/ITTokenVerifierTest.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java new file mode 100644 index 000000000..3bb69d865 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.auth.oauth2; + +import com.google.api.client.util.Clock; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class ITTokenVerifierTest { + 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); + + // Fixed to 2020-02-26 08:00:00 to allow expiration tests to pass + private static final Clock FIXED_CLOCK = + new Clock() { + @Override + public long currentTimeMillis() { + return 1582704000000L; + } + }; + + @Test + public void verifyExpiredToken() { + for (String token : ALL_TOKENS) { + TokenVerifier tokenVerifier = new TokenVerifier(); + try { + tokenVerifier.verify(token); + fail("Should have thrown a VerificationException"); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("expired")); + } + } + } + + @Test + public void verifyExpectedAudience() { + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder().setAudience("expected audience").build(); + for (String token : ALL_TOKENS) { + try { + tokenVerifier.verify(token); + fail("Should have thrown a VerificationException"); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("audience does not match")); + } + } + } + + @Test + public void verifyExpectedIssuer() { + TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setIssuer("expected issuer").build(); + for (String token : ALL_TOKENS) { + try { + tokenVerifier.verify(token); + fail("Should have thrown a VerificationException"); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("issuer does not match")); + } + } + } + + @Test + public void verifyEs256Token() throws TokenVerifier.VerificationException { + TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); + assertTrue(tokenVerifier.verify(ES256_TOKEN)); + } + + @Test + public void verifyRs256Token() throws TokenVerifier.VerificationException { + TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); + assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); + } + + @Test + public void verifyRs256TokenWithLegacyCertificateUrlFormat() + throws TokenVerifier.VerificationException { + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder() + .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) + .setClock(FIXED_CLOCK) + .build(); + assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); + } + + @Test + public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationException { + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder() + .setClock(FIXED_CLOCK) + .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) + .build(); + assertTrue(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); + } +} From b32e57b79f9411a645352f8893e7f93e37413887 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Fri, 24 Apr 2020 10:35:33 -0700 Subject: [PATCH 03/18] test: inject HttpTransportFactory for testing --- .../com/google/auth/oauth2/TokenVerifier.java | 22 ++++- .../auth/oauth2/ITTokenVerifierTest.java | 9 +- .../google/auth/oauth2/TokenVerifierTest.java | 82 +++++++++++++++++++ pom.xml | 78 ++++++++++++++++++ 4 files changed, 183 insertions(+), 8 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index e97ea77c2..f1d2e4208 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -81,11 +81,15 @@ private TokenVerifier(Builder builder) { this.publicKey = builder.publicKey; this.clock = builder.clock; this.publicKeyCache = - CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build(new PublicKeyLoader()); + CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(new PublicKeyLoader(builder.httpTransportFactory)); } public static Builder newBuilder() { - return new Builder().setClock(Clock.SYSTEM); + return new Builder() + .setClock(Clock.SYSTEM) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY); } public boolean verify(String token) throws VerificationException { @@ -128,6 +132,7 @@ public static class Builder { private String issuer; private PublicKey publicKey; private Clock clock; + private HttpTransportFactory httpTransportFactory; public Builder setAudience(String audience) { this.audience = audience; @@ -154,12 +159,19 @@ public Builder setClock(Clock clock) { return this; } + public Builder setHttpTransportFactory(HttpTransportFactory httpTransportFactory) { + this.httpTransportFactory = httpTransportFactory; + return this; + } + public TokenVerifier build() { return new TokenVerifier(this); } } static class PublicKeyLoader extends CacheLoader> { + private final HttpTransportFactory httpTransportFactory; + public static class JsonWebKeySet extends GenericJson { @Key public List keys; } @@ -184,9 +196,13 @@ public static class JsonWebKey { @Key public String n; } + PublicKeyLoader(HttpTransportFactory httpTransportFactory) { + super(); + this.httpTransportFactory = httpTransportFactory; + } + @Override public Map load(String certificateUrl) throws Exception { - HttpTransportFactory httpTransportFactory = OAuth2Utils.HTTP_TRANSPORT_FACTORY; HttpTransport httpTransport = httpTransportFactory.create(); JsonWebKeySet jwks; try { diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java index 3bb69d865..b0fc9421a 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java @@ -15,14 +15,13 @@ */ package com.google.auth.oauth2; -import com.google.api.client.util.Clock; -import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import com.google.api.client.util.Clock; import java.util.Arrays; import java.util.List; - -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import org.junit.Test; public class ITTokenVerifierTest { private static final String ES256_TOKEN = diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 9fa2e137c..259d7f971 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -18,7 +18,15 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +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.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.auth.http.HttpTransportFactory; +import java.io.IOException; import java.util.Arrays; import java.util.List; import org.junit.Test; @@ -89,6 +97,80 @@ public void verifyExpectedIssuer() { } } + @Test + public void verifyEs256Token404CertificateUrl() throws TokenVerifier.VerificationException { + // 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(404); + response.setContentType("application/json"); + response.setContent(""); + return response; + } + }; + } + }; + } + }; + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder() + .setClock(FIXED_CLOCK) + .setHttpTransportFactory(httpTransportFactory) + .build(); + try { + tokenVerifier.verify(ES256_TOKEN); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("Could not find publicKey")); + } + } + + @Test + public void verifyEs256TokenPublicKeyMismatch() throws TokenVerifier.VerificationException { + // 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; + } + }; + } + }; + } + }; + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder() + .setClock(FIXED_CLOCK) + .setHttpTransportFactory(httpTransportFactory) + .build(); + try { + tokenVerifier.verify(ES256_TOKEN); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("Could not find publicKey")); + } + } + @Test public void verifyEs256Token() throws TokenVerifier.VerificationException { TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); diff --git a/pom.xml b/pom.xml index 9398b6a89..0d22098b5 100644 --- a/pom.xml +++ b/pom.xml @@ -421,5 +421,83 @@ + + autovalue-java7 + + 1.7 + + + 1.7 + 1.4 + + + + + maven-compiler-plugin + + + + com.google.auto.value + auto-value + ${auto-value.version} + + + + + + + + + + + autovalue-java8 + + [1.8,) + + + 1.7 + ${auto-value.version} + 1.0-rc6 + + + + + maven-compiler-plugin + + + + com.google.auto.value + auto-value + ${auto-value.version} + + + + com.google.auto.service + auto-service-annotations + ${auto-service-annotations.version} + + + + + + + From 141686ab41d64455575ad1fe49975b090dae1a98 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Mon, 27 Apr 2020 11:51:21 -0700 Subject: [PATCH 04/18] fix: use google-http-client for actual signature verification --- .../com/google/auth/oauth2/TokenVerifier.java | 128 ++++++------------ .../google/auth/oauth2/TokenVerifierTest.java | 5 +- 2 files changed, 43 insertions(+), 90 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index f1d2e4208..ffcba61fc 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -31,18 +31,17 @@ 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.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.spec.ECGenParameterSpec; @@ -52,9 +51,9 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidParameterSpecException; import java.security.spec.RSAPublicKeySpec; -import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -62,6 +61,7 @@ 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_ALGORITMS = ImmutableSet.of("RS256", "ES256"); private final String audience; private final String certificatesLocation; @@ -115,15 +115,45 @@ public boolean verify(String token) throws VerificationException { throw new VerificationException("Token is expired"); } - switch (jsonWebSignature.getHeader().getAlgorithm()) { + // Short-circuit signature types + if (!SUPPORTED_ALGORITMS.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 { + return jsonWebSignature.verifySignature(publicKeyToUse); + } 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 verifyRs256(jsonWebSignature); + return FEDERATED_SIGNON_CERT_URL; case "ES256": - return verifyEs256(jsonWebSignature); - default: - throw new VerificationException( - "Unexpected signing algorithm: expected either RS256 or ES256"); + return IAP_CERT_URL; } + + throw new VerificationException("Unknown algorithm"); } public static class Builder { @@ -297,82 +327,4 @@ public VerificationException(String message, Throwable cause) { super(message, cause); } } - - private boolean verifyEs256(JsonWebSignature jsonWebSignature) throws VerificationException { - String certsUrl = certificatesLocation == null ? IAP_CERT_URL : certificatesLocation; - PublicKey verifyPublicKey = publicKey; - if (verifyPublicKey == null) { - try { - verifyPublicKey = publicKeyCache.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); - } catch (ExecutionException e) { - throw new VerificationException("Error fetching PublicKey for ES256 token", e); - } - } - if (verifyPublicKey == null) { - throw new VerificationException( - "Could not find publicKey for provided keyId: " - + jsonWebSignature.getHeader().getKeyId()); - } - try { - Signature signatureAlgorithm = Signature.getInstance("SHA256withECDSA"); - signatureAlgorithm.initVerify(verifyPublicKey); - signatureAlgorithm.update(jsonWebSignature.getSignedContentBytes()); - byte[] derBytes = convertDerBytes(jsonWebSignature.getSignatureBytes()); - return signatureAlgorithm.verify(derBytes); - } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { - throw new VerificationException("Error validating ES256 token", e); - } - } - - private boolean verifyRs256(JsonWebSignature jsonWebSignature) throws VerificationException { - String certsUrl = - certificatesLocation == null ? FEDERATED_SIGNON_CERT_URL : certificatesLocation; - PublicKey verifyPublicKey = publicKey; - if (verifyPublicKey == null) { - try { - verifyPublicKey = publicKeyCache.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); - } catch (ExecutionException e) { - throw new VerificationException("Error fetching PublicKey for ES256 token", e); - } - } - if (verifyPublicKey == null) { - throw new VerificationException( - "Could not find publicKey for provided keyId: " - + jsonWebSignature.getHeader().getKeyId()); - } - try { - return jsonWebSignature.verifySignature(verifyPublicKey); - } catch (GeneralSecurityException e) { - throw new VerificationException("Error validating RS256 token", e); - } - } - - private static byte DER_TAG_SIGNATURE_OBJECT = 0x30; - private static byte DER_TAG_ASN1_INTEGER = 0x02; - - private static byte[] convertDerBytes(byte[] signature) { - // expect the signature to be 64 bytes long - Preconditions.checkState(signature.length == 64); - - byte[] int1 = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32)).toByteArray(); - byte[] int2 = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64)).toByteArray(); - byte[] der = new byte[6 + int1.length + int2.length]; - - // Mark that this is a signature object - der[0] = DER_TAG_SIGNATURE_OBJECT; - der[1] = (byte) (der.length - 2); - - // Start ASN1 integer and write the first 32 bits - der[2] = DER_TAG_ASN1_INTEGER; - der[3] = (byte) int1.length; - System.arraycopy(int1, 0, der, 4, int1.length); - - // Start ASN1 integer and write the second 32 bits - int offset = int1.length + 4; - der[offset] = DER_TAG_ASN1_INTEGER; - der[offset + 1] = (byte) int2.length; - System.arraycopy(int2, 0, der, offset + 2, int2.length); - - return der; - } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 259d7f971..e37e391e3 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -130,7 +130,7 @@ public LowLevelHttpResponse execute() throws IOException { try { tokenVerifier.verify(ES256_TOKEN); } catch (TokenVerifier.VerificationException e) { - assertTrue(e.getMessage().contains("Could not find publicKey")); + assertTrue(e.getMessage().contains("Could not find PublicKey")); } } @@ -166,8 +166,9 @@ public LowLevelHttpResponse execute() throws IOException { .build(); try { tokenVerifier.verify(ES256_TOKEN); + fail("Should have failed verification"); } catch (TokenVerifier.VerificationException e) { - assertTrue(e.getMessage().contains("Could not find publicKey")); + assertTrue(e.getMessage().contains("Error fetching PublicKey")); } } From 261c7c2c9416b364c8670b535a4dd34757f828ae Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Mon, 27 Apr 2020 12:01:41 -0700 Subject: [PATCH 05/18] chore: lint --- .../java/com/google/auth/oauth2/TokenVerifier.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index ffcba61fc..2838c9144 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -125,15 +125,17 @@ public boolean verify(String token) throws VerificationException { if (publicKeyToUse == null) { try { String certificateLocation = getCertificateLocation(jsonWebSignature); - publicKeyToUse = publicKeyCache.get(certificateLocation).get(jsonWebSignature.getHeader().getKeyId()); + 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()); + throw new VerificationException( + "Could not find PublicKey for provided keyId: " + + jsonWebSignature.getHeader().getKeyId()); } try { @@ -143,10 +145,11 @@ public boolean verify(String token) throws VerificationException { } } - private String getCertificateLocation(JsonWebSignature jsonWebSignature) throws VerificationException { + private String getCertificateLocation(JsonWebSignature jsonWebSignature) + throws VerificationException { if (certificatesLocation != null) return certificatesLocation; - switch(jsonWebSignature.getHeader().getAlgorithm()) { + switch (jsonWebSignature.getHeader().getAlgorithm()) { case "RS256": return FEDERATED_SIGNON_CERT_URL; case "ES256": From 59bfcd0c645c7f2117e592c9af48f43b4fa5e9da Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Mon, 27 Apr 2020 12:59:05 -0700 Subject: [PATCH 06/18] test: split test into unit and integration Unit tests mock out the http request activity. Integration tests hit the live urls. --- .../google/auth/oauth2/TokenVerifierTest.java | 76 ++++++++++++++++--- oauth2_http/testresources/federated_keys.json | 20 +++++ oauth2_http/testresources/iap_keys.json | 49 ++++++++++++ .../testresources/legacy_federated_keys.json | 4 + .../testresources/service_account_keys.json | 4 + 5 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 oauth2_http/testresources/federated_keys.json create mode 100644 oauth2_http/testresources/iap_keys.json create mode 100644 oauth2_http/testresources/legacy_federated_keys.json create mode 100644 oauth2_http/testresources/service_account_keys.json diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index e37e391e3..0e9ce7d51 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -15,9 +15,6 @@ */ package com.google.auth.oauth2; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - import com.google.api.client.http.HttpTransport; import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.http.LowLevelHttpResponse; @@ -26,11 +23,21 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.client.util.Clock; import com.google.auth.http.HttpTransportFactory; + import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; import java.util.Arrays; import java.util.List; + +import com.google.common.io.CharStreams; import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + public class TokenVerifierTest { private static final String ES256_TOKEN = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ.yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA"; @@ -98,7 +105,7 @@ public void verifyExpectedIssuer() { } @Test - public void verifyEs256Token404CertificateUrl() throws TokenVerifier.VerificationException { + public void verifyEs256Token404CertificateUrl() { // Mock HTTP requests HttpTransportFactory httpTransportFactory = new HttpTransportFactory() { @@ -135,7 +142,7 @@ public LowLevelHttpResponse execute() throws IOException { } @Test - public void verifyEs256TokenPublicKeyMismatch() throws TokenVerifier.VerificationException { + public void verifyEs256TokenPublicKeyMismatch() { // Mock HTTP requests HttpTransportFactory httpTransportFactory = new HttpTransportFactory() { @@ -173,30 +180,43 @@ public LowLevelHttpResponse execute() throws IOException { } @Test - public void verifyEs256Token() throws TokenVerifier.VerificationException { - TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); + public void verifyEs256Token() throws TokenVerifier.VerificationException, IOException { + HttpTransportFactory httpTransportFactory = mockTransport("https://www.gstatic.com/iap/verify/public_key-jwk", readResourceAsString("iap_keys.json")); + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder() + .setClock(FIXED_CLOCK) + .setHttpTransportFactory(httpTransportFactory) + .build(); assertTrue(tokenVerifier.verify(ES256_TOKEN)); } @Test - public void verifyRs256Token() throws TokenVerifier.VerificationException { - TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); + public void verifyRs256Token() throws TokenVerifier.VerificationException, IOException { + HttpTransportFactory httpTransportFactory = mockTransport("https://www.googleapis.com/oauth2/v3/certs", readResourceAsString("federated_keys.json")); + TokenVerifier tokenVerifier = + TokenVerifier.newBuilder() + .setClock(FIXED_CLOCK) + .setHttpTransportFactory(httpTransportFactory) + .build(); assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); } @Test public void verifyRs256TokenWithLegacyCertificateUrlFormat() - throws TokenVerifier.VerificationException { + throws TokenVerifier.VerificationException, IOException { + HttpTransportFactory httpTransportFactory = mockTransport(LEGACY_FEDERATED_SIGNON_CERT_URL, readResourceAsString("legacy_federated_keys.json")); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) .setClock(FIXED_CLOCK) + .setHttpTransportFactory(httpTransportFactory) .build(); assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); } @Test - public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationException { + public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationException, IOException { + HttpTransportFactory httpTransportFactory = mockTransport(SERVICE_ACCOUNT_CERT_URL, readResourceAsString("service_account_keys.json")); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(FIXED_CLOCK) @@ -204,4 +224,38 @@ public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationEx .build(); assertTrue(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); } + + static String readResourceAsString(String resourceName) throws IOException { + InputStream inputStream = TokenVerifierTest.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; + } + }; + } + }; + } + }; + } } diff --git a/oauth2_http/testresources/federated_keys.json b/oauth2_http/testresources/federated_keys.json new file mode 100644 index 000000000..9986a8e0f --- /dev/null +++ b/oauth2_http/testresources/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/oauth2_http/testresources/iap_keys.json b/oauth2_http/testresources/iap_keys.json new file mode 100644 index 000000000..2ba2bfa01 --- /dev/null +++ b/oauth2_http/testresources/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/oauth2_http/testresources/legacy_federated_keys.json b/oauth2_http/testresources/legacy_federated_keys.json new file mode 100644 index 000000000..3a5748399 --- /dev/null +++ b/oauth2_http/testresources/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/oauth2_http/testresources/service_account_keys.json b/oauth2_http/testresources/service_account_keys.json new file mode 100644 index 000000000..361bb2e4d --- /dev/null +++ b/oauth2_http/testresources/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 From 5dd6b44e77652c79c377f32153fe2236ab778016 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Mon, 27 Apr 2020 13:01:04 -0700 Subject: [PATCH 07/18] chore: lint --- .../google/auth/oauth2/TokenVerifierTest.java | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 0e9ce7d51..a0ce86aa8 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -15,6 +15,10 @@ */ package com.google.auth.oauth2; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import com.google.api.client.http.HttpTransport; import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.http.LowLevelHttpResponse; @@ -23,21 +27,15 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.client.util.Clock; import com.google.auth.http.HttpTransportFactory; - +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.List; - -import com.google.common.io.CharStreams; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - public class TokenVerifierTest { private static final String ES256_TOKEN = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ.yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA"; @@ -181,7 +179,10 @@ public LowLevelHttpResponse execute() throws IOException { @Test public void verifyEs256Token() throws TokenVerifier.VerificationException, IOException { - HttpTransportFactory httpTransportFactory = mockTransport("https://www.gstatic.com/iap/verify/public_key-jwk", readResourceAsString("iap_keys.json")); + HttpTransportFactory httpTransportFactory = + mockTransport( + "https://www.gstatic.com/iap/verify/public_key-jwk", + readResourceAsString("iap_keys.json")); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(FIXED_CLOCK) @@ -192,7 +193,10 @@ public void verifyEs256Token() throws TokenVerifier.VerificationException, IOExc @Test public void verifyRs256Token() throws TokenVerifier.VerificationException, IOException { - HttpTransportFactory httpTransportFactory = mockTransport("https://www.googleapis.com/oauth2/v3/certs", readResourceAsString("federated_keys.json")); + HttpTransportFactory httpTransportFactory = + mockTransport( + "https://www.googleapis.com/oauth2/v3/certs", + readResourceAsString("federated_keys.json")); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(FIXED_CLOCK) @@ -204,7 +208,9 @@ public void verifyRs256Token() throws TokenVerifier.VerificationException, IOExc @Test public void verifyRs256TokenWithLegacyCertificateUrlFormat() throws TokenVerifier.VerificationException, IOException { - HttpTransportFactory httpTransportFactory = mockTransport(LEGACY_FEDERATED_SIGNON_CERT_URL, readResourceAsString("legacy_federated_keys.json")); + HttpTransportFactory httpTransportFactory = + mockTransport( + LEGACY_FEDERATED_SIGNON_CERT_URL, readResourceAsString("legacy_federated_keys.json")); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) @@ -215,8 +221,10 @@ public void verifyRs256TokenWithLegacyCertificateUrlFormat() } @Test - public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationException, IOException { - HttpTransportFactory httpTransportFactory = mockTransport(SERVICE_ACCOUNT_CERT_URL, readResourceAsString("service_account_keys.json")); + public void verifyServiceAccountRs256Token() + throws TokenVerifier.VerificationException, IOException { + HttpTransportFactory httpTransportFactory = + mockTransport(SERVICE_ACCOUNT_CERT_URL, readResourceAsString("service_account_keys.json")); TokenVerifier tokenVerifier = TokenVerifier.newBuilder() .setClock(FIXED_CLOCK) @@ -226,7 +234,8 @@ public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationEx } static String readResourceAsString(String resourceName) throws IOException { - InputStream inputStream = TokenVerifierTest.class.getClassLoader().getResourceAsStream(resourceName); + InputStream inputStream = + TokenVerifierTest.class.getClassLoader().getResourceAsStream(resourceName); try (final Reader reader = new InputStreamReader(inputStream)) { return CharStreams.toString(reader); } @@ -240,8 +249,7 @@ static HttpTransportFactory mockTransport(String url, String certificates) { public HttpTransport create() { return new MockHttpTransport() { @Override - public LowLevelHttpRequest buildRequest(String method, String url) - throws IOException { + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { assertEquals(certificatesUrl, url); return new MockLowLevelHttpRequest() { @Override From 48aea1d815b5ae6fa366fa96cda9fefda940e7cb Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Fri, 1 May 2020 09:21:33 -0700 Subject: [PATCH 08/18] fix: return the JsonWebSignature instance on verify --- .../java/com/google/auth/oauth2/TokenVerifier.java | 7 +++++-- .../com/google/auth/oauth2/ITTokenVerifierTest.java | 9 +++++---- .../com/google/auth/oauth2/TokenVerifierTest.java | 9 +++++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index 2838c9144..83e676ce8 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -92,7 +92,7 @@ public static Builder newBuilder() { .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY); } - public boolean verify(String token) throws VerificationException { + public JsonWebSignature verify(String token) throws VerificationException { JsonWebSignature jsonWebSignature; try { jsonWebSignature = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, token); @@ -139,7 +139,10 @@ public boolean verify(String token) throws VerificationException { } try { - return jsonWebSignature.verifySignature(publicKeyToUse); + if (jsonWebSignature.verifySignature(publicKeyToUse)) { + return jsonWebSignature; + } + throw new VerificationException("Invalid signature"); } catch (GeneralSecurityException e) { throw new VerificationException("Error validating token", e); } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java index b0fc9421a..82b445c2c 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java @@ -15,6 +15,7 @@ */ package com.google.auth.oauth2; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -92,13 +93,13 @@ public void verifyExpectedIssuer() { @Test public void verifyEs256Token() throws TokenVerifier.VerificationException { TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); - assertTrue(tokenVerifier.verify(ES256_TOKEN)); + assertNotNull(tokenVerifier.verify(ES256_TOKEN)); } @Test public void verifyRs256Token() throws TokenVerifier.VerificationException { TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); - assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); + assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); } @Test @@ -109,7 +110,7 @@ public void verifyRs256TokenWithLegacyCertificateUrlFormat() .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) .setClock(FIXED_CLOCK) .build(); - assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); + assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); } @Test @@ -119,6 +120,6 @@ public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationEx .setClock(FIXED_CLOCK) .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) .build(); - assertTrue(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); + assertNotNull(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index a0ce86aa8..183f001a9 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -16,6 +16,7 @@ package com.google.auth.oauth2; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -188,7 +189,7 @@ public void verifyEs256Token() throws TokenVerifier.VerificationException, IOExc .setClock(FIXED_CLOCK) .setHttpTransportFactory(httpTransportFactory) .build(); - assertTrue(tokenVerifier.verify(ES256_TOKEN)); + assertNotNull(tokenVerifier.verify(ES256_TOKEN)); } @Test @@ -202,7 +203,7 @@ public void verifyRs256Token() throws TokenVerifier.VerificationException, IOExc .setClock(FIXED_CLOCK) .setHttpTransportFactory(httpTransportFactory) .build(); - assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); + assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); } @Test @@ -217,7 +218,7 @@ public void verifyRs256TokenWithLegacyCertificateUrlFormat() .setClock(FIXED_CLOCK) .setHttpTransportFactory(httpTransportFactory) .build(); - assertTrue(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); + assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); } @Test @@ -230,7 +231,7 @@ public void verifyServiceAccountRs256Token() .setClock(FIXED_CLOCK) .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) .build(); - assertTrue(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); + assertNotNull(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); } static String readResourceAsString(String resourceName) throws IOException { From 5709b80c2b0dc3836eac0637142641fa7b805254 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 28 May 2020 10:54:21 -0700 Subject: [PATCH 09/18] test: remove IT test as the signature keys can/will change over time --- .../auth/oauth2/ITTokenVerifierTest.java | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java deleted file mode 100644 index 82b445c2c..000000000 --- a/oauth2_http/javatests/com/google/auth/oauth2/ITTokenVerifierTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.auth.oauth2; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import com.google.api.client.util.Clock; -import java.util.Arrays; -import java.util.List; -import org.junit.Test; - -public class ITTokenVerifierTest { - 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); - - // Fixed to 2020-02-26 08:00:00 to allow expiration tests to pass - private static final Clock FIXED_CLOCK = - new Clock() { - @Override - public long currentTimeMillis() { - return 1582704000000L; - } - }; - - @Test - public void verifyExpiredToken() { - for (String token : ALL_TOKENS) { - TokenVerifier tokenVerifier = new TokenVerifier(); - try { - tokenVerifier.verify(token); - fail("Should have thrown a VerificationException"); - } catch (TokenVerifier.VerificationException e) { - assertTrue(e.getMessage().contains("expired")); - } - } - } - - @Test - public void verifyExpectedAudience() { - TokenVerifier tokenVerifier = - TokenVerifier.newBuilder().setAudience("expected audience").build(); - for (String token : ALL_TOKENS) { - try { - tokenVerifier.verify(token); - fail("Should have thrown a VerificationException"); - } catch (TokenVerifier.VerificationException e) { - assertTrue(e.getMessage().contains("audience does not match")); - } - } - } - - @Test - public void verifyExpectedIssuer() { - TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setIssuer("expected issuer").build(); - for (String token : ALL_TOKENS) { - try { - tokenVerifier.verify(token); - fail("Should have thrown a VerificationException"); - } catch (TokenVerifier.VerificationException e) { - assertTrue(e.getMessage().contains("issuer does not match")); - } - } - } - - @Test - public void verifyEs256Token() throws TokenVerifier.VerificationException { - TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); - assertNotNull(tokenVerifier.verify(ES256_TOKEN)); - } - - @Test - public void verifyRs256Token() throws TokenVerifier.VerificationException { - TokenVerifier tokenVerifier = TokenVerifier.newBuilder().setClock(FIXED_CLOCK).build(); - assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); - } - - @Test - public void verifyRs256TokenWithLegacyCertificateUrlFormat() - throws TokenVerifier.VerificationException { - TokenVerifier tokenVerifier = - TokenVerifier.newBuilder() - .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) - .setClock(FIXED_CLOCK) - .build(); - assertNotNull(tokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); - } - - @Test - public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationException { - TokenVerifier tokenVerifier = - TokenVerifier.newBuilder() - .setClock(FIXED_CLOCK) - .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) - .build(); - assertNotNull(tokenVerifier.verify(SERVICE_ACCOUNT_RS256_TOKEN)); - } -} From d3ab6ea6fcff0193b33bca87867634e379c37514 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 28 May 2020 12:23:27 -0700 Subject: [PATCH 10/18] docs: add javadoc for TokenVerifier --- .../com/google/auth/oauth2/TokenVerifier.java | 79 ++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index 83e676ce8..b66f34ee6 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -26,6 +26,7 @@ import com.google.api.client.util.Clock; import com.google.api.client.util.Key; import com.google.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; @@ -57,11 +58,18 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +/** + * Handle verification of Google-signed 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_ALGORITMS = ImmutableSet.of("RS256", "ES256"); + private static final Set SUPPORTED_ALGORITHMS = ImmutableSet.of("RS256", "ES256"); private final String audience; private final String certificatesLocation; @@ -70,6 +78,9 @@ public class TokenVerifier { private final Clock clock; private final LoadingCache> publicKeyCache; + /** + * Construct the default TokenVerifier. By default, this verifier will only validate the signature. + */ public TokenVerifier() { this(newBuilder()); } @@ -92,6 +103,14 @@ public static Builder newBuilder() { .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 Any verification error or failed claim with throw a VerificationException + * wrapping the underlying issue. + */ public JsonWebSignature verify(String token) throws VerificationException { JsonWebSignature jsonWebSignature; try { @@ -116,7 +135,7 @@ public JsonWebSignature verify(String token) throws VerificationException { } // Short-circuit signature types - if (!SUPPORTED_ALGORITMS.contains(jsonWebSignature.getHeader().getAlgorithm())) { + if (!SUPPORTED_ALGORITHMS.contains(jsonWebSignature.getHeader().getAlgorithm())) { throw new VerificationException( "Unexpected signing algorithm: expected either RS256 or ES256"); } @@ -170,48 +189,101 @@ public static class Builder { 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; @@ -324,6 +396,9 @@ private PublicKey buildEs256PublicKey(JsonWebKey key) } } + /** + * Custom exception for wrapping all verification errors. + */ public static class VerificationException extends Exception { public VerificationException(String message) { super(message); From 96ab0dbe9ff37fffa097471db25be5c25f5692ae Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 28 May 2020 12:38:43 -0700 Subject: [PATCH 11/18] docs: add guide for verifying tokens in the README --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index 78e5b1d30..6f8f9a82a 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,51 @@ Bigquery bq = new Bigquery.Builder(HTTP_TRANSPORT, JSON_FACTORY, requestInitiali .build(); ``` +## Verifying JWT Tokens (Beta) + +To verify a JWT token, use the [`TokenVerifier`][token-verifier] class. + +### Verifying a Signature + +To verify a signature, use the default [`TokenVerifier`][token-verifier]: + +```java +import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.auth.oauth2.TokenVerifier; + +TokenVerifier tokenVerifier = new TokenVerifier(); +try { + JsonWebSignature jsonWebSignature = tokenVerifier.verify(tokenString); + // optionally verify additional claims + jsonWebSignature +} catch (TokenVerifier.VerificationException e) { + // invalid token +} +``` + +### Customizing the TokenVerifier + +To customize a [`TokenVerifier`][token-verifier], instantiate it via its builder: + +```java +import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.auth.oauth2.TokenVerifier; + +TokenVerifier tokenVerifier = TokenVerifier.newBuilder() + .setAudience("audience-to-verify") + .setIssuer("issuer-to-verify") + .build(); +try { + JsonWebSignature jsonWebSignature = tokenVerifier.verify(tokenString); + // optionally verify additional claims + jsonWebSignature +} catch (TokenVerifier.VerificationException e) { + // invalid token +} +``` + +For more options, see the [`TokenVerifier.Builder`][token-verifier-builder] documentation. + ## CI Status Java Version | Status @@ -220,3 +265,5 @@ BSD 3-Clause - See [LICENSE](LICENSE) for more information. [apiary-clients]: https://search.maven.org/search?q=g:com.google.apis [http-credentials-adapter]: https://googleapis.dev/java/google-auth-library/latest/index.html?com/google/auth/http/HttpCredentialsAdapter.html [http-request-initializer]: https://googleapis.dev/java/google-http-client/latest/index.html?com/google/api/client/http/HttpRequestInitializer.html +[token-verifier]: https://googleapis.dev/java/google-auth-library/latest/index.html?com/google/auth/oauth2/TokenVerifier.html +[token-verifier-builder]: https://googleapis.dev/java/google-auth-library/latest/index.html?com/google/auth/oauth2/TokenVerifier.Builder.html \ No newline at end of file From a94672a95aa42c433893dc03181aff68d66f67a3 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 28 May 2020 12:39:28 -0700 Subject: [PATCH 12/18] chore: remove auto-value config changes --- pom.xml | 78 --------------------------------------------------------- 1 file changed, 78 deletions(-) diff --git a/pom.xml b/pom.xml index be19256cf..8f5833bc5 100644 --- a/pom.xml +++ b/pom.xml @@ -421,83 +421,5 @@ - - autovalue-java7 - - 1.7 - - - 1.7 - 1.4 - - - - - maven-compiler-plugin - - - - com.google.auto.value - auto-value - ${auto-value.version} - - - - - - - - - - - autovalue-java8 - - [1.8,) - - - 1.7 - ${auto-value.version} - 1.0-rc6 - - - - - maven-compiler-plugin - - - - com.google.auto.value - auto-value - ${auto-value.version} - - - - com.google.auto.service - auto-service-annotations - ${auto-service-annotations.version} - - - - - - - From b79e90d65c0e13dd7fdc9f0cd04e05d85699095a Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 28 May 2020 13:18:08 -0700 Subject: [PATCH 13/18] chore: tense, lower-case first word, no period --- .../com/google/auth/oauth2/TokenVerifier.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index b66f34ee6..8c052fec5 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -79,7 +79,7 @@ public class TokenVerifier { private final LoadingCache> publicKeyCache; /** - * Construct the default TokenVerifier. By default, this verifier will only validate the signature. + * Construct the default TokenVerifier. By default, this verifier only validates the signature. */ public TokenVerifier() { this(newBuilder()); @@ -106,8 +106,8 @@ public static Builder newBuilder() { /** * Verify an encoded JWT token. * - * @param token Encoded JWT token. - * @return The parsed JsonWebSignature instance for additional validation if necessary. + * @param token encoded JWT token + * @return The parsed JsonWebSignature instance for additional validation if necessary * @throws VerificationException Any verification error or failed claim with throw a VerificationException * wrapping the underlying issue. */ @@ -192,7 +192,7 @@ public static class Builder { /** * Set a target audience to verify. * - * @param audience The audience claim to verify. + * @param audience the audience claim to verify * @return the builder */ public Builder setAudience(String audience) { @@ -204,7 +204,7 @@ public Builder setAudience(String audience) { * Override the location URL that contains published public keys. Defaults to well-known * Google locations. * - * @param certificatesLocation URL to published public keys. + * @param certificatesLocation URL to published public keys * @return the builder */ public Builder setCertificatesLocation(String certificatesLocation) { @@ -215,7 +215,7 @@ public Builder setCertificatesLocation(String certificatesLocation) { /** * Set the issuer to verify. * - * @param issuer The issuer claim to verify. + * @param issuer the issuer claim to verify * @return the builder */ public Builder setIssuer(String issuer) { @@ -227,7 +227,7 @@ public Builder setIssuer(String issuer) { * 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. + * @param publicKey the public key to validate the signature * @return the builder */ public Builder setPublicKey(PublicKey publicKey) { @@ -238,7 +238,7 @@ public Builder setPublicKey(PublicKey publicKey) { /** * Set the clock for checking token expiry. Used for testing. * - * @param clock The clock to use. Defaults to the system clock. + * @param clock the clock to use. Defaults to the system clock * @return the builder */ public Builder setClock(Clock clock) { @@ -250,7 +250,7 @@ public Builder setClock(Clock clock) { * 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. + * @param httpTransportFactory the HttpTransportFactory used to build certificate URL requests * @return the builder */ public Builder setHttpTransportFactory(HttpTransportFactory httpTransportFactory) { From c57fdb1997b3e90de3295a250c2e83b468d8a313 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 28 May 2020 13:19:31 -0700 Subject: [PATCH 14/18] chore: run formatter --- .../com/google/auth/oauth2/TokenVerifier.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index 8c052fec5..6e26494db 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -108,8 +108,8 @@ public static Builder newBuilder() { * * @param token encoded JWT token * @return The parsed JsonWebSignature instance for additional validation if necessary - * @throws VerificationException Any verification error or failed claim with throw a VerificationException - * wrapping the underlying issue. + * @throws VerificationException Any verification error or failed claim with throw a + * VerificationException wrapping the underlying issue. */ public JsonWebSignature verify(String token) throws VerificationException { JsonWebSignature jsonWebSignature; @@ -201,8 +201,8 @@ public Builder setAudience(String audience) { } /** - * Override the location URL that contains published public keys. Defaults to well-known - * Google locations. + * 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 @@ -224,8 +224,8 @@ public Builder setIssuer(String issuer) { } /** - * Set the PublicKey for verifying the signature. This will ignore the key id from the - * JWT token header. + * 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 @@ -247,8 +247,8 @@ public Builder setClock(Clock clock) { } /** - * Set the HttpTransportFactory used for requesting public keys from the - * certificate URL. Used mostly for testing. + * 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 @@ -268,9 +268,7 @@ public TokenVerifier build() { } } - /** - * Custom CacheLoader for mapping certificate urls to the contained public keys. - */ + /** Custom CacheLoader for mapping certificate urls to the contained public keys. */ static class PublicKeyLoader extends CacheLoader> { private final HttpTransportFactory httpTransportFactory; @@ -281,9 +279,7 @@ public static class JsonWebKeySet extends GenericJson { @Key public List keys; } - /** - * Data class used for deserializing a single JSON Web Key. - */ + /** Data class used for deserializing a single JSON Web Key. */ public static class JsonWebKey { @Key public String alg; @@ -396,9 +392,7 @@ private PublicKey buildEs256PublicKey(JsonWebKey key) } } - /** - * Custom exception for wrapping all verification errors. - */ + /** Custom exception for wrapping all verification errors. */ public static class VerificationException extends Exception { public VerificationException(String message) { super(message); From 7e3234db8bb9e60fa40b6eff2002a100e29aafff Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 28 May 2020 13:34:12 -0700 Subject: [PATCH 15/18] chore: more javadoc fixes --- oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index 6e26494db..ca39d9238 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -107,9 +107,8 @@ public static Builder newBuilder() { * Verify an encoded JWT token. * * @param token encoded JWT token - * @return The parsed JsonWebSignature instance for additional validation if necessary - * @throws VerificationException Any verification error or failed claim with throw a - * VerificationException wrapping the underlying issue. + * @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; From 24e1ec6f626ea4a60c04165b3bb6af4659e72c14 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 28 May 2020 14:20:18 -0700 Subject: [PATCH 16/18] chore: remove line from README example --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 13e67d7c5..ec14ac580 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,6 @@ TokenVerifier tokenVerifier = new TokenVerifier(); try { JsonWebSignature jsonWebSignature = tokenVerifier.verify(tokenString); // optionally verify additional claims - jsonWebSignature } catch (TokenVerifier.VerificationException e) { // invalid token } @@ -280,7 +279,6 @@ TokenVerifier tokenVerifier = TokenVerifier.newBuilder() try { JsonWebSignature jsonWebSignature = tokenVerifier.verify(tokenString); // optionally verify additional claims - jsonWebSignature } catch (TokenVerifier.VerificationException e) { // invalid token } From cdedabc3d5dbe1c4a095d6be9adb1bb7c690114b Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Fri, 29 May 2020 12:55:51 -0700 Subject: [PATCH 17/18] sample: add snippet showing check for additional claim --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index ec14ac580..ffd5935d6 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,9 @@ TokenVerifier tokenVerifier = new TokenVerifier(); try { JsonWebSignature jsonWebSignature = tokenVerifier.verify(tokenString); // optionally verify additional claims + if (!"expected-value".equals(jsonWebSignature.getPayload().get("additional-claim"))) { + // handle custom verification error + } } catch (TokenVerifier.VerificationException e) { // invalid token } @@ -279,6 +282,9 @@ TokenVerifier tokenVerifier = TokenVerifier.newBuilder() try { JsonWebSignature jsonWebSignature = tokenVerifier.verify(tokenString); // optionally verify additional claims + if (!"expected-value".equals(jsonWebSignature.getPayload().get("additional-claim"))) { + // handle custom verification error + } } catch (TokenVerifier.VerificationException e) { // invalid token } From 96aabab7f099cb70953eb2993248fe5c453a3fca Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Wed, 24 Jun 2020 09:52:17 -0700 Subject: [PATCH 18/18] fix: remove default constructor - users should always use builder --- README.md | 2 +- oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java | 7 ------- .../com/google/auth/oauth2/TokenVerifierTest.java | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ffd5935d6..9b1b9f18d 100644 --- a/README.md +++ b/README.md @@ -255,7 +255,7 @@ To verify a signature, use the default [`TokenVerifier`][token-verifier]: import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.auth.oauth2.TokenVerifier; -TokenVerifier tokenVerifier = new TokenVerifier(); +TokenVerifier tokenVerifier = TokenVerifier.newBuilder().build(); try { JsonWebSignature jsonWebSignature = tokenVerifier.verify(tokenString); // optionally verify additional claims diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index ca39d9238..321b01217 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -78,13 +78,6 @@ public class TokenVerifier { private final Clock clock; private final LoadingCache> publicKeyCache; - /** - * Construct the default TokenVerifier. By default, this verifier only validates the signature. - */ - public TokenVerifier() { - this(newBuilder()); - } - private TokenVerifier(Builder builder) { this.audience = builder.audience; this.certificatesLocation = builder.certificatesLocation; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 183f001a9..ef2448507 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -66,7 +66,7 @@ public long currentTimeMillis() { @Test public void verifyExpiredToken() { for (String token : ALL_TOKENS) { - TokenVerifier tokenVerifier = new TokenVerifier(); + TokenVerifier tokenVerifier = TokenVerifier.newBuilder().build(); try { tokenVerifier.verify(token); fail("Should have thrown a VerificationException");