From 379c09177c4efd95e25e5241115b3113f286d8b8 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 12 Mar 2020 07:12:20 -0700 Subject: [PATCH 1/8] feat: can verify ES256 tokens --- .../javatests/com/google/auth/TestUtils.java | 17 +- .../com/google/auth/oauth2/TokenVerifier.java | 264 ++++++++++++++++++ .../google/auth/oauth2/TokenVerifierTest.java | 31 ++ pom.xml | 79 ++++++ 4 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java diff --git a/oauth2_http/javatests/com/google/auth/TestUtils.java b/oauth2_http/javatests/com/google/auth/TestUtils.java index 0726fb1f6..81fa9057f 100644 --- a/oauth2_http/javatests/com/google/auth/TestUtils.java +++ b/oauth2_http/javatests/com/google/auth/TestUtils.java @@ -40,10 +40,7 @@ import com.google.auth.http.AuthHttpConstants; import com.google.common.base.Splitter; import com.google.common.collect.Lists; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.net.URLDecoder; import java.util.HashMap; import java.util.List; @@ -81,6 +78,18 @@ private static boolean hasBearerToken(Map> metadata, String return false; } + // public foo() { + // // for ServiceAccountCredentials + // ServiceAccountCredentials saCreds = ServiceAccountCredentials.fromStream(new + // FileInputStream(credPath)); + // saCreds = (ServiceAccountCredentials) + // saCreds.createScoped(Arrays.asList("https://www.googleapis.com/auth/iam")); + // IdTokenCredentials tokenCredential = IdTokenCredentials.newBuilder() + // .setIdTokenProvider(saCreds) + // .setTargetAudience(targetAudience).build(); + // Http + // } + public static InputStream jsonToInputStream(GenericJson json) throws IOException { json.setFactory(JSON_FACTORY); String text = json.toPrettyString(); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java new file mode 100644 index 000000000..400b55044 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java @@ -0,0 +1,264 @@ +/** + * 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.webtoken.JsonWebSignature; +import com.google.api.client.util.Base64; +import com.google.api.client.util.Key; +import com.google.auth.http.HttpTransportFactory; +import com.google.auto.value.AutoValue; +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 javax.annotation.Nullable; +import java.io.IOException; +import java.math.BigInteger; +import java.security.*; +import java.security.spec.*; +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"; + + public static class JsonWebKeySet { + @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; + } + + private static final LoadingCache> PUBLIC_KEY_CACHE = + CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(new CacheLoader>() { + @Override + public Map load(String certificateUrl) throws Exception { + ImmutableMap.Builder keyCacheBuilder = + new ImmutableMap.Builder<>(); + 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(); + } + for (JsonWebKey key : jwks.keys) { + try { + keyCacheBuilder.put(key.kid, buildPublicKey(key)); + } catch (NoSuchAlgorithmException|InvalidKeySpecException|InvalidParameterSpecException ignored) { + ignored.printStackTrace(); + } + } + + return keyCacheBuilder.build(); + } + }); + + private static 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 static PublicKey buildRs256PublicKey(JsonWebKey key) { + Preconditions.checkArgument("RSA".equals(key.kty)); + // TODO(chingor): implement this + return null; + } + + private static 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(Base64.decodeBase64(key.x)); + BigInteger y = new BigInteger(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); + } + + @AutoValue + public abstract static class VerifyOptions { + @Nullable + abstract String getAudience(); + + @Nullable + abstract String getIssuer(); + + @Nullable + abstract String getCertificatesLocation(); + + static Builder newBuilder() { + return new AutoValue_TokenVerifier_VerifyOptions.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setAudience(String audience); + abstract Builder setIssuer(String issuer); + abstract Builder setCertificatesLocation(String certificatesLocation); + abstract VerifyOptions build(); + } + } + + public static class VerificationException extends Exception { + public VerificationException(String message) { + super(message); + } + public VerificationException(String message, Throwable cause) { + super(message, cause); + } + } + + public static boolean verify(String token, VerifyOptions verifyOptions) throws VerificationException { + JsonWebSignature jsonWebSignature; + try { + jsonWebSignature = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, token); + } catch (IOException e) { + throw new VerificationException("Error parsing JsonWebSignature token", e); + } + if (verifyOptions.getAudience() != null && !verifyOptions.getAudience().equals(jsonWebSignature.getPayload().getAudience())) { + throw new VerificationException("Expected audience does not match"); + } + if (verifyOptions.getIssuer() != null && !verifyOptions.getIssuer().equals(jsonWebSignature.getPayload().getIssuer())) { + throw new VerificationException("Expected issuer does not match"); + } + + String certificatesLocation = verifyOptions.getCertificatesLocation(); + switch(jsonWebSignature.getHeader().getAlgorithm()) { + case "RS256": + return verifyRs256(jsonWebSignature, verifyOptions.getCertificatesLocation()); + case "ES256": + return verifyEs256(jsonWebSignature, verifyOptions.getCertificatesLocation()); + default: + throw new VerificationException("Unexpected signing algorithm: expected either RS256 or ES256"); + } + } + + private static boolean verifyEs256(JsonWebSignature jsonWebSignature, String certificatesLocation) throws VerificationException { + String certsUrl = certificatesLocation == null ? + IAP_CERT_URL : + certificatesLocation; + try { + PublicKey publicKey = PUBLIC_KEY_CACHE.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); + Signature signatureAlgorithm = Signature.getInstance("SHA256withECDSA"); + signatureAlgorithm.initVerify(publicKey); + signatureAlgorithm.update(jsonWebSignature.getSignedContentBytes()); + byte[] derBytes = convertDerBytes(jsonWebSignature.getSignatureBytes()); + return signatureAlgorithm.verify(derBytes); + } catch (ExecutionException | NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new VerificationException("Error validating ES256 token", e); + } + } + + private static boolean verifyRs256(JsonWebSignature jsonWebSignature, String certificatesLocation) throws VerificationException { + String certsUrl = certificatesLocation == null ? + FEDERATED_SIGNON_CERT_URL : + certificatesLocation; + try { + PublicKey publicKey = PUBLIC_KEY_CACHE.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); + return jsonWebSignature.verifySignature(publicKey); + } catch (ExecutionException | GeneralSecurityException e) { + throw new VerificationException("Error validating RS256 token", e); + } + } + + /** + * Verify a Json Web Signature token against Google's published public keys. + * + * @param token The JWS token expressed as a string + * @return true if we can verify the provided token against Google's tokens + * @throws IOException if the provided token string cannot be parsed as a valid JsonWebSignature + */ + public static boolean verify(String token) throws VerificationException { + return verify(token, VerifyOptions.newBuilder().build()); + } + + 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..17faf13af --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -0,0 +1,31 @@ +/** + * 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 org.junit.Test; + +public class TokenVerifierTest { + static String TEST_TOKEN = + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ.yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA"; + + @Test + public void verifyToken() throws TokenVerifier.VerificationException { + assertTrue(TokenVerifier.verify(TEST_TOKEN)); + } +} diff --git a/pom.xml b/pom.xml index 9398b6a89..e6ec16109 100644 --- a/pom.xml +++ b/pom.xml @@ -421,5 +421,84 @@ + + + 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 a53176844da17195adecb1cf5e0dca142ada5617 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Wed, 22 Apr 2020 16:39:43 -0700 Subject: [PATCH 2/8] feat: can parse RS256 certificates --- .../com/google/auth/oauth2/TokenVerifier.java | 83 ++++++++++++++----- .../google/auth/oauth2/TokenVerifierTest.java | 14 +++- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java index 400b55044..04f9bd1a7 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java @@ -34,8 +34,21 @@ import javax.annotation.Nullable; import java.io.IOException; import java.math.BigInteger; -import java.security.*; -import java.security.spec.*; +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.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; @@ -116,18 +129,25 @@ private static PublicKey buildPublicKey(JsonWebKey key) } } - private static PublicKey buildRs256PublicKey(JsonWebKey key) { + private static PublicKey buildRs256PublicKey(JsonWebKey key) throws NoSuchAlgorithmException, InvalidKeySpecException { Preconditions.checkArgument("RSA".equals(key.kty)); - // TODO(chingor): implement this - return null; + 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 static 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(Base64.decodeBase64(key.x)); - BigInteger y = new BigInteger(Base64.decodeBase64(key.y)); + 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")); @@ -148,6 +168,9 @@ public abstract static class VerifyOptions { @Nullable abstract String getCertificatesLocation(); + @Nullable + abstract PublicKey getPublicKey(); + static Builder newBuilder() { return new AutoValue_TokenVerifier_VerifyOptions.Builder(); } @@ -155,8 +178,9 @@ static Builder newBuilder() { @AutoValue.Builder abstract static class Builder { abstract Builder setAudience(String audience); - abstract Builder setIssuer(String issuer); abstract Builder setCertificatesLocation(String certificatesLocation); + abstract Builder setIssuer(String issuer); + abstract Builder setPublicKey(PublicKey publicKey); abstract VerifyOptions build(); } } @@ -174,6 +198,7 @@ public static boolean verify(String token, VerifyOptions verifyOptions) throws V JsonWebSignature jsonWebSignature; try { jsonWebSignature = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, token); + System.out.println(jsonWebSignature); } catch (IOException e) { throw new VerificationException("Error parsing JsonWebSignature token", e); } @@ -184,41 +209,57 @@ public static boolean verify(String token, VerifyOptions verifyOptions) throws V throw new VerificationException("Expected issuer does not match"); } - String certificatesLocation = verifyOptions.getCertificatesLocation(); switch(jsonWebSignature.getHeader().getAlgorithm()) { case "RS256": - return verifyRs256(jsonWebSignature, verifyOptions.getCertificatesLocation()); + return verifyRs256(jsonWebSignature, verifyOptions); case "ES256": - return verifyEs256(jsonWebSignature, verifyOptions.getCertificatesLocation()); + return verifyEs256(jsonWebSignature, verifyOptions); default: throw new VerificationException("Unexpected signing algorithm: expected either RS256 or ES256"); } } - private static boolean verifyEs256(JsonWebSignature jsonWebSignature, String certificatesLocation) throws VerificationException { - String certsUrl = certificatesLocation == null ? + private static boolean verifyEs256(JsonWebSignature jsonWebSignature, VerifyOptions verifyOptions) throws VerificationException { + String certsUrl = verifyOptions.getCertificatesLocation() == null ? IAP_CERT_URL : - certificatesLocation; + verifyOptions.getCertificatesLocation(); + PublicKey publicKey = verifyOptions.getPublicKey(); + if (publicKey == null) { + try { + publicKey = PUBLIC_KEY_CACHE.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); + } catch (ExecutionException e) { + throw new VerificationException("Error fetching PublicKey for ES256 token", e); + } + } try { - PublicKey publicKey = PUBLIC_KEY_CACHE.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); Signature signatureAlgorithm = Signature.getInstance("SHA256withECDSA"); signatureAlgorithm.initVerify(publicKey); signatureAlgorithm.update(jsonWebSignature.getSignedContentBytes()); byte[] derBytes = convertDerBytes(jsonWebSignature.getSignatureBytes()); return signatureAlgorithm.verify(derBytes); - } catch (ExecutionException | NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { throw new VerificationException("Error validating ES256 token", e); } } - private static boolean verifyRs256(JsonWebSignature jsonWebSignature, String certificatesLocation) throws VerificationException { - String certsUrl = certificatesLocation == null ? + private static boolean verifyRs256(JsonWebSignature jsonWebSignature, VerifyOptions verifyOptions) throws VerificationException { + String certsUrl = verifyOptions.getCertificatesLocation() == null ? FEDERATED_SIGNON_CERT_URL : - certificatesLocation; + verifyOptions.getCertificatesLocation(); + PublicKey publicKey = verifyOptions.getPublicKey(); + if (publicKey == null) { + try { + publicKey = PUBLIC_KEY_CACHE.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); + } catch (ExecutionException e) { + throw new VerificationException("Error fetching PublicKey for ES256 token", e); + } + } + if (publicKey == null) { + throw new VerificationException("Could not find publicKey for provided keyId: " + jsonWebSignature.getHeader().getKeyId()); + } try { - PublicKey publicKey = PUBLIC_KEY_CACHE.get(certsUrl).get(jsonWebSignature.getHeader().getKeyId()); return jsonWebSignature.verifySignature(publicKey); - } catch (ExecutionException | GeneralSecurityException e) { + } catch (GeneralSecurityException e) { throw new VerificationException("Error validating RS256 token", e); } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 17faf13af..8dd374034 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -21,11 +21,19 @@ import org.junit.Test; public class TokenVerifierTest { - static String TEST_TOKEN = + private static final String ES256_TOKEN = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ.yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA"; + private static final String RS256_TOKEN = + "eyJhbGciOiJSUzI1NiIsImtpZCI6IjM0OTRiMWU3ODZjZGFkMDkyZTQyMzc2NmJiZTM3ZjU0ZWQ4N2IyMmQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhdWQiOiJodHRwczovL2Zvby5iYXIiLCJhenAiOiJzdmMtMi00MjlAbWluZXJhbC1taW51dGlhLTgyMC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInN1YiI6IjEwMDE0NzEwNjk5Njc2NDQ3OTA4NSIsImVtYWlsIjoic3ZjLTItNDI5QG1pbmVyYWwtbWludXRpYS04MjAuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWF0IjoxNTY1Mzg3NTM4LCJleHAiOjE1NjUzOTExMzh9.foo"; + + @Test + public void verifyEs256Token() throws TokenVerifier.VerificationException { + assertTrue(TokenVerifier.verify(ES256_TOKEN)); + } + @Test - public void verifyToken() throws TokenVerifier.VerificationException { - assertTrue(TokenVerifier.verify(TEST_TOKEN)); + public void verifyRs256Token() throws TokenVerifier.VerificationException { + assertTrue(TokenVerifier.verify(RS256_TOKEN)); } } From fbd58447c3890981749f1b7f9c3ea87f727a8f7b Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Wed, 22 Apr 2020 23:47:12 -0700 Subject: [PATCH 3/8] feat: can parse json map version of certificate url --- .../com/google/auth/oauth2/TokenVerifier.java | 222 ++++++++++-------- .../google/auth/oauth2/TokenVerifierTest.java | 10 +- 2 files changed, 135 insertions(+), 97 deletions(-) rename oauth2_http/{javatests => java}/com/google/auth/oauth2/TokenVerifier.java (53%) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java similarity index 53% rename from oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java rename to oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index 04f9bd1a7..c9e05766e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -1,4 +1,4 @@ -/** +/* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,13 +13,13 @@ * 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.Key; @@ -30,9 +30,9 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableMap; - -import javax.annotation.Nullable; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.AlgorithmParameters; import java.security.GeneralSecurityException; @@ -42,26 +42,24 @@ import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; -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.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.spec.*; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; 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 String FEDERATED_SIGNON_CERT_URL = + "https://www.googleapis.com/oauth2/v3/certs"; - public static class JsonWebKeySet { - @Key - public List keys; + public static class JsonWebKeySet extends GenericJson { + @Key public List keys; } public static class JsonWebKey { @@ -87,75 +85,96 @@ public static class JsonWebKey { private static final LoadingCache> PUBLIC_KEY_CACHE = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) - .build(new CacheLoader>() { - @Override - public Map load(String certificateUrl) throws Exception { - ImmutableMap.Builder keyCacheBuilder = - new ImmutableMap.Builder<>(); - 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(); - } - for (JsonWebKey key : jwks.keys) { - try { - keyCacheBuilder.put(key.kid, buildPublicKey(key)); - } catch (NoSuchAlgorithmException|InvalidKeySpecException|InvalidParameterSpecException ignored) { - ignored.printStackTrace(); + .build( + new CacheLoader>() { + @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) { + 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(); } - } - - return keyCacheBuilder.build(); - } - }); - - private static 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 static PublicKey buildRs256PublicKey(JsonWebKey key) throws NoSuchAlgorithmException, InvalidKeySpecException { - Preconditions.checkArgument("RSA".equals(key.kty)); - Preconditions.checkNotNull(key.e); - Preconditions.checkNotNull(key.n); + 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; + } + } - BigInteger modulus = new BigInteger(1, Base64.decodeBase64(key.n)); - BigInteger exponent = new BigInteger(1, Base64.decodeBase64(key.e)); + private PublicKey buildPublicKey(String publicPem) + throws CertificateException, UnsupportedEncodingException { + CertificateFactory f = CertificateFactory.getInstance("X.509"); + Certificate certificate = + f.generateCertificate(new ByteArrayInputStream(publicPem.getBytes("UTF-8"))); + return certificate.getPublicKey(); + } - RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); - KeyFactory factory = KeyFactory.getInstance("RSA"); - return factory.generatePublic(spec); - } + private PublicKey buildRs256PublicKey(JsonWebKey key) + throws NoSuchAlgorithmException, InvalidKeySpecException { + Preconditions.checkArgument("RSA".equals(key.kty)); + Preconditions.checkNotNull(key.e); + Preconditions.checkNotNull(key.n); - private static 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); - } + 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); + } + }); @AutoValue public abstract static class VerifyOptions { @@ -178,9 +197,13 @@ static Builder newBuilder() { @AutoValue.Builder abstract static class Builder { abstract Builder setAudience(String audience); + abstract Builder setCertificatesLocation(String certificatesLocation); + abstract Builder setIssuer(String issuer); + abstract Builder setPublicKey(PublicKey publicKey); + abstract VerifyOptions build(); } } @@ -189,40 +212,46 @@ public static class VerificationException extends Exception { public VerificationException(String message) { super(message); } + public VerificationException(String message, Throwable cause) { super(message, cause); } } - public static boolean verify(String token, VerifyOptions verifyOptions) throws VerificationException { + public static boolean verify(String token, VerifyOptions verifyOptions) + throws VerificationException { JsonWebSignature jsonWebSignature; try { jsonWebSignature = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, token); - System.out.println(jsonWebSignature); } catch (IOException e) { throw new VerificationException("Error parsing JsonWebSignature token", e); } - if (verifyOptions.getAudience() != null && !verifyOptions.getAudience().equals(jsonWebSignature.getPayload().getAudience())) { + if (verifyOptions.getAudience() != null + && !verifyOptions.getAudience().equals(jsonWebSignature.getPayload().getAudience())) { throw new VerificationException("Expected audience does not match"); } - if (verifyOptions.getIssuer() != null && !verifyOptions.getIssuer().equals(jsonWebSignature.getPayload().getIssuer())) { + if (verifyOptions.getIssuer() != null + && !verifyOptions.getIssuer().equals(jsonWebSignature.getPayload().getIssuer())) { throw new VerificationException("Expected issuer does not match"); } - switch(jsonWebSignature.getHeader().getAlgorithm()) { + switch (jsonWebSignature.getHeader().getAlgorithm()) { case "RS256": return verifyRs256(jsonWebSignature, verifyOptions); case "ES256": return verifyEs256(jsonWebSignature, verifyOptions); default: - throw new VerificationException("Unexpected signing algorithm: expected either RS256 or ES256"); + throw new VerificationException( + "Unexpected signing algorithm: expected either RS256 or ES256"); } } - private static boolean verifyEs256(JsonWebSignature jsonWebSignature, VerifyOptions verifyOptions) throws VerificationException { - String certsUrl = verifyOptions.getCertificatesLocation() == null ? - IAP_CERT_URL : - verifyOptions.getCertificatesLocation(); + private static boolean verifyEs256(JsonWebSignature jsonWebSignature, VerifyOptions verifyOptions) + throws VerificationException { + String certsUrl = + verifyOptions.getCertificatesLocation() == null + ? IAP_CERT_URL + : verifyOptions.getCertificatesLocation(); PublicKey publicKey = verifyOptions.getPublicKey(); if (publicKey == null) { try { @@ -242,10 +271,12 @@ private static boolean verifyEs256(JsonWebSignature jsonWebSignature, VerifyOpti } } - private static boolean verifyRs256(JsonWebSignature jsonWebSignature, VerifyOptions verifyOptions) throws VerificationException { - String certsUrl = verifyOptions.getCertificatesLocation() == null ? - FEDERATED_SIGNON_CERT_URL : - verifyOptions.getCertificatesLocation(); + private static boolean verifyRs256(JsonWebSignature jsonWebSignature, VerifyOptions verifyOptions) + throws VerificationException { + String certsUrl = + verifyOptions.getCertificatesLocation() == null + ? FEDERATED_SIGNON_CERT_URL + : verifyOptions.getCertificatesLocation(); PublicKey publicKey = verifyOptions.getPublicKey(); if (publicKey == null) { try { @@ -255,7 +286,9 @@ private static boolean verifyRs256(JsonWebSignature jsonWebSignature, VerifyOpti } } if (publicKey == 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 { return jsonWebSignature.verifySignature(publicKey); @@ -277,6 +310,7 @@ public static boolean verify(String token) throws VerificationException { 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); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 8dd374034..3dbf57c44 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -1,4 +1,4 @@ -/** +/* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,7 +13,6 @@ * 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; @@ -34,6 +33,11 @@ public void verifyEs256Token() throws TokenVerifier.VerificationException { @Test public void verifyRs256Token() throws TokenVerifier.VerificationException { - assertTrue(TokenVerifier.verify(RS256_TOKEN)); + assertTrue( + TokenVerifier.verify( + RS256_TOKEN, + TokenVerifier.VerifyOptions.newBuilder() + .setCertificatesLocation("https://www.googleapis.com/oauth2/v1/certs") + .build())); } } From c2c883f72a0fa9ab759c9eeadc84e74f24bfc705 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 23 Apr 2020 00:05:55 -0700 Subject: [PATCH 4/8] chore: format and add test for x509 certificate format --- .../com/google/auth/oauth2/TokenVerifier.java | 24 ++++++++++++------- .../google/auth/oauth2/TokenVerifierTest.java | 6 +++++ 2 files changed, 22 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 c9e05766e..2f417e059 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -13,6 +13,7 @@ * 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; @@ -42,10 +43,15 @@ import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; -import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; -import java.security.spec.*; +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; @@ -107,6 +113,7 @@ public Map load(String certificateUrl) throws Exception { 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)); @@ -127,7 +134,8 @@ public Map load(String certificateUrl) throws Exception { } private PublicKey buildPublicKey(JsonWebKey key) - throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException { + throws NoSuchAlgorithmException, InvalidParameterSpecException, + InvalidKeySpecException { if ("ES256".equals(key.alg)) { return buildEs256PublicKey(key); } else if ("RS256".equals((key.alg))) { @@ -139,10 +147,9 @@ private PublicKey buildPublicKey(JsonWebKey key) private PublicKey buildPublicKey(String publicPem) throws CertificateException, UnsupportedEncodingException { - CertificateFactory f = CertificateFactory.getInstance("X.509"); - Certificate certificate = - f.generateCertificate(new ByteArrayInputStream(publicPem.getBytes("UTF-8"))); - return certificate.getPublicKey(); + return CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(publicPem.getBytes("UTF-8"))) + .getPublicKey(); } private PublicKey buildRs256PublicKey(JsonWebKey key) @@ -160,7 +167,8 @@ private PublicKey buildRs256PublicKey(JsonWebKey key) } private PublicKey buildEs256PublicKey(JsonWebKey key) - throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException { + throws NoSuchAlgorithmException, InvalidParameterSpecException, + InvalidKeySpecException { Preconditions.checkArgument("EC".equals(key.kty)); Preconditions.checkArgument("P-256".equals(key.crv)); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 3dbf57c44..85a6acd76 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -33,6 +33,12 @@ public void verifyEs256Token() throws TokenVerifier.VerificationException { @Test public void verifyRs256Token() throws TokenVerifier.VerificationException { + assertTrue(TokenVerifier.verify(RS256_TOKEN)); + } + + @Test + public void verifyRs256TokenWithLegacyCertificateUrlFormat() + throws TokenVerifier.VerificationException { assertTrue( TokenVerifier.verify( RS256_TOKEN, From c504cd2af5e6ef6cc8b1888ffb7b69950cae6792 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 23 Apr 2020 00:32:55 -0700 Subject: [PATCH 5/8] test: add test for service account jwt tokens, fix federated signon sample token --- .../google/auth/oauth2/TokenVerifierTest.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 85a6acd76..6f74db0af 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -23,8 +23,14 @@ public class TokenVerifierTest { private static final String ES256_TOKEN = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ.yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA"; - private static final String RS256_TOKEN = - "eyJhbGciOiJSUzI1NiIsImtpZCI6IjM0OTRiMWU3ODZjZGFkMDkyZTQyMzc2NmJiZTM3ZjU0ZWQ4N2IyMmQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhdWQiOiJodHRwczovL2Zvby5iYXIiLCJhenAiOiJzdmMtMi00MjlAbWluZXJhbC1taW51dGlhLTgyMC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInN1YiI6IjEwMDE0NzEwNjk5Njc2NDQ3OTA4NSIsImVtYWlsIjoic3ZjLTItNDI5QG1pbmVyYWwtbWludXRpYS04MjAuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWF0IjoxNTY1Mzg3NTM4LCJleHAiOjE1NjUzOTExMzh9.foo"; + 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"; + @Test public void verifyEs256Token() throws TokenVerifier.VerificationException { @@ -33,7 +39,7 @@ public void verifyEs256Token() throws TokenVerifier.VerificationException { @Test public void verifyRs256Token() throws TokenVerifier.VerificationException { - assertTrue(TokenVerifier.verify(RS256_TOKEN)); + assertTrue(TokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); } @Test @@ -41,9 +47,19 @@ public void verifyRs256TokenWithLegacyCertificateUrlFormat() throws TokenVerifier.VerificationException { assertTrue( TokenVerifier.verify( - RS256_TOKEN, + FEDERATED_SIGNON_RS256_TOKEN, + TokenVerifier.VerifyOptions.newBuilder() + .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) + .build())); + } + + @Test + public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationException { + assertTrue( + TokenVerifier.verify( + SERVICE_ACCOUNT_RS256_TOKEN, TokenVerifier.VerifyOptions.newBuilder() - .setCertificatesLocation("https://www.googleapis.com/oauth2/v1/certs") + .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) .build())); } } From e3ded090c3086206b9d8e563b7cb7fb80b05eed7 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 23 Apr 2020 08:54:38 -0700 Subject: [PATCH 6/8] feat: verify expiration time and add tests for verifying issuer/audience --- .../com/google/auth/oauth2/TokenVerifier.java | 19 +++++++- .../google/auth/oauth2/TokenVerifierTest.java | 47 ++++++++++++++++++- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index 2f417e059..7e2e06701 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -198,8 +198,10 @@ public abstract static class VerifyOptions { @Nullable abstract PublicKey getPublicKey(); + abstract boolean getValidateExpiration(); + static Builder newBuilder() { - return new AutoValue_TokenVerifier_VerifyOptions.Builder(); + return new AutoValue_TokenVerifier_VerifyOptions.Builder().setValidateExpiration(true); } @AutoValue.Builder @@ -212,6 +214,8 @@ abstract static class Builder { abstract Builder setPublicKey(PublicKey publicKey); + abstract Builder setValidateExpiration(boolean validateExpiration); + abstract VerifyOptions build(); } } @@ -234,15 +238,26 @@ public static boolean verify(String token, VerifyOptions verifyOptions) } catch (IOException e) { throw new VerificationException("Error parsing JsonWebSignature token", e); } + + // Verify the expected audience if an audience is provided in the verifyOptions if (verifyOptions.getAudience() != null && !verifyOptions.getAudience().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 (verifyOptions.getIssuer() != null && !verifyOptions.getIssuer().equals(jsonWebSignature.getPayload().getIssuer())) { throw new VerificationException("Expected issuer does not match"); } + if (verifyOptions.getValidateExpiration()) { + Long expiresAt = jsonWebSignature.getPayload().getExpirationTimeSeconds(); + if (expiresAt != null && expiresAt <= System.currentTimeMillis() / 1000) { + throw new VerificationException("Token is expired"); + } + } + switch (jsonWebSignature.getHeader().getAlgorithm()) { case "RS256": return verifyRs256(jsonWebSignature, verifyOptions); @@ -310,7 +325,7 @@ private static boolean verifyRs256(JsonWebSignature jsonWebSignature, VerifyOpti * * @param token The JWS token expressed as a string * @return true if we can verify the provided token against Google's tokens - * @throws IOException if the provided token string cannot be parsed as a valid JsonWebSignature + * @throws VerificationException if the provided token string cannot be parsed as a valid JsonWebSignature */ public static boolean verify(String token) throws VerificationException { return verify(token, VerifyOptions.newBuilder().build()); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 6f74db0af..5f62826db 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -16,9 +16,13 @@ package com.google.auth.oauth2; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import org.junit.Test; +import java.util.Arrays; +import java.util.List; + public class TokenVerifierTest { private static final String ES256_TOKEN = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ.yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA"; @@ -31,15 +35,52 @@ public class TokenVerifierTest { "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); + + @Test + public void verifyExpiredToken() { + for (String token : ALL_TOKENS) { + try { + TokenVerifier.verify(token); + fail("Should have thrown a VerificationException"); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("expired")); + } + } + } + + @Test + public void verifyExpectedAudience() { + for (String token : ALL_TOKENS) { + try { + TokenVerifier.verify(token, TokenVerifier.VerifyOptions.newBuilder().setAudience("expected audience").build()); + fail("Should have thrown a VerificationException"); + } catch (TokenVerifier.VerificationException e) { + assertTrue(e.getMessage().contains("audience does not match")); + } + } + } + + @Test + public void verifyExpectedIssuer() { + for (String token : ALL_TOKENS) { + try { + TokenVerifier.verify(token, TokenVerifier.VerifyOptions.newBuilder().setIssuer("expected issuer").build()); + 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 { - assertTrue(TokenVerifier.verify(ES256_TOKEN)); + assertTrue(TokenVerifier.verify(ES256_TOKEN, TokenVerifier.VerifyOptions.newBuilder().setValidateExpiration(false).build())); } @Test public void verifyRs256Token() throws TokenVerifier.VerificationException { - assertTrue(TokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN)); + assertTrue(TokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN, TokenVerifier.VerifyOptions.newBuilder().setValidateExpiration(false).build())); } @Test @@ -50,6 +91,7 @@ public void verifyRs256TokenWithLegacyCertificateUrlFormat() FEDERATED_SIGNON_RS256_TOKEN, TokenVerifier.VerifyOptions.newBuilder() .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) + .setValidateExpiration(false) .build())); } @@ -60,6 +102,7 @@ public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationEx SERVICE_ACCOUNT_RS256_TOKEN, TokenVerifier.VerifyOptions.newBuilder() .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) + .setValidateExpiration(false) .build())); } } From e9c273695d1744f1170c4efe98146ab5ac531e64 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 23 Apr 2020 08:55:00 -0700 Subject: [PATCH 7/8] chore: lint --- .../com/google/auth/oauth2/TokenVerifier.java | 3 +- .../google/auth/oauth2/TokenVerifierTest.java | 29 +++++++++++++------ 2 files changed, 22 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 7e2e06701..0f129db9f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -325,7 +325,8 @@ private static boolean verifyRs256(JsonWebSignature jsonWebSignature, VerifyOpti * * @param token The JWS token expressed as a string * @return true if we can verify the provided token against Google's tokens - * @throws VerificationException if the provided token string cannot be parsed as a valid JsonWebSignature + * @throws VerificationException if the provided token string cannot be parsed as a valid + * JsonWebSignature */ public static boolean verify(String token) throws VerificationException { return verify(token, VerifyOptions.newBuilder().build()); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 5f62826db..31d7c29ed 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -18,10 +18,9 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.junit.Test; - import java.util.Arrays; import java.util.List; +import org.junit.Test; public class TokenVerifierTest { private static final String ES256_TOKEN = @@ -29,13 +28,16 @@ public class TokenVerifierTest { 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 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 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); + private static final List ALL_TOKENS = + Arrays.asList(ES256_TOKEN, FEDERATED_SIGNON_RS256_TOKEN, SERVICE_ACCOUNT_RS256_TOKEN); @Test public void verifyExpiredToken() { @@ -53,7 +55,9 @@ public void verifyExpiredToken() { public void verifyExpectedAudience() { for (String token : ALL_TOKENS) { try { - TokenVerifier.verify(token, TokenVerifier.VerifyOptions.newBuilder().setAudience("expected audience").build()); + TokenVerifier.verify( + token, + TokenVerifier.VerifyOptions.newBuilder().setAudience("expected audience").build()); fail("Should have thrown a VerificationException"); } catch (TokenVerifier.VerificationException e) { assertTrue(e.getMessage().contains("audience does not match")); @@ -65,7 +69,8 @@ public void verifyExpectedAudience() { public void verifyExpectedIssuer() { for (String token : ALL_TOKENS) { try { - TokenVerifier.verify(token, TokenVerifier.VerifyOptions.newBuilder().setIssuer("expected issuer").build()); + TokenVerifier.verify( + token, TokenVerifier.VerifyOptions.newBuilder().setIssuer("expected issuer").build()); fail("Should have thrown a VerificationException"); } catch (TokenVerifier.VerificationException e) { assertTrue(e.getMessage().contains("issuer does not match")); @@ -75,12 +80,18 @@ public void verifyExpectedIssuer() { @Test public void verifyEs256Token() throws TokenVerifier.VerificationException { - assertTrue(TokenVerifier.verify(ES256_TOKEN, TokenVerifier.VerifyOptions.newBuilder().setValidateExpiration(false).build())); + assertTrue( + TokenVerifier.verify( + ES256_TOKEN, + TokenVerifier.VerifyOptions.newBuilder().setValidateExpiration(false).build())); } @Test public void verifyRs256Token() throws TokenVerifier.VerificationException { - assertTrue(TokenVerifier.verify(FEDERATED_SIGNON_RS256_TOKEN, TokenVerifier.VerifyOptions.newBuilder().setValidateExpiration(false).build())); + assertTrue( + TokenVerifier.verify( + FEDERATED_SIGNON_RS256_TOKEN, + TokenVerifier.VerifyOptions.newBuilder().setValidateExpiration(false).build())); } @Test From 3b128979da9105e8798110cb561563cb234fd245 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 23 Apr 2020 09:34:32 -0700 Subject: [PATCH 8/8] refactor: inject a Clock rather than a boolean for skipping expiration validation --- .../com/google/auth/oauth2/TokenVerifier.java | 15 +++++++-------- .../google/auth/oauth2/TokenVerifierTest.java | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java index 0f129db9f..bd2fc3e83 100644 --- a/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java +++ b/oauth2_http/java/com/google/auth/oauth2/TokenVerifier.java @@ -23,6 +23,7 @@ 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.auto.value.AutoValue; @@ -198,10 +199,10 @@ public abstract static class VerifyOptions { @Nullable abstract PublicKey getPublicKey(); - abstract boolean getValidateExpiration(); + abstract Clock getClock(); static Builder newBuilder() { - return new AutoValue_TokenVerifier_VerifyOptions.Builder().setValidateExpiration(true); + return new AutoValue_TokenVerifier_VerifyOptions.Builder().setClock(Clock.SYSTEM); } @AutoValue.Builder @@ -214,7 +215,7 @@ abstract static class Builder { abstract Builder setPublicKey(PublicKey publicKey); - abstract Builder setValidateExpiration(boolean validateExpiration); + abstract Builder setClock(Clock clock); abstract VerifyOptions build(); } @@ -251,11 +252,9 @@ public static boolean verify(String token, VerifyOptions verifyOptions) throw new VerificationException("Expected issuer does not match"); } - if (verifyOptions.getValidateExpiration()) { - Long expiresAt = jsonWebSignature.getPayload().getExpirationTimeSeconds(); - if (expiresAt != null && expiresAt <= System.currentTimeMillis() / 1000) { - throw new VerificationException("Token is expired"); - } + Long expiresAt = jsonWebSignature.getPayload().getExpirationTimeSeconds(); + if (expiresAt != null && expiresAt <= verifyOptions.getClock().currentTimeMillis() / 1000) { + throw new VerificationException("Token is expired"); } switch (jsonWebSignature.getHeader().getAlgorithm()) { diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java index 31d7c29ed..77cd0774a 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/TokenVerifierTest.java @@ -20,6 +20,8 @@ import java.util.Arrays; import java.util.List; + +import com.google.api.client.util.Clock; import org.junit.Test; public class TokenVerifierTest { @@ -39,6 +41,14 @@ public class TokenVerifierTest { 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) { @@ -83,7 +93,7 @@ public void verifyEs256Token() throws TokenVerifier.VerificationException { assertTrue( TokenVerifier.verify( ES256_TOKEN, - TokenVerifier.VerifyOptions.newBuilder().setValidateExpiration(false).build())); + TokenVerifier.VerifyOptions.newBuilder().setClock(FIXED_CLOCK).build())); } @Test @@ -91,7 +101,7 @@ public void verifyRs256Token() throws TokenVerifier.VerificationException { assertTrue( TokenVerifier.verify( FEDERATED_SIGNON_RS256_TOKEN, - TokenVerifier.VerifyOptions.newBuilder().setValidateExpiration(false).build())); + TokenVerifier.VerifyOptions.newBuilder().setClock(FIXED_CLOCK).build())); } @Test @@ -102,7 +112,7 @@ public void verifyRs256TokenWithLegacyCertificateUrlFormat() FEDERATED_SIGNON_RS256_TOKEN, TokenVerifier.VerifyOptions.newBuilder() .setCertificatesLocation(LEGACY_FEDERATED_SIGNON_CERT_URL) - .setValidateExpiration(false) + .setClock(FIXED_CLOCK) .build())); } @@ -113,7 +123,7 @@ public void verifyServiceAccountRs256Token() throws TokenVerifier.VerificationEx SERVICE_ACCOUNT_RS256_TOKEN, TokenVerifier.VerifyOptions.newBuilder() .setCertificatesLocation(SERVICE_ACCOUNT_CERT_URL) - .setValidateExpiration(false) + .setClock(FIXED_CLOCK) .build())); } }